Terrascan for Infrastructure as Code (IaC) in Java

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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
}

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

  1. Multi-format Support: Terraform, Kubernetes, CloudFormation, Dockerfile, etc.
  2. Extensible Policy Engine: Custom rules with complex conditions
  3. Comprehensive Scanning: File and resource-level analysis
  4. Multiple Report Formats: Console, JSON, YAML, JUnit XML
  5. CI/CD Integration: Proper exit codes and machine-readable outputs
  6. Built-in Security Policies: Pre-configured security rules for common IaC
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper