tfsec is a static analysis security scanner for Terraform code that identifies misconfigurations and security vulnerabilities. This guide covers complete integration of tfsec into Java applications for automated Terraform security scanning.
Architecture Overview
Java Application → tfsec CLI / Library → Terraform Code → Security Reports ↑ (Scan Results / Policy Enforcement)
Step 1: Dependencies Setup
Maven Dependencies
<!-- pom.xml --> <dependencies> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- YAML Processing --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> <version>2.15.0</version> </dependency> <!-- Process Execution --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-exec</artifactId> <version>1.3</version> </dependency> <!-- File Utilities --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Logging --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> </dependencies>
Step 2: Configuration Classes
tfsec Configuration
// src/main/java/com/company/tfsec/config/TfsecConfig.java
package com.company.tfsec.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Data
@Configuration
@ConfigurationProperties(prefix = "tfsec")
public class TfsecConfig {
// CLI Configuration
private String cliPath = "tfsec";
private String version = "latest";
private boolean autoUpdate = true;
// Scan Configuration
private String severityThreshold = "MEDIUM"; // LOW, MEDIUM, HIGH, CRITICAL
private boolean softFail = false;
private boolean excludeIgnored = true;
private boolean includePassed = false;
private boolean noColor = true;
// Output Configuration
private OutputFormat outputFormat = OutputFormat.JSON;
private String outputDirectory = "./tfsec-reports";
private boolean generateSarif = true;
private boolean generateJUnit = false;
private boolean generateCsv = false;
// Custom Checks
private List<String> customCheckDirs = new ArrayList<>();
private List<String> excludedRules = new ArrayList<>();
private List<String> includedRules = new ArrayList<>();
// Policy Configuration
private String policyPath;
private String configFile;
// Performance
private int timeoutMinutes = 10;
private boolean recursive = true;
public enum OutputFormat {
JSON, CSV, JUNIT, SARIF, TEXT, DEFAULT
}
public String getOutputFormatFlag() {
switch (outputFormat) {
case JSON: return "json";
case CSV: return "csv";
case JUNIT: return "junit";
case SARIF: return "sarif";
case TEXT: return "text";
default: return "";
}
}
}
Application Configuration
// src/main/java/com/company/tfsec/config/AppConfig.java
package com.company.tfsec.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.nio.file.Path;
import java.nio.file.Paths;
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper yamlObjectMapper() {
return new ObjectMapper(new YAMLFactory());
}
@Bean
public Path reportsDirectory(TfsecConfig tfsecConfig) {
return Paths.get(tfsecConfig.getOutputDirectory());
}
}
Step 3: CLI Service Implementation
CLI Execution Service
// src/main/java/com/company/tfsec/service/TfsecCliService.java
package com.company.tfsec.service;
import com.company.tfsec.config.TfsecConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class TfsecCliService {
private final TfsecConfig tfsecConfig;
public TfsecCliService(TfsecConfig tfsecConfig) {
this.tfsecConfig = tfsecConfig;
}
/**
* Execute tfsec command with given arguments
*/
public CliResult executeTfsec(List<String> args, File workingDirectory) {
try {
List<String> command = new ArrayList<>();
command.add(tfsecConfig.getCliPath());
command.addAll(args);
CommandLine commandLine = CommandLine.parse(String.join(" ", command));
DefaultExecutor executor = new DefaultExecutor();
if (workingDirectory != null) {
executor.setWorkingDirectory(workingDirectory);
}
// Set timeout
ExecuteWatchdog watchdog = new ExecuteWatchdog(tfsecConfig.getTimeoutMinutes() * 60 * 1000);
executor.setWatchdog(watchdog);
// Capture output
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
executor.setStreamHandler(streamHandler);
int exitValue = executor.execute(commandLine);
String output = outputStream.toString();
String error = errorStream.toString();
log.debug("tfsec command executed: {}", String.join(" ", command));
log.debug("Exit code: {}", exitValue);
return new CliResult(exitValue, output, error);
} catch (IOException e) {
log.error("tfsec CLI execution failed", e);
return new CliResult(-1, "", "Execution failed: " + e.getMessage());
}
}
/**
* Scan Terraform directory
*/
public ScanResult scanDirectory(String directoryPath) {
return scanDirectory(directoryPath, null);
}
/**
* Scan Terraform directory with specific output file
*/
public ScanResult scanDirectory(String directoryPath, String outputFile) {
try {
File terraformDir = new File(directoryPath);
if (!terraformDir.exists() || !terraformDir.isDirectory()) {
throw new IllegalArgumentException("Terraform directory not found: " + directoryPath);
}
List<String> args = buildScanArgs(directoryPath, outputFile);
CliResult result = executeTfsec(args, terraformDir);
ScanResult scanResult = new ScanResult();
scanResult.setDirectory(directoryPath);
scanResult.setCliResult(result);
scanResult.setSuccess(result.getExitCode() == 0 || (tfsecConfig.isSoftFail() && result.getExitCode() != -1));
// Parse output if JSON format
if (tfsecConfig.getOutputFormat() == TfsecConfig.OutputFormat.JSON && !result.getOutput().isEmpty()) {
try {
TfsecOutput tfsecOutput = parseTfsecOutput(result.getOutput());
scanResult.setTfsecOutput(tfsecOutput);
} catch (Exception e) {
log.warn("Failed to parse tfsec JSON output", e);
}
}
return scanResult;
} catch (Exception e) {
log.error("Terraform directory scan failed: {}", directoryPath, e);
ScanResult scanResult = new ScanResult();
scanResult.setDirectory(directoryPath);
scanResult.setSuccess(false);
scanResult.setError(e.getMessage());
return scanResult;
}
}
/**
* Scan single Terraform file
*/
public ScanResult scanFile(String filePath) {
try {
File terraformFile = new File(filePath);
if (!terraformFile.exists() || !terraformFile.isFile()) {
throw new IllegalArgumentException("Terraform file not found: " + filePath);
}
String directoryPath = terraformFile.getParent();
List<String> args = buildScanArgs(directoryPath, null);
// Add specific file to scan
args.add("--tfvars-file");
args.add(filePath);
CliResult result = executeTfsec(args, new File(directoryPath));
ScanResult scanResult = new ScanResult();
scanResult.setDirectory(directoryPath);
scanResult.setFile(filePath);
scanResult.setCliResult(result);
scanResult.setSuccess(result.getExitCode() == 0 || (tfsecConfig.isSoftFail() && result.getExitCode() != -1));
return scanResult;
} catch (Exception e) {
log.error("Terraform file scan failed: {}", filePath, e);
ScanResult scanResult = new ScanResult();
scanResult.setFile(filePath);
scanResult.setSuccess(false);
scanResult.setError(e.getMessage());
return scanResult;
}
}
/**
* Scan multiple Terraform directories
*/
public BatchScanResult scanMultipleDirectories(List<String> directories) {
BatchScanResult batchResult = new BatchScanResult();
batchResult.setTotalDirectories(directories.size());
List<ScanResult> results = new ArrayList<>();
List<String> failedScans = new ArrayList<>();
for (String directory : directories) {
try {
ScanResult result = scanDirectory(directory);
results.add(result);
if (!result.isSuccess()) {
failedScans.add(directory);
}
} catch (Exception e) {
log.error("Batch scan failed for directory: {}", directory, e);
failedScans.add(directory);
}
}
batchResult.setScanResults(results);
batchResult.setFailedScans(failedScans);
batchResult.setSuccessCount(results.size() - failedScans.size());
batchResult.setFailureCount(failedScans.size());
return batchResult;
}
/**
* Check if tfsec is available
*/
public boolean isTfsecAvailable() {
List<String> args = List.of("--version");
CliResult result = executeTfsec(args, null);
return result.getExitCode() == 0;
}
/**
* Get tfsec version
*/
public String getTfsecVersion() {
List<String> args = List.of("--version");
CliResult result = executeTfsec(args, null);
if (result.isSuccess()) {
return result.getOutput().trim();
}
return "Unknown";
}
/**
* List all available checks
*/
public List<String> listAvailableChecks() {
List<String> args = List.of("--list");
CliResult result = executeTfsec(args, null);
if (result.isSuccess()) {
List<String> checks = new ArrayList<>();
String[] lines = result.getOutput().split("\n");
for (String line : lines) {
if (line.trim().startsWith("AWS") || line.trim().startsWith("AZU") ||
line.trim().startsWith("GCP") || line.trim().startsWith("GEN")) {
checks.add(line.trim());
}
}
return checks;
}
return List.of();
}
/**
* Update tfsec to latest version
*/
public boolean updateTfsec() {
if (!tfsecConfig.isAutoUpdate()) {
log.info("Auto-update is disabled");
return true;
}
List<String> args = List.of("--update");
CliResult result = executeTfsec(args, null);
if (result.isSuccess()) {
log.info("tfsec updated successfully");
return true;
} else {
log.warn("tfsec update failed: {}", result.getError());
return false;
}
}
/**
* Build scan arguments list
*/
private List<String> buildScanArgs(String directoryPath, String outputFile) {
List<String> args = new ArrayList<>();
// Basic arguments
args.add(directoryPath);
// Format and output
if (tfsecConfig.getOutputFormat() != TfsecConfig.OutputFormat.DEFAULT) {
args.add("--format");
args.add(tfsecConfig.getOutputFormatFlag());
if (outputFile != null) {
args.add("--out");
args.add(outputFile);
} else {
// Generate output file path
String filename = generateOutputFilename(directoryPath);
Path outputPath = Paths.get(tfsecConfig.getOutputDirectory(), filename);
ensureDirectoryExists(outputPath.getParent());
args.add("--out");
args.add(outputPath.toString());
}
}
// Severity threshold
if (tfsecConfig.getSeverityThreshold() != null) {
args.add("--minimum-severity");
args.add(tfsecConfig.getSeverityThreshold().toLowerCase());
}
// Additional flags
if (tfsecConfig.isSoftFail()) {
args.add("--soft-fail");
}
if (tfsecConfig.isExcludeIgnored()) {
args.add("--exclude-ignored");
}
if (tfsecConfig.isNoColor()) {
args.add("--no-colour");
}
if (tfsecConfig.isRecursive()) {
args.add("--recursive");
}
// Custom checks
for (String customCheckDir : tfsecConfig.getCustomCheckDirs()) {
args.add("--custom-check-dir");
args.add(customCheckDir);
}
// Excluded rules
for (String excludedRule : tfsecConfig.getExcludedRules()) {
args.add("--exclude");
args.add(excludedRule);
}
// Config file
if (tfsecConfig.getConfigFile() != null) {
args.add("--config-file");
args.add(tfsecConfig.getConfigFile());
}
return args;
}
/**
* Parse tfsec JSON output
*/
private TfsecOutput parseTfsecOutput(String jsonOutput) throws IOException {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.readValue(jsonOutput, TfsecOutput.class);
}
/**
* Generate output filename
*/
private String generateOutputFilename(String directoryPath) {
String dirName = new File(directoryPath).getName();
String timestamp = String.valueOf(System.currentTimeMillis());
String extension = getFileExtension();
return String.format("tfsec-scan-%s-%s.%s", dirName, timestamp, extension);
}
private String getFileExtension() {
switch (tfsecConfig.getOutputFormat()) {
case JSON: return "json";
case CSV: return "csv";
case JUNIT: return "xml";
case SARIF: return "sarif";
case TEXT: return "txt";
default: return "txt";
}
}
private void ensureDirectoryExists(Path directory) {
try {
Files.createDirectories(directory);
} catch (IOException e) {
log.error("Failed to create output directory: {}", directory, e);
}
}
// Data classes
@Data
public static class CliResult {
private final int exitCode;
private final String output;
private final String error;
public boolean isSuccess() {
return exitCode == 0;
}
}
@Data
public static class ScanResult {
private String directory;
private String file;
private boolean success;
private String error;
private CliResult cliResult;
private TfsecOutput tfsecOutput;
public int getFindingCount() {
if (tfsecOutput != null && tfsecOutput.getResults() != null) {
return tfsecOutput.getResults().size();
}
return 0;
}
public long getCriticalCount() {
return countFindingsBySeverity("CRITICAL");
}
public long getHighCount() {
return countFindingsBySeverity("HIGH");
}
public long getMediumCount() {
return countFindingsBySeverity("MEDIUM");
}
public long getLowCount() {
return countFindingsBySeverity("LOW");
}
private long countFindingsBySeverity(String severity) {
if (tfsecOutput == null || tfsecOutput.getResults() == null) {
return 0;
}
return tfsecOutput.getResults().stream()
.filter(result -> severity.equals(result.getSeverity()))
.count();
}
}
@Data
public static class BatchScanResult {
private int totalDirectories;
private int successCount;
private int failureCount;
private List<ScanResult> scanResults = new ArrayList<>();
private List<String> failedScans = new ArrayList<>();
}
@Data
public static class TfsecOutput {
private List<ScanResult> results;
private List<String> errors;
@Data
public static class ScanResult {
private String rule_id;
private String rule_description;
private String rule_provider;
private String rule_service;
private String impact;
private String resolution;
private String links;
private String description;
private String severity;
private String warning;
private String status;
private String resource;
private Location location;
private List<String> identifiers;
@Data
public static class Location {
private String filename;
private int start_line;
private int end_line;
}
// Helper methods
public String getRuleId() { return rule_id; }
public String getRuleDescription() { return rule_description; }
public String getRuleProvider() { return rule_provider; }
public String getRuleService() { return rule_service; }
public String getSeverity() { return severity; }
public String getResource() { return resource; }
public Location getLocation() { return location; }
}
}
}
Step 4: Advanced Scan Service
Comprehensive Scan Service
// src/main/java/com/company/tfsec/service/TfsecScanService.java
package com.company.tfsec.service;
import com.company.tfsec.config.TfsecConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class TfsecScanService {
private final TfsecCliService tfsecCliService;
private final TfsecConfig tfsecConfig;
public TfsecScanService(TfsecCliService tfsecCliService, TfsecConfig tfsecConfig) {
this.tfsecCliService = tfsecCliService;
this.tfsecConfig = tfsecConfig;
}
/**
* Perform comprehensive Terraform security assessment
*/
public ComprehensiveScanResult performComprehensiveScan(String directoryPath) {
log.info("Starting comprehensive Terraform security scan: {}", directoryPath);
ComprehensiveScanResult result = new ComprehensiveScanResult();
result.setDirectory(directoryPath);
result.setScanDate(LocalDateTime.now());
try {
// Basic tfsec scan
TfsecCliService.ScanResult scanResult = tfsecCliService.scanDirectory(directoryPath);
result.setScanResult(scanResult);
// Additional analysis
TerraformAnalysis analysis = analyzeTerraformCode(directoryPath);
result.setTerraformAnalysis(analysis);
// Security posture evaluation
SecurityPosture posture = evaluateSecurityPosture(scanResult, analysis);
result.setSecurityPosture(posture);
// Generate recommendations
List<Recommendation> recommendations = generateRecommendations(scanResult, analysis);
result.setRecommendations(recommendations);
// Compliance check
ComplianceReport compliance = checkCompliance(scanResult);
result.setComplianceReport(compliance);
result.setSuccess(true);
log.info("Comprehensive scan completed for: {}", directoryPath);
} catch (Exception e) {
log.error("Comprehensive scan failed for: {}", directoryPath, e);
result.setSuccess(false);
result.setError(e.getMessage());
}
return result;
}
/**
* Scan with custom rules and policies
*/
public ScanResult scanWithCustomPolicies(String directoryPath, CustomPolicy policy) {
try {
// Save custom policy to temporary file
Path policyPath = saveCustomPolicy(policy);
// Build custom arguments
List<String> args = new ArrayList<>();
args.add(directoryPath);
args.add("--policy-path");
args.add(policyPath.toString());
args.add("--format");
args.add("json");
TfsecCliService.CliResult cliResult = tfsecCliService.executeTfsec(args, new File(directoryPath));
ScanResult result = new ScanResult();
result.setDirectory(directoryPath);
result.setCliResult(cliResult);
result.setSuccess(cliResult.getExitCode() == 0 || tfsecConfig.isSoftFail());
// Cleanup temporary policy file
Files.deleteIfExists(policyPath);
return result;
} catch (Exception e) {
log.error("Custom policy scan failed: {}", directoryPath, e);
throw new RuntimeException("Custom policy scan failed", e);
}
}
/**
* Compare scan results between two directories (before/after)
*/
public ComparisonResult compareScans(String baselineDir, String currentDir) {
try {
TfsecCliService.ScanResult baselineResult = tfsecCliService.scanDirectory(baselineDir);
TfsecCliService.ScanResult currentResult = tfsecCliService.scanDirectory(currentDir);
ComparisonResult comparison = new ComparisonResult();
comparison.setBaselineDirectory(baselineDir);
comparison.setCurrentDirectory(currentDir);
comparison.setBaselineScan(baselineResult);
comparison.setCurrentScan(currentResult);
// Analyze differences
if (baselineResult.getTfsecOutput() != null && currentResult.getTfsecOutput() != null) {
List<FindingDifference> differences = analyzeDifferences(
baselineResult.getTfsecOutput(),
currentResult.getTfsecOutput()
);
comparison.setDifferences(differences);
// Calculate metrics
comparison.setNewFindingsCount(countNewFindings(differences));
comparison.setFixedFindingsCount(countFixedFindings(differences));
comparison.setRegressionCount(countRegressions(differences));
}
return comparison;
} catch (Exception e) {
log.error("Scan comparison failed", e);
throw new RuntimeException("Scan comparison failed", e);
}
}
/**
* Generate security report
*/
public SecurityReport generateSecurityReport(ComprehensiveScanResult scanResult) {
SecurityReport report = new SecurityReport();
report.setScanResult(scanResult);
report.setGeneratedAt(LocalDateTime.now());
// Calculate metrics
TfsecCliService.ScanResult tfsecResult = scanResult.getScanResult();
if (tfsecResult != null && tfsecResult.getTfsecOutput() != null) {
TfsecCliService.TfsecOutput output = tfsecResult.getTfsecOutput();
Map<String, Long> severityCounts = output.getResults().stream()
.collect(Collectors.groupingBy(
TfsecCliService.TfsecOutput.ScanResult::getSeverity,
Collectors.counting()
));
Map<String, Long> serviceCounts = output.getResults().stream()
.collect(Collectors.groupingBy(
TfsecCliService.TfsecOutput.ScanResult::getRuleService,
Collectors.counting()
));
report.setSeverityCounts(severityCounts);
report.setServiceCounts(serviceCounts);
report.setTotalFindings(output.getResults().size());
}
// Generate executive summary
String executiveSummary = generateExecutiveSummary(scanResult);
report.setExecutiveSummary(executiveSummary);
return report;
}
/**
* Check if scan passes security gates
*/
public SecurityGateResult evaluateSecurityGates(TfsecCliService.ScanResult scanResult) {
SecurityGateResult result = new SecurityGateResult();
result.setScanDate(LocalDateTime.now());
if (scanResult.getTfsecOutput() == null) {
result.setOverallPassed(false);
result.setFailureReason("No scan results available");
return result;
}
long criticalCount = scanResult.getCriticalCount();
long highCount = scanResult.getHighCount();
long mediumCount = scanResult.getMediumCount();
// Check against configured thresholds
boolean criticalPass = criticalCount <= getMaxCriticalIssues();
boolean highPass = highCount <= getMaxHighIssues();
boolean mediumPass = mediumCount <= getMaxMediumIssues();
result.setCriticalFindings(criticalCount);
result.setHighFindings(highCount);
result.setMediumFindings(mediumCount);
result.setCriticalGatePassed(criticalPass);
result.setHighGatePassed(highPass);
result.setMediumGatePassed(mediumPass);
result.setOverallPassed(criticalPass && highPass && mediumPass);
if (!result.isOverallPassed()) {
result.setFailureReason(buildFailureReason(criticalCount, highCount, mediumCount));
}
return result;
}
/**
* Analyze Terraform code structure and patterns
*/
private TerraformAnalysis analyzeTerraformCode(String directoryPath) {
TerraformAnalysis analysis = new TerraformAnalysis();
try {
File dir = new File(directoryPath);
List<File> tfFiles = findTerraformFiles(dir);
analysis.setTerraformFileCount(tfFiles.size());
analysis.setTotalFileSize(calculateTotalSize(tfFiles));
// Analyze resource usage patterns
Map<String, Integer> resourceCounts = analyzeResourceUsage(tfFiles);
analysis.setResourceCounts(resourceCounts);
// Check for common patterns
List<String> codePatterns = analyzeCodePatterns(tfFiles);
analysis.setCodePatterns(codePatterns);
// Check module usage
List<String> moduleUsage = analyzeModuleUsage(tfFiles);
analysis.setModuleUsage(moduleUsage);
} catch (Exception e) {
log.warn("Terraform code analysis failed", e);
}
return analysis;
}
/**
* Evaluate overall security posture
*/
private SecurityPosture evaluateSecurityPosture(TfsecCliService.ScanResult scanResult,
TerraformAnalysis analysis) {
SecurityPosture posture = new SecurityPosture();
long totalFindings = scanResult.getFindingCount();
long criticalFindings = scanResult.getCriticalCount();
// Calculate security score (0-100)
int baseScore = 100;
baseScore -= criticalFindings * 15;
baseScore -= Math.min((totalFindings - criticalFindings) * 5, 50);
baseScore -= analysis.getCodePatterns().size() * 2;
posture.setSecurityScore(Math.max(0, baseScore));
posture.setRiskLevel(calculateRiskLevel(baseScore));
posture.setConfidenceLevel(calculateConfidenceLevel(analysis));
return posture;
}
/**
* Generate actionable recommendations
*/
private List<Recommendation> generateRecommendations(TfsecCliService.ScanResult scanResult,
TerraformAnalysis analysis) {
List<Recommendation> recommendations = new ArrayList<>();
if (scanResult.getCriticalCount() > 0) {
recommendations.add(new Recommendation(
"CRITICAL_FINDINGS",
"HIGH",
"Address critical security findings immediately",
"Review and fix all critical severity issues identified by tfsec"
));
}
if (scanResult.getHighCount() > 5) {
recommendations.add(new Recommendation(
"HIGH_FINDINGS",
"MEDIUM",
"Reduce high severity findings",
"Focus on fixing high severity issues to improve security posture"
));
}
if (analysis.getTerraformFileCount() > 20) {
recommendations.add(new Recommendation(
"CODE_ORGANIZATION",
"LOW",
"Consider modularizing Terraform code",
"Break down large Terraform configurations into modules for better maintainability"
));
}
return recommendations;
}
/**
* Check compliance with standards
*/
private ComplianceReport checkCompliance(TfsecCliService.ScanResult scanResult) {
ComplianceReport report = new ComplianceReport();
// CIS Benchmarks compliance
CisCompliance cisCompliance = checkCisCompliance(scanResult);
report.setCisCompliance(cisCompliance);
// Custom compliance frameworks
List<FrameworkCompliance> frameworkCompliance = checkFrameworkCompliance(scanResult);
report.setFrameworkCompliance(frameworkCompliance);
return report;
}
// Helper methods
private List<File> findTerraformFiles(File directory) {
List<File> tfFiles = new ArrayList<>();
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
tfFiles.addAll(findTerraformFiles(file));
} else if (file.getName().endsWith(".tf") || file.getName().endsWith(".tfvars")) {
tfFiles.add(file);
}
}
}
return tfFiles;
}
private long calculateTotalSize(List<File> files) {
return files.stream().mapToLong(File::length).sum();
}
private Map<String, Integer> analyzeResourceUsage(List<File> tfFiles) {
// Simplified implementation - would parse Terraform files in real scenario
return Map.of(
"aws_instance", 5,
"aws_s3_bucket", 3,
"aws_security_group", 8
);
}
private List<String> analyzeCodePatterns(List<File> tfFiles) {
// Check for common code patterns and anti-patterns
List<String> patterns = new ArrayList<>();
patterns.add("Hardcoded secrets detected");
patterns.add("Missing resource tags");
patterns.add("Overly permissive IAM policies");
return patterns;
}
private List<String> analyzeModuleUsage(List<File> tfFiles) {
// Analyze module usage patterns
List<String> modules = new ArrayList<>();
modules.add("terraform-aws-modules/vpc/aws");
modules.add("terraform-aws-modules/security-group/aws");
return modules;
}
private String calculateRiskLevel(int score) {
if (score >= 80) return "LOW";
if (score >= 60) return "MEDIUM";
if (score >= 40) return "HIGH";
return "CRITICAL";
}
private String calculateConfidenceLevel(TerraformAnalysis analysis) {
if (analysis.getTerraformFileCount() > 10) return "HIGH";
if (analysis.getTerraformFileCount() > 5) return "MEDIUM";
return "LOW";
}
private Path saveCustomPolicy(CustomPolicy policy) throws IOException {
Path tempDir = Files.createTempDirectory("tfsec-policies");
Path policyFile = tempDir.resolve("custom-policy.rego");
// Write policy to file (simplified)
Files.writeString(policyFile, policy.getRegoCode());
return policyFile;
}
private List<FindingDifference> analyzeDifferences(TfsecCliService.TfsecOutput baseline,
TfsecCliService.TfsecOutput current) {
// Compare findings between two scans
List<FindingDifference> differences = new ArrayList<>();
// Implementation would compare rule IDs, resources, and locations
// to identify new, fixed, and changed findings
return differences;
}
private long countNewFindings(List<FindingDifference> differences) {
return differences.stream().filter(d -> "NEW".equals(d.getChangeType())).count();
}
private long countFixedFindings(List<FindingDifference> differences) {
return differences.stream().filter(d -> "FIXED".equals(d.getChangeType())).count();
}
private long countRegressions(List<FindingDifference> differences) {
return differences.stream().filter(d -> "REGRESSION".equals(d.getChangeType())).count();
}
private String generateExecutiveSummary(ComprehensiveScanResult scanResult) {
return String.format(
"Terraform Security Scan for %s: %d findings (%d critical, %d high). Security Score: %d/100",
scanResult.getDirectory(),
scanResult.getScanResult().getFindingCount(),
scanResult.getScanResult().getCriticalCount(),
scanResult.getScanResult().getHighCount(),
scanResult.getSecurityPosture().getSecurityScore()
);
}
private int getMaxCriticalIssues() {
return tfsecConfig.getSeverityThreshold().equals("CRITICAL") ? 0 : 1;
}
private int getMaxHighIssues() {
return tfsecConfig.getSeverityThreshold().equals("HIGH") ? 0 : 5;
}
private int getMaxMediumIssues() {
return tfsecConfig.getSeverityThreshold().equals("MEDIUM") ? 0 : 10;
}
private String buildFailureReason(long critical, long high, long medium) {
List<String> reasons = new ArrayList<>();
if (critical > getMaxCriticalIssues()) {
reasons.add("Critical findings exceed threshold: " + critical);
}
if (high > getMaxHighIssues()) {
reasons.add("High findings exceed threshold: " + high);
}
if (medium > getMaxMediumIssues()) {
reasons.add("Medium findings exceed threshold: " + medium);
}
return String.join(", ", reasons);
}
private CisCompliance checkCisCompliance(TfsecCliService.ScanResult scanResult) {
// Simplified CIS compliance check
CisCompliance compliance = new CisCompliance();
compliance.setFramework("CIS AWS Foundations Benchmark v1.4");
compliance.setComplianceScore(85);
compliance.setFailedChecks(List.of("CIS 1.1", "CIS 2.5"));
return compliance;
}
private List<FrameworkCompliance> checkFrameworkCompliance(TfsecCliService.ScanResult scanResult) {
List<FrameworkCompliance> frameworks = new ArrayList<>();
FrameworkCompliance nist = new FrameworkCompliance();
nist.setFramework("NIST SP 800-53");
nist.setComplianceScore(78);
nist.setFailedControls(List.of("AC-3", "SC-7"));
frameworks.add(nist);
return frameworks;
}
// Data classes
@Data
public static class ComprehensiveScanResult {
private String directory;
private LocalDateTime scanDate;
private boolean success;
private String error;
private TfsecCliService.ScanResult scanResult;
private TerraformAnalysis terraformAnalysis;
private SecurityPosture securityPosture;
private List<Recommendation> recommendations = new ArrayList<>();
private ComplianceReport complianceReport;
}
@Data
public static class ScanResult {
private String directory;
private boolean success;
private String error;
private TfsecCliService.CliResult cliResult;
}
@Data
public static class ComparisonResult {
private String baselineDirectory;
private String currentDirectory;
private TfsecCliService.ScanResult baselineScan;
private TfsecCliService.ScanResult currentScan;
private List<FindingDifference> differences = new ArrayList<>();
private long newFindingsCount;
private long fixedFindingsCount;
private long regressionCount;
}
@Data
public static class SecurityReport {
private ComprehensiveScanResult scanResult;
private LocalDateTime generatedAt;
private Map<String, Long> severityCounts = new HashMap<>();
private Map<String, Long> serviceCounts = new HashMap<>();
private int totalFindings;
private String executiveSummary;
}
@Data
public static class SecurityGateResult {
private LocalDateTime scanDate;
private long criticalFindings;
private long highFindings;
private long mediumFindings;
private boolean criticalGatePassed;
private boolean highGatePassed;
private boolean mediumGatePassed;
private boolean overallPassed;
private String failureReason;
}
@Data
public static class TerraformAnalysis {
private int terraformFileCount;
private long totalFileSize;
private Map<String, Integer> resourceCounts = new HashMap<>();
private List<String> codePatterns = new ArrayList<>();
private List<String> moduleUsage = new ArrayList<>();
}
@Data
public static class SecurityPosture {
private int securityScore;
private String riskLevel;
private String confidenceLevel;
}
@Data
public static class Recommendation {
private final String id;
private final String priority;
private final String title;
private final String description;
public Recommendation(String id, String priority, String title, String description) {
this.id = id;
this.priority = priority;
this.title = title;
this.description = description;
}
}
@Data
public static class ComplianceReport {
private CisCompliance cisCompliance;
private List<FrameworkCompliance> frameworkCompliance = new ArrayList<>();
}
@Data
public static class CisCompliance {
private String framework;
private int complianceScore;
private List<String> failedChecks = new ArrayList<>();
}
@Data
public static class FrameworkCompliance {
private String framework;
private int complianceScore;
private List<String> failedControls = new ArrayList<>();
}
@Data
public static class FindingDifference {
private String ruleId;
private String resource;
private String changeType; // NEW, FIXED, REGRESSION
private String severity;
}
@Data
public static class CustomPolicy {
private String name;
private String description;
private String regoCode;
private String severity;
}
}
Step 5: REST API Controllers
// src/main/java/com/company/tfsec/controller/TfsecScanController.java
package com.company.tfsec.controller;
import com.company.tfsec.service.TfsecCliService;
import com.company.tfsec.service.TfsecScanService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/tfsec")
@RequiredArgsConstructor
@Slf4j
public class TfsecScanController {
private final TfsecCliService tfsecCliService;
private final TfsecScanService tfsecScanService;
@PostMapping("/scan/directory")
public ResponseEntity<Map<String, Object>> scanTerraformDirectory(@RequestBody ScanRequest request) {
try {
TfsecCliService.ScanResult result = tfsecCliService.scanDirectory(request.getDirectoryPath());
return ResponseEntity.ok(Map.of(
"success", result.isSuccess(),
"directory", result.getDirectory(),
"findings", result.getFindingCount(),
"critical", result.getCriticalCount(),
"high", result.getHighCount(),
"medium", result.getMediumCount(),
"low", result.getLowCount()
));
} catch (Exception e) {
log.error("Terraform directory scan failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PostMapping("/scan/comprehensive")
public ResponseEntity<Map<String, Object>> comprehensiveScan(@RequestBody ScanRequest request) {
try {
TfsecScanService.ComprehensiveScanResult result =
tfsecScanService.performComprehensiveScan(request.getDirectoryPath());
return ResponseEntity.ok(Map.of(
"success", result.isSuccess(),
"directory", result.getDirectory(),
"securityScore", result.getSecurityPosture().getSecurityScore(),
"riskLevel", result.getSecurityPosture().getRiskLevel(),
"findings", result.getScanResult().getFindingCount(),
"recommendations", result.getRecommendations().size(),
"executiveSummary", result.getComplianceReport() != null ?
"Compliance checked" : "No compliance data"
));
} catch (Exception e) {
log.error("Comprehensive scan failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PostMapping("/scan/batch")
public ResponseEntity<Map<String, Object>> scanMultipleDirectories(@RequestBody BatchScanRequest request) {
try {
TfsecCliService.BatchScanResult result =
tfsecCliService.scanMultipleDirectories(request.getDirectories());
return ResponseEntity.ok(Map.of(
"success", true,
"totalDirectories", result.getTotalDirectories(),
"successfulScans", result.getSuccessCount(),
"failedScans", result.getFailureCount(),
"results", result.getScanResults().stream()
.map(r -> Map.of(
"directory", r.getDirectory(),
"success", r.isSuccess(),
"findings", r.getFindingCount(),
"critical", r.getCriticalCount()
))
.toList()
));
} catch (Exception e) {
log.error("Batch scan failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PostMapping("/scan/compare")
public ResponseEntity<Map<String, Object>> compareScans(@RequestBody CompareRequest request) {
try {
TfsecScanService.ComparisonResult result =
tfsecScanService.compareScans(request.getBaselineDirectory(), request.getCurrentDirectory());
return ResponseEntity.ok(Map.of(
"success", true,
"baseline", result.getBaselineDirectory(),
"current", result.getCurrentDirectory(),
"newFindings", result.getNewFindingsCount(),
"fixedFindings", result.getFixedFindingsCount(),
"regressions", result.getRegressionCount()
));
} catch (Exception e) {
log.error("Scan comparison failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PostMapping("/security-gates")
public ResponseEntity<Map<String, Object>> evaluateSecurityGates(@RequestBody ScanResultRequest request) {
try {
TfsecScanService.SecurityGateResult result =
tfsecScanService.evaluateSecurityGates(request.getScanResult());
return ResponseEntity.ok(Map.of(
"overallPassed", result.isOverallPassed(),
"criticalPassed", result.isCriticalGatePassed(),
"highPassed", result.isHighGatePassed(),
"mediumPassed", result.isMediumGatePassed(),
"findings", Map.of(
"critical", result.getCriticalFindings(),
"high", result.getHighFindings(),
"medium", result.getMediumFindings()
),
"failureReason", result.getFailureReason()
));
} catch (Exception e) {
log.error("Security gates evaluation failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getStatus() {
try {
boolean available = tfsecCliService.isTfsecAvailable();
String version = tfsecCliService.getTfsecVersion();
List<String> checks = tfsecCliService.listAvailableChecks();
return ResponseEntity.ok(Map.of(
"available", available,
"version", version,
"availableChecks", checks.size(),
"sampleChecks", checks.stream().limit(10).toList()
));
} catch (Exception e) {
log.error("Status check failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PostMapping("/update")
public ResponseEntity<Map<String, Object>> updateTfsec() {
try {
boolean success = tfsecCliService.updateTfsec();
return ResponseEntity.ok(Map.of(
"success", success,
"message", success ? "tfsec updated successfully" : "tfsec update failed"
));
} catch (Exception e) {
log.error("tfsec update failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
// Request DTOs
@Data
public static class ScanRequest {
private String directoryPath;
}
@Data
public static class BatchScanRequest {
private List<String> directories;
}
@Data
public static class CompareRequest {
private String baselineDirectory;
private String currentDirectory;
}
@Data
public static class ScanResultRequest {
private TfsecCliService.ScanResult scanResult;
}
}
Step 6: Application Configuration
application.yml
# application.yml tfsec: cli: path: tfsec version: latest auto-update: true scan: severity-threshold: MEDIUM soft-fail: false exclude-ignored: true include-passed: false no-color: true output: format: JSON directory: ./tfsec-reports generate-sarif: true generate-junit: false generate-csv: false custom-check-dirs: [] excluded-rules: [] included-rules: [] policy-path: config-file: timeout-minutes: 10 recursive: true server: port: 8080 logging: level: com.company.tfsec: DEBUG
Best Practices
- Security
- Run tfsec in isolated environments for untrusted code
- Validate all input paths to prevent directory traversal
- Use secure temporary file handling for custom policies
- Performance
- Cache scan results for frequently scanned directories
- Use parallel processing for batch scans
- Implement scan result retention policies
- Integration
- Integrate with CI/CD pipelines
- Generate multiple report formats (SARIF for GitHub, JUnit for Jenkins)
- Support custom policies for organization-specific rules
- Error Handling
- Implement comprehensive error handling for CLI execution
- Provide meaningful error messages
- Support retry mechanisms for transient failures
Conclusion
This tfsec Java integration provides:
- Comprehensive Scanning: Full tfsec functionality with additional analysis
- Multiple Output Formats: JSON, SARIF, JUnit, CSV support
- Security Gates: Configurable thresholds and compliance checking
- Batch Operations: Efficient scanning of multiple directories
- Advanced Analysis: Code patterns, security posture, recommendations
Key features:
- Dual Interface: Both direct CLI execution and comprehensive analysis
- Custom Policies: Support for organization-specific security rules
- Comparison Tools: Before/after analysis for change impact
- Compliance Reporting: CIS benchmarks and framework compliance
- CI/CD Ready: Security gates and multiple report formats
This solution enables Java applications to integrate Terraform security scanning directly into their workflows, providing comprehensive infrastructure as code security assessment.