Overview
Terrascan is a static code analyzer for Infrastructure as Code (IaC) that detects security vulnerabilities and compliance violations. This Java implementation provides a comprehensive Terrascan-like tool for scanning Terraform, Kubernetes, and other IaC files.
1. Dependencies
<dependencies> <!-- YAML Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> <version>2.15.2</version> </dependency> <!-- HCL (Terraform) Parser --> <dependency> <groupId>com.hashicorp</groupId> <artifactId>hcl</artifactId> <version>1.0.0</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20230618</version> </dependency> <!-- File Utilities --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.13.0</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> <!-- Collection Utilities --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> </dependencies>
2. Core Domain Models
Scan Results and Violations
package com.example.terrascan.model; import com.fasterxml.jackson.annotation.JsonInclude; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) public class ScanResult { private String scanId; private String directory; private IaCType iacType; private LocalDateTime scanTimestamp; private List<Violation> violations; private ScanSummary summary; private boolean passed; public ScanResult() { this.violations = new ArrayList<>(); this.scanTimestamp = LocalDateTime.now(); } // Getters and Setters public String getScanId() { return scanId; } public void setScanId(String scanId) { this.scanId = scanId; } public String getDirectory() { return directory; } public void setDirectory(String directory) { this.directory = directory; } public IaCType getIacType() { return iacType; } public void setIacType(IaCType iacType) { this.iacType = iacType; } public LocalDateTime getScanTimestamp() { return scanTimestamp; } public void setScanTimestamp(LocalDateTime scanTimestamp) { this.scanTimestamp = scanTimestamp; } public List<Violation> getViolations() { return violations; } public void setViolations(List<Violation> violations) { this.violations = violations; } public ScanSummary getSummary() { return summary; } public void setSummary(ScanSummary summary) { this.summary = summary; } public boolean isPassed() { return passed; } public void setPassed(boolean passed) { this.passed = passed; } public void addViolation(Violation violation) { this.violations.add(violation); } } @JsonInclude(JsonInclude.Include.NON_NULL) public class Violation { private String ruleId; private String ruleName; private String description; private Severity severity; private String category; private String resourceType; private String resourceName; private String file; private int line; private int column; private String remediation; // Getters and Setters public String getRuleId() { return ruleId; } public void setRuleId(String ruleId) { this.ruleId = ruleId; } public String getRuleName() { return ruleName; } public void setRuleName(String ruleName) { this.ruleName = ruleName; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Severity getSeverity() { return severity; } public void setSeverity(Severity severity) { this.severity = severity; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getResourceType() { return resourceType; } public void setResourceType(String resourceType) { this.resourceType = resourceType; } public String getResourceName() { return resourceName; } public void setResourceName(String resourceName) { this.resourceName = resourceName; } public String getFile() { return file; } public void setFile(String file) { this.file = file; } public int getLine() { return line; } public void setLine(int line) { this.line = line; } public int getColumn() { return column; } public void setColumn(int column) { this.column = column; } public String getRemediation() { return remediation; } public void setRemediation(String remediation) { this.remediation = remediation; } } @JsonInclude(JsonInclude.Include.NON_NULL) public class ScanSummary { private int totalViolations; private int lowViolations; private int mediumViolations; private int highViolations; private int criticalViolations; private int filesScanned; private int resourcesScanned; // Getters and Setters public int getTotalViolations() { return totalViolations; } public void setTotalViolations(int totalViolations) { this.totalViolations = totalViolations; } public int getLowViolations() { return lowViolations; } public void setLowViolations(int lowViolations) { this.lowViolations = lowViolations; } public int getMediumViolations() { return mediumViolations; } public void setMediumViolations(int mediumViolations) { this.mediumViolations = mediumViolations; } public int getHighViolations() { return highViolations; } public void setHighViolations(int highViolations) { this.highViolations = highViolations; } public int getCriticalViolations() { return criticalViolations; } public void setCriticalViolations(int criticalViolations) { this.criticalViolations = criticalViolations; } public int getFilesScanned() { return filesScanned; } public void setFilesScanned(int filesScanned) { this.filesScanned = filesScanned; } public int getResourcesScanned() { return resourcesScanned; } public void setResourcesScanned(int resourcesScanned) { this.resourcesScanned = resourcesScanned; } public void calculateFromViolations(List<Violation> violations) { this.totalViolations = violations.size(); this.lowViolations = (int) violations.stream().filter(v -> v.getSeverity() == Severity.LOW).count(); this.mediumViolations = (int) violations.stream().filter(v -> v.getSeverity() == Severity.MEDIUM).count(); this.highViolations = (int) violations.stream().filter(v -> v.getSeverity() == Severity.HIGH).count(); this.criticalViolations = (int) violations.stream().filter(v -> v.getSeverity() == Severity.CRITICAL).count(); } } public enum Severity { LOW("Low"), MEDIUM("Medium"), HIGH("High"), CRITICAL("Critical"); private final String value; Severity(String value) { this.value = value; } public String getValue() { return value; } public static Severity fromString(String value) { for (Severity severity : values()) { if (severity.value.equalsIgnoreCase(value)) { return severity; } } return LOW; } } public enum IaCType { TERRAFORM("terraform"), KUBERNETES("kubernetes"), CLOUDFORMATION("cloudformation"), DOCKERFILE("dockerfile"), HELM("helm"), AZURE_RESOURCE_MANAGER("arm"), GOOGLE_DEPLOYMENT_MANAGER("gdm"); private final String value; IaCType(String value) { this.value = value; } public String getValue() { return value; } public static IaCType fromString(String value) { for (IaCType type : values()) { if (type.value.equalsIgnoreCase(value)) { return type; } } return TERRAFORM; } } Policy and Rule Models
package com.example.terrascan.model; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.List; import java.util.Map; @JsonInclude(JsonInclude.Include.NON_NULL) public class PolicyRule { private String id; private String name; private String description; private Severity severity; private String category; private String resourceType; private List<String> filePatterns; private RuleCondition condition; private String remediation; private boolean enabled; // Getters and Setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Severity getSeverity() { return severity; } public void setSeverity(Severity severity) { this.severity = severity; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getResourceType() { return resourceType; } public void setResourceType(String resourceType) { this.resourceType = resourceType; } public List<String> getFilePatterns() { return filePatterns; } public void setFilePatterns(List<String> filePatterns) { this.filePatterns = filePatterns; } public RuleCondition getCondition() { return condition; } public void setCondition(RuleCondition condition) { this.condition = condition; } public String getRemediation() { return remediation; } public void setRemediation(String remediation) { this.remediation = remediation; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } @JsonInclude(JsonInclude.Include.NON_NULL) public class RuleCondition { private String type; // "attribute", "resource", "file" private String attribute; private Operator operator; private Object value; private List<RuleCondition> andConditions; private List<RuleCondition> orConditions; // Getters and Setters public String getType() { return type; } public void setType(String type) { this.type = type; } public String getAttribute() { return attribute; } public void setAttribute(String attribute) { this.attribute = attribute; } public Operator getOperator() { return operator; } public void setOperator(Operator operator) { this.operator = operator; } public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } public List<RuleCondition> getAndConditions() { return andConditions; } public void setAndConditions(List<RuleCondition> andConditions) { this.andConditions = andConditions; } public List<RuleCondition> getOrConditions() { return orConditions; } public void setOrConditions(List<RuleCondition> orConditions) { this.orConditions = orConditions; } } public enum Operator { EQUALS("equals"), NOT_EQUALS("not_equals"), CONTAINS("contains"), NOT_CONTAINS("not_contains"), EXISTS("exists"), NOT_EXISTS("not_exists"), GREATER_THAN("greater_than"), LESS_THAN("less_than"), MATCHES_PATTERN("matches_pattern"), IN_LIST("in_list"); private final String value; Operator(String value) { this.value = value; } public String getValue() { return value; } } @JsonInclude(JsonInclude.Include.NON_NULL) public class PolicyBundle { private String name; private String version; private String description; private List<PolicyRule> rules; private Map<String, Object> metadata; // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public List<PolicyRule> getRules() { return rules; } public void setRules(List<PolicyRule> rules) { this.rules = rules; } public Map<String, Object> getMetadata() { return metadata; } public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; } } 3. File Scanner and Parser
package com.example.terrascan.scanner; import com.example.terrascan.model.*; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.*; import java.util.stream.Collectors; import java.util.stream.Stream; public class FileScanner { private static final Logger log = LoggerFactory.getLogger(FileScanner.class); private final ObjectMapper jsonMapper; private final ObjectMapper yamlMapper; public FileScanner() { this.jsonMapper = new ObjectMapper(); this.yamlMapper = new ObjectMapper(new YAMLFactory()); } public List<IaCFile> scanDirectory(String directoryPath, IaCType iacType) throws IOException { List<IaCFile> files = new ArrayList<>(); Path startPath = Paths.get(directoryPath); if (!Files.exists(startPath) || !Files.isDirectory(startPath)) { throw new IOException("Directory does not exist: " + directoryPath); } try (Stream<Path> paths = Files.walk(startPath)) { paths.filter(Files::isRegularFile) .filter(path -> matchesFileType(path, iacType)) .forEach(path -> { try { IaCFile iacFile = parseFile(path, iacType); if (iacFile != null) { files.add(iacFile); } } catch (IOException e) { log.warn("Failed to parse file: {}", path, e); } }); } log.info("Found {} {} files in directory: {}", files.size(), iacType, directoryPath); return files; } private boolean matchesFileType(Path path, IaCType iacType) { String fileName = path.getFileName().toString(); switch (iacType) { case TERRAFORM: return fileName.endsWith(".tf") || fileName.endsWith(".tfvars"); case KUBERNETES: return fileName.endsWith(".yaml") || fileName.endsWith(".yml") || fileName.endsWith(".json"); case CLOUDFORMATION: return fileName.endsWith(".yaml") || fileName.endsWith(".yml") || fileName.endsWith(".json") || fileName.endsWith(".template"); case DOCKERFILE: return fileName.equalsIgnoreCase("Dockerfile") || fileName.startsWith("Dockerfile."); case HELM: return fileName.endsWith(".yaml") || fileName.endsWith(".yml") || path.toString().contains("charts/"); case AZURE_RESOURCE_MANAGER: return fileName.endsWith(".json") && (fileName.contains("azuredeploy") || fileName.contains("template")); case GOOGLE_DEPLOYMENT_MANAGER: return fileName.endsWith(".yaml") || fileName.endsWith(".yml") || fileName.endsWith(".jinja"); default: return false; } } private IaCFile parseFile(Path path, IaCType iacType) throws IOException { String content = Files.readString(path); IaCFile file = new IaCFile(); file.setPath(path.toString()); file.setFileName(path.getFileName().toString()); file.setIacType(iacType); file.setContent(content); try { switch (iacType) { case TERRAFORM: file.setResources(parseTerraformFile(content, path.toString())); break; case KUBERNETES: file.setResources(parseKubernetesFile(content, path.toString())); break; case CLOUDFORMATION: file.setResources(parseCloudFormationFile(content, path.toString())); break; case DOCKERFILE: file.setResources(parseDockerfile(content, path.toString())); break; default: // For other types, parse as generic structured file file.setResources(parseStructuredFile(content, path.toString(), iacType)); } } catch (Exception e) { log.warn("Failed to parse file content: {}", path, e); // Continue with empty resources file.setResources(new ArrayList<>()); } return file; } private List<IaCResource> parseTerraformFile(String content, String filePath) { List<IaCResource> resources = new ArrayList<>(); // Simplified Terraform parsing - in production, use HCL parser String[] lines = content.split("\n"); for (int i = 0; i < lines.length; i++) { String line = lines[i].trim(); if (line.startsWith("resource") || line.startsWith("data") || line.startsWith("module") || line.startsWith("provider")) { IaCResource resource = new IaCResource(); resource.setType(extractResourceType(line)); resource.setName(extractResourceName(line)); resource.setFile(filePath); resource.setLine(i + 1); resource.setProperties(extractResourceProperties(content, i)); resources.add(resource); } } return resources; } private List<IaCResource> parseKubernetesFile(String content, String filePath) { List<IaCResource> resources = new ArrayList<>(); try { if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) { // Parse YAML Object yamlContent = yamlMapper.readValue(content, Object.class); if (yamlContent instanceof Map) { Map<String, Object> manifest = (Map<String, Object>) yamlContent; resources.add(createKubernetesResource(manifest, filePath, 1)); } else if (yamlContent instanceof List) { // Multiple documents in one file List<Object> manifests = (List<Object>) yamlContent; for (int i = 0; i < manifests.size(); i++) { if (manifests.get(i) instanceof Map) { Map<String, Object> manifest = (Map<String, Object>) manifests.get(i); resources.add(createKubernetesResource(manifest, filePath, i + 1)); } } } } else if (filePath.endsWith(".json")) { // Parse JSON Map<String, Object> manifest = jsonMapper.readValue(content, Map.class); resources.add(createKubernetesResource(manifest, filePath, 1)); } } catch (Exception e) { log.warn("Failed to parse Kubernetes file: {}", filePath, e); } return resources; } private IaCResource createKubernetesResource(Map<String, Object> manifest, String filePath, int line) { IaCResource resource = new IaCResource(); resource.setType(getKubernetesResourceType(manifest)); resource.setName(getKubernetesResourceName(manifest)); resource.setFile(filePath); resource.setLine(line); resource.setProperties(manifest); return resource; } private String getKubernetesResourceType(Map<String, Object> manifest) { String kind = (String) manifest.get("kind"); String apiVersion = (String) manifest.get("apiVersion"); return apiVersion + "/" + kind; } private String getKubernetesResourceName(Map<String, Object> manifest) { Map<String, Object> metadata = (Map<String, Object>) manifest.get("metadata"); if (metadata != null) { return (String) metadata.get("name"); } return "unknown"; } private List<IaCResource> parseCloudFormationFile(String content, String filePath) { List<IaCResource> resources = new ArrayList<>(); try { Map<String, Object> template = jsonMapper.readValue(content, Map.class); Map<String, Object> resourcesSection = (Map<String, Object>) template.get("Resources"); if (resourcesSection != null) { for (Map.Entry<String, Object> entry : resourcesSection.entrySet()) { IaCResource resource = new IaCResource(); resource.setName(entry.getKey()); Map<String, Object> resourceProps = (Map<String, Object>) entry.getValue(); resource.setType((String) resourceProps.get("Type")); resource.setFile(filePath); resource.setLine(1); // CloudFormation doesn't have line numbers resource.setProperties(resourceProps); resources.add(resource); } } } catch (Exception e) { log.warn("Failed to parse CloudFormation file: {}", filePath, e); } return resources; } private List<IaCResource> parseDockerfile(String content, String filePath) { List<IaCResource> resources = new ArrayList<>(); String[] lines = content.split("\n"); IaCResource dockerfile = new IaCResource(); dockerfile.setType("Dockerfile"); dockerfile.setName("Dockerfile"); dockerfile.setFile(filePath); dockerfile.setLine(1); Map<String, Object> properties = new HashMap<>(); properties.put("instructions", Arrays.asList(lines)); dockerfile.setProperties(properties); resources.add(dockerfile); return resources; } private List<IaCResource> parseStructuredFile(String content, String filePath, IaCType iacType) { List<IaCResource> resources = new ArrayList<>(); try { Map<String, Object> data; if (filePath.endsWith(".json")) { data = jsonMapper.readValue(content, Map.class); } else { data = yamlMapper.readValue(content, Map.class); } IaCResource resource = new IaCResource(); resource.setType(iacType.getValue()); resource.setName(filePath); resource.setFile(filePath); resource.setLine(1); resource.setProperties(data); resources.add(resource); } catch (Exception e) { log.warn("Failed to parse structured file: {}", filePath, e); } return resources; } // Helper methods for Terraform parsing private String extractResourceType(String line) { // Simplified extraction - in production, use proper HCL parsing String[] parts = line.split("\""); return parts.length > 1 ? parts[1] : "unknown"; } private String extractResourceName(String line) { // Simplified extraction - in production, use proper HCL parsing String[] parts = line.split("\""); return parts.length > 3 ? parts[3] : "unknown"; } private Map<String, Object> extractResourceProperties(String content, int startLine) { // Simplified property extraction Map<String, Object> properties = new HashMap<>(); // In production, implement proper HCL block parsing return properties; } } class IaCFile { private String path; private String fileName; private IaCType iacType; private String content; private List<IaCResource> resources; // Getters and Setters public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public IaCType getIacType() { return iacType; } public void setIacType(IaCType iacType) { this.iacType = iacType; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public List<IaCResource> getResources() { return resources; } public void setResources(List<IaCResource> resources) { this.resources = resources; } } class IaCResource { private String type; private String name; private String file; private int line; private Map<String, Object> properties; // Getters and Setters public String getType() { return type; } public void setType(String type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getFile() { return file; } public void setFile(String file) { this.file = file; } public int getLine() { return line; } public void setLine(int line) { this.line = line; } public Map<String, Object> getProperties() { return properties; } public void setProperties(Map<String, Object> properties) { this.properties = properties; } } 4. Policy Engine
package com.example.terrascan.engine; import com.example.terrascan.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; public class PolicyEngine { private static final Logger log = LoggerFactory.getLogger(PolicyEngine.class); private final PolicyBundle policyBundle; public PolicyEngine(PolicyBundle policyBundle) { this.policyBundle = policyBundle; } public List<Violation> evaluateResource(IaCResource resource) { List<Violation> violations = new ArrayList<>(); for (PolicyRule rule : policyBundle.getRules()) { if (!rule.isEnabled()) { continue; } if (appliesToResource(rule, resource)) { if (evaluateRule(rule, resource)) { Violation violation = createViolation(rule, resource); violations.add(violation); log.debug("Rule violation detected: {} for resource {}", rule.getId(), resource.getName()); } } } return violations; } private boolean appliesToResource(PolicyRule rule, IaCResource resource) { // Check resource type match if (rule.getResourceType() != null && !rule.getResourceType().equals(resource.getType())) { return false; } // Check file pattern match if (rule.getFilePatterns() != null && !rule.getFilePatterns().isEmpty()) { boolean fileMatch = rule.getFilePatterns().stream() .anyMatch(pattern -> resource.getFile().matches(pattern)); if (!fileMatch) { return false; } } return true; } private boolean evaluateRule(PolicyRule rule, IaCResource resource) { RuleCondition condition = rule.getCondition(); if (condition == null) { return false; } return evaluateCondition(condition, resource); } private boolean evaluateCondition(RuleCondition condition, IaCResource resource) { if (condition == null) { return false; } switch (condition.getType()) { case "attribute": return evaluateAttributeCondition(condition, resource); case "resource": return evaluateResourceCondition(condition, resource); case "file": return evaluateFileCondition(condition, resource); default: log.warn("Unknown condition type: {}", condition.getType()); return false; } } private boolean evaluateAttributeCondition(RuleCondition condition, IaCResource resource) { String attribute = condition.getAttribute(); Object actualValue = getAttributeValue(resource, attribute); Object expectedValue = condition.getValue(); Operator operator = condition.getOperator(); return compareValues(actualValue, expectedValue, operator); } private boolean evaluateResourceCondition(RuleCondition condition, IaCResource resource) { // Evaluate resource-level conditions String attribute = condition.getAttribute(); Object actualValue = getResourceAttribute(resource, attribute); Object expectedValue = condition.getValue(); Operator operator = condition.getOperator(); return compareValues(actualValue, expectedValue, operator); } private boolean evaluateFileCondition(RuleCondition condition, IaCResource resource) { // Evaluate file-level conditions String attribute = condition.getAttribute(); Object actualValue = getFileAttribute(resource, attribute); Object expectedValue = condition.getValue(); Operator operator = condition.getOperator(); return compareValues(actualValue, expectedValue, operator); } private Object getAttributeValue(IaCResource resource, String attributePath) { Map<String, Object> properties = resource.getProperties(); if (properties == null) { return null; } String[] path = attributePath.split("\\."); Object current = properties; for (String key : path) { if (current instanceof Map) { current = ((Map<String, Object>) current).get(key); } else { return null; } } return current; } private Object getResourceAttribute(IaCResource resource, String attribute) { switch (attribute) { case "type": return resource.getType(); case "name": return resource.getName(); case "file": return resource.getFile(); case "line": return resource.getLine(); default: return null; } } private Object getFileAttribute(IaCResource resource, String attribute) { // File-level attributes would be extracted from the file path or metadata return null; } @SuppressWarnings("unchecked") private boolean compareValues(Object actual, Object expected, Operator operator) { if (operator == Operator.EXISTS) { return actual != null; } if (operator == Operator.NOT_EXISTS) { return actual == null; } if (actual == null) { return false; } switch (operator) { case EQUALS: return actual.equals(expected); case NOT_EQUALS: return !actual.equals(expected); case CONTAINS: if (actual instanceof String && expected instanceof String) { return ((String) actual).contains((String) expected); } return false; case NOT_CONTAINS: if (actual instanceof String && expected instanceof String) { return !((String) actual).contains((String) expected); } return false; case GREATER_THAN: if (actual instanceof Number && expected instanceof Number) { return ((Number) actual).doubleValue() > ((Number) expected).doubleValue(); } return false; case LESS_THAN: if (actual instanceof Number && expected instanceof Number) { return ((Number) actual).doubleValue() < ((Number) expected).doubleValue(); } return false; case MATCHES_PATTERN: if (actual instanceof String && expected instanceof String) { return ((String) actual).matches((String) expected); } return false; case IN_LIST: if (expected instanceof List) { return ((List<Object>) expected).contains(actual); } return false; default: return false; } } private Violation createViolation(PolicyRule rule, IaCResource resource) { Violation violation = new Violation(); violation.setRuleId(rule.getId()); violation.setRuleName(rule.getName()); violation.setDescription(rule.getDescription()); violation.setSeverity(rule.getSeverity()); violation.setCategory(rule.getCategory()); violation.setResourceType(resource.getType()); violation.setResourceName(resource.getName()); violation.setFile(resource.getFile()); violation.setLine(resource.getLine()); violation.setRemediation(rule.getRemediation()); return violation; } } 5. Terrascan Scanner Service
package com.example.terrascan.service; import com.example.terrascan.engine.PolicyEngine; import com.example.terrascan.model.*; import com.example.terrascan.scanner.FileScanner; import com.example.terrascan.scanner.IaCFile; import com.example.terrascan.scanner.IaCResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; public class TerrascanService { private static final Logger log = LoggerFactory.getLogger(TerrascanService.class); private final FileScanner fileScanner; private final PolicyEngine policyEngine; public TerrascanService(FileScanner fileScanner, PolicyEngine policyEngine) { this.fileScanner = fileScanner; this.policyEngine = policyEngine; } public ScanResult scanDirectory(String directoryPath, IaCType iacType) { return scanDirectory(directoryPath, iacType, new ScanConfig()); } public ScanResult scanDirectory(String directoryPath, IaCType iacType, ScanConfig config) { ScanResult result = new ScanResult(); result.setScanId(UUID.randomUUID().toString()); result.setDirectory(directoryPath); result.setIacType(iacType); try { log.info("Starting scan for directory: {} (IaC type: {})", directoryPath, iacType); // Scan files List<IaCFile> files = fileScanner.scanDirectory(directoryPath, iacType); result.getSummary().setFilesScanned(files.size()); // Evaluate each resource against policies List<Violation> allViolations = new ArrayList<>(); int resourcesScanned = 0; for (IaCFile file : files) { for (IaCResource resource : file.getResources()) { List<Violation> violations = policyEngine.evaluateResource(resource); allViolations.addAll(violations); resourcesScanned++; } } result.getSummary().setResourcesScanned(resourcesScanned); result.setViolations(allViolations); // Calculate summary result.getSummary().calculateFromViolations(allViolations); // Determine if scan passed based on config boolean passed = determineScanPassed(result, config); result.setPassed(passed); log.info("Scan completed: {} violations found ({} resources scanned)", allViolations.size(), resourcesScanned); } catch (IOException e) { log.error("Failed to scan directory: {}", directoryPath, e); result.setPassed(false); } return result; } private boolean determineScanPassed(ScanResult result, ScanConfig config) { if (config.isFailOnCritical() && result.getSummary().getCriticalViolations() > 0) { return false; } if (config.isFailOnHigh() && result.getSummary().getHighViolations() > 0) { return false; } if (config.getMaxCritical() >= 0 && result.getSummary().getCriticalViolations() > config.getMaxCritical()) { return false; } if (config.getMaxHigh() >= 0 && result.getSummary().getHighViolations() > config.getMaxHigh()) { return false; } if (config.getMaxMedium() >= 0 && result.getSummary().getMediumViolations() > config.getMaxMedium()) { return false; } return true; } } class ScanConfig { private boolean failOnCritical = true; private boolean failOnHigh = false; private int maxCritical = 0; private int maxHigh = -1; private int maxMedium = -1; private int maxLow = -1; // Getters and Setters public boolean isFailOnCritical() { return failOnCritical; } public void setFailOnCritical(boolean failOnCritical) { this.failOnCritical = failOnCritical; } public boolean isFailOnHigh() { return failOnHigh; } public void setFailOnHigh(boolean failOnHigh) { this.failOnHigh = failOnHigh; } public int getMaxCritical() { return maxCritical; } public void setMaxCritical(int maxCritical) { this.maxCritical = maxCritical; } public int getMaxHigh() { return maxHigh; } public void setMaxHigh(int maxHigh) { this.maxHigh = maxHigh; } public int getMaxMedium() { return maxMedium; } public void setMaxMedium(int maxMedium) { this.maxMedium = maxMedium; } public int getMaxLow() { return maxLow; } public void setMaxLow(int maxLow) { this.maxLow = maxLow; } } 6. Built-in Policy Rules
package com.example.terrascan.policies; import com.example.terrascan.model.*; import java.util.Arrays; import java.util.List; public class BuiltInPolicies { public static PolicyBundle getTerraformPolicies() { PolicyBundle bundle = new PolicyBundle(); bundle.setName("terraform-security"); bundle.setVersion("1.0.0"); bundle.setDescription("Security policies for Terraform configurations"); List<PolicyRule> rules = Arrays.asList( createRule("TERRAFORM_001", "S3 Bucket should not be public", "S3 buckets should not have public read or write access", Severity.HIGH, "AWS", "aws_s3_bucket", "acl", Operator.IN_LIST, Arrays.asList("public-read", "public-read-write", "website")), createRule("TERRAFORM_002", "Security groups should not allow all traffic", "Security groups should restrict inbound and outbound traffic", Severity.HIGH, "AWS", "aws_security_group", "ingress.*.cidr_blocks", Operator.CONTAINS, "0.0.0.0/0"), createRule("TERRAFORM_003", "EC2 instances should not have public IPs", "EC2 instances should not be assigned public IP addresses", Severity.MEDIUM, "AWS", "aws_instance", "associate_public_ip_address", Operator.EQUALS, true), createRule("TERRAFORM_004", "Database should not be publicly accessible", "Databases should not be exposed to the public internet", Severity.CRITICAL, "AWS", "aws_db_instance", "publicly_accessible", Operator.EQUALS, true), createRule("TERRAFORM_005", "IAM policies should not use wildcards", "IAM policies should avoid using wildcards for sensitive permissions", Severity.HIGH, "AWS", "aws_iam_policy", "policy", Operator.MATCHES_PATTERN, ".*\\\\*.*") ); bundle.setRules(rules); return bundle; } public static PolicyBundle getKubernetesPolicies() { PolicyBundle bundle = new PolicyBundle(); bundle.setName("kubernetes-security"); bundle.setVersion("1.0.0"); bundle.setDescription("Security policies for Kubernetes manifests"); List<PolicyRule> rules = Arrays.asList( createRule("K8S_001", "Containers should not run as root", "Containers should run as non-root users", Severity.MEDIUM, "Security", "apps/v1/Deployment", "spec.template.spec.containers[*].securityContext.runAsNonRoot", Operator.NOT_EQUALS, true), createRule("K8S_002", "Containers should not allow privilege escalation", "Containers should not allow privilege escalation", Severity.HIGH, "Security", "apps/v1/Deployment", "spec.template.spec.containers[*].securityContext.allowPrivilegeEscalation", Operator.EQUALS, true), createRule("K8S_003", "Secrets should not be in environment variables", "Sensitive data should not be passed via environment variables", Severity.MEDIUM, "Security", "apps/v1/Deployment", "spec.template.spec.containers[*].env[*].valueFrom.secretKeyRef", Operator.EXISTS, null), createRule("K8S_004", "Resources should have limits defined", "Containers should have resource limits defined", Severity.LOW, "Resources", "apps/v1/Deployment", "spec.template.spec.containers[*].resources.limits", Operator.NOT_EXISTS, null), createRule("K8S_005", "Host networking should not be used", "Pods should not use host networking", Severity.HIGH, "Security", "apps/v1/Deployment", "spec.template.spec.hostNetwork", Operator.EQUALS, true) ); bundle.setRules(rules); return bundle; } public static PolicyBundle getDockerfilePolicies() { PolicyBundle bundle = new PolicyBundle(); bundle.setName("dockerfile-security"); bundle.setVersion("1.0.0"); bundle.setDescription("Security policies for Dockerfiles"); List<PolicyRule> rules = Arrays.asList( createRule("DOCKER_001", "Base image should use specific version", "Base images should use specific versions instead of latest", Severity.MEDIUM, "Security", "Dockerfile", "instructions", Operator.MATCHES_PATTERN, "FROM.*latest"), createRule("DOCKER_002", "Should not run as root", "Containers should not run as root user", Severity.HIGH, "Security", "Dockerfile", "instructions", Operator.NOT_CONTAINS, "USER"), createRule("DOCKER_003", "Should use .dockerignore", "Docker builds should use .dockerignore file", Severity.LOW, "Best Practices", "Dockerfile", "file", Operator.NOT_CONTAINS, ".dockerignore") ); bundle.setRules(rules); return bundle; } private static PolicyRule createRule(String id, String name, String description, Severity severity, String category, String resourceType, String attribute, Operator operator, Object value) { PolicyRule rule = new PolicyRule(); rule.setId(id); rule.setName(name); rule.setDescription(description); rule.setSeverity(severity); rule.setCategory(category); rule.setResourceType(resourceType); rule.setEnabled(true); RuleCondition condition = new RuleCondition(); condition.setType("attribute"); condition.setAttribute(attribute); condition.setOperator(operator); condition.setValue(value); rule.setCondition(condition); return rule; } } 7. Report Generators
package com.example.terrascan.report; import com.example.terrascan.model.ScanResult; import com.example.terrascan.model.Violation; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.format.DateTimeFormatter; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; public class ReportGenerator { private static final Logger log = LoggerFactory.getLogger(ReportGenerator.class); private final ObjectMapper jsonMapper; private final ObjectMapper yamlMapper; public ReportGenerator() { this.jsonMapper = new ObjectMapper(); this.yamlMapper = new ObjectMapper(new YAMLFactory()); } public String generateConsoleReport(ScanResult result) { StringBuilder report = new StringBuilder(); report.append("=== Terrascan Scan Report ===\n"); report.append("Scan ID: ").append(result.getScanId()).append("\n"); report.append("Directory: ").append(result.getDirectory()).append("\n"); report.append("IaC Type: ").append(result.getIacType()).append("\n"); report.append("Scan Time: ").append(result.getScanTimestamp().format( DateTimeFormatter.ISO_LOCAL_DATE_TIME)).append("\n"); report.append("Status: ").append(result.isPassed() ? "PASSED" : "FAILED").append("\n\n"); // Summary report.append("=== Summary ===\n"); report.append(String.format("Files Scanned: %d\n", result.getSummary().getFilesScanned())); report.append(String.format("Resources Scanned: %d\n", result.getSummary().getResourcesScanned())); report.append(String.format("Total Violations: %d\n", result.getSummary().getTotalViolations())); report.append(String.format("Critical: %d\n", result.getSummary().getCriticalViolations())); report.append(String.format("High: %d\n", result.getSummary().getHighViolations())); report.append(String.format("Medium: %d\n", result.getSummary().getMediumViolations())); report.append(String.format("Low: %d\n", result.getSummary().getLowViolations())); report.append("\n"); // Violations if (!result.getViolations().isEmpty()) { report.append("=== Violations ===\n"); List<Violation> sortedViolations = result.getViolations().stream() .sorted(Comparator.comparing(v -> v.getSeverity().ordinal()).reversed() .thenComparing(Violation::getFile) .thenComparing(Violation::getLine)) .collect(Collectors.toList()); for (Violation violation : sortedViolations) { report.append(String.format("[%s] %s\n", violation.getSeverity(), violation.getRuleName())); report.append(String.format(" Rule: %s\n", violation.getRuleId())); report.append(String.format(" File: %s:%d\n", violation.getFile(), violation.getLine())); report.append(String.format(" Resource: %s/%s\n", violation.getResourceType(), violation.getResourceName())); report.append(String.format(" Description: %s\n", violation.getDescription())); if (violation.getRemediation() != null) { report.append(String.format(" Remediation: %s\n", violation.getRemediation())); } report.append("\n"); } } else { report.append("No violations found!\n"); } return report.toString(); } public String generateJsonReport(ScanResult result) { try { return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(result); } catch (Exception e) { log.error("Failed to generate JSON report", e); return "{}"; } } public String generateYamlReport(ScanResult result) { try { return yamlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(result); } catch (Exception e) { log.error("Failed to generate YAML report", e); return ""; } } public String generateJUnitReport(ScanResult result) { // Generate JUnit XML format for CI/CD integration StringBuilder xml = new StringBuilder(); xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); xml.append("<testsuite name=\"Terrascan\" tests=\"") .append(result.getSummary().getResourcesScanned()) .append("\" failures=\"") .append(result.getSummary().getTotalViolations()) .append("\">\n"); for (Violation violation : result.getViolations()) { xml.append(" <testcase name=\"") .append(escapeXml(violation.getRuleId())) .append("\" classname=\"") .append(escapeXml(violation.getResourceType())) .append("\">\n"); xml.append(" <failure message=\"") .append(escapeXml(violation.getDescription())) .append("\" type=\"") .append(violation.getSeverity()) .append("\">") .append(escapeXml(violation.getRemediation())) .append("</failure>\n"); xml.append(" </testcase>\n"); } xml.append("</testsuite>"); return xml.toString(); } private String escapeXml(String text) { if (text == null) return ""; return text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } } 8. Example Usage
package com.example.terrascan.demo; import com.example.terrascan.engine.PolicyEngine; import com.example.terrascan.model.IaCType; import com.example.terrascan.model.PolicyBundle; import com.example.terrascan.model.ScanResult; import com.example.terrascan.policies.BuiltInPolicies; import com.example.terrascan.report.ReportGenerator; import com.example.terrascan.scanner.FileScanner; import com.example.terrascan.service.ScanConfig; import com.example.terrascan.service.TerrascanService; public class TerrascanDemo { public static void main(String[] args) { // Initialize components FileScanner fileScanner = new FileScanner(); PolicyBundle policyBundle = BuiltInPolicies.getTerraformPolicies(); PolicyEngine policyEngine = new PolicyEngine(policyBundle); TerrascanService terrascan = new TerrascanService(fileScanner, policyEngine); ReportGenerator reportGenerator = new ReportGenerator(); // Configure scan ScanConfig config = new ScanConfig(); config.setFailOnCritical(true); config.setFailOnHigh(false); config.setMaxCritical(0); config.setMaxHigh(5); try { // Scan directory String directoryToScan = "./terraform-examples"; ScanResult result = terrascan.scanDirectory(directoryToScan, IaCType.TERRAFORM, config); // Generate reports System.out.println("=== Console Report ==="); System.out.println(reportGenerator.generateConsoleReport(result)); System.out.println("=== JSON Report ==="); System.out.println(reportGenerator.generateJsonReport(result)); System.out.println("=== JUnit Report ==="); System.out.println(reportGenerator.generateJUnitReport(result)); // Exit with appropriate code for CI/CD System.exit(result.isPassed() ? 0 : 1); } catch (Exception e) { System.err.println("Scan failed: " + e.getMessage()); e.printStackTrace(); System.exit(1); } } } Key Features
- Multi-format Support: Terraform, Kubernetes, CloudFormation, Dockerfile, etc.
- Extensible Policy Engine: Custom rules with complex conditions
- Comprehensive Scanning: File and resource-level analysis
- Multiple Report Formats: Console, JSON, YAML, JUnit XML
- CI/CD Integration: Proper exit codes and machine-readable outputs
- Built-in Security Policies: Pre-configured security rules for common IaC
- Configurable Thresholds: Flexible pass/fail criteria
This implementation provides a complete Terrascan-like tool for Infrastructure as Code security scanning in Java, suitable for integration into CI/CD pipelines and security automation workflows.