CFN-Nag is a security and compliance tool that scans AWS CloudFormation templates for potential security issues and compliance violations. This guide provides a complete Java implementation for analyzing CloudFormation templates.
CFN-Nag Overview
What is CFN-Nag?
- Static analysis tool for AWS CloudFormation templates
- Checks for security best practices and compliance rules
- Identifies potential IAM policy violations, security group misconfigurations, etc.
- Supports JSON and YAML CloudFormation templates
Key Security Checks:
- IAM policies with wildcard permissions
- Security groups with overly permissive rules
- Unencrypted resources (S3, EBS, RDS)
- Publicly accessible resources
- Missing logging configurations
Dependencies and Setup
Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<snakeyaml.version>2.0</snakeyaml.version>
<jackson.version>2.15.2</jackson.version>
<aws-java-sdk.version>2.20.0</aws-java-sdk.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- YAML/JSON Processing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- AWS SDK -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-sdk-java</artifactId>
<version>${aws-java-sdk.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Application Configuration
# application.yml app: cfn-nag: # Rule configuration rules: # IAM Rules iam-wildcard-actions: true iam-wildcard-resources: true iam-passrole-wildcard: true iam-policy-too-permissive: true # Security Group Rules security-group-world-open: true security-group-missing-description: true # Encryption Rules s3-bucket-public-read: true s3-bucket-public-write: true s3-bucket-unencrypted: true ebs-volume-unencrypted: true rds-unencrypted: true # Logging Rules cloudtrail-logging-disabled: true cloudwatch-log-group-unencrypted: true # Network Rules elb-internet-facing: true nacl-world-open: true # Severity levels: FAIL, WARN, INFO min-severity: WARN # Custom rule files custom-rules-path: "/rules/custom" # Output formats output-formats: ["json", "yaml", "text", "html"] server: port: 8080 logging: level: com.example.cfnnag: DEBUG
Core Models
1. Analysis Models
// CfnNagRequest.java
package com.example.cfnnag.model;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.NotNull;
@Data
public class CfnNagRequest {
@NotNull(message = "CloudFormation template is required")
private MultipartFile template;
private AnalysisOptions options = new AnalysisOptions();
@Data
public static class AnalysisOptions {
private boolean failOnWarnings = true;
private Severity minSeverity = Severity.WARN;
private boolean printSuppression = false;
private String outputFormat = "json";
private String ruleDirectory;
private String inputPath;
private boolean debug = false;
}
}
// CfnNagResponse.java
package com.example.cfnnag.model;
import lombok.Data;
import lombok.Builder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
public class CfnNagResponse {
private boolean success;
private String message;
private LocalDateTime timestamp;
private AnalysisSummary summary;
private List<Violation> violations;
private String rawOutput;
@Data
@Builder
public static class AnalysisSummary {
private int totalResources;
private int failViolations;
private int warnViolations;
private int infoViolations;
private int totalViolations;
private String status;
private double complianceScore;
}
}
// Violation.java
package com.example.cfnnag.model;
import lombok.Data;
import lombok.Builder;
import java.util.List;
@Data
@Builder
public class Violation {
private String ruleId;
private RuleType type;
private Severity severity;
private String message;
private String logicalResourceId;
private String resourceType;
private List<String> violatingResources;
private String ruleDescription;
private List<String> recommendations;
private String ruleDocumentation;
public boolean isFail() {
return severity == Severity.FAIL;
}
public boolean isWarn() {
return severity == Severity.WARN;
}
public boolean isInfo() {
return severity == Severity.INFO;
}
}
// Severity.java
package com.example.cfnnag.model;
public enum Severity {
FAIL,
WARN,
INFO
}
// RuleType.java
package com.example.cfnnag.model;
public enum RuleType {
IAM,
SECURITY_GROUP,
ENCRYPTION,
LOGGING,
NETWORK,
COMPLIANCE,
CUSTOM
}
2. CloudFormation Template Models
// CloudFormationTemplate.java
package com.example.cfnnag.model;
import lombok.Data;
import java.util.Map;
@Data
public class CloudFormationTemplate {
private String awsTemplateFormatVersion;
private String description;
private Map<String, Parameter> parameters;
private Map<String, Resource> resources;
private Map<String, Output> outputs;
private Map<String, Object> metadata;
private Map<String, Object> mappings;
private Map<String, Object> conditions;
private Map<String, Object> transforms;
@Data
public static class Parameter {
private String type;
private String description;
private String defaultValue;
private List<String> allowedValues;
private Map<String, Object> constraintDescription;
private String minValue;
private String maxValue;
private String minLength;
private String maxLength;
private String noEcho;
}
@Data
public static class Resource {
private String type;
private Map<String, Object> properties;
private Map<String, Object> metadata;
private String deletionPolicy;
private String updateReplacePolicy;
private Map<String, List<String>> dependsOn;
private Map<String, Object> condition;
private Map<String, Object> creationPolicy;
private Map<String, Object> updatePolicy;
}
@Data
public static class Output {
private String description;
private Object value;
private Map<String, Object> export;
private Map<String, Object> condition;
}
}
// IamPolicyDocument.java
package com.example.cfnnag.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class IamPolicyDocument {
private String version;
private String id;
private List<Statement> statements;
@Data
public static class Statement {
private String sid;
private String effect;
private Object principal;
private Object notPrincipal;
private Object action;
private Object notAction;
private Object resource;
private Object notResource;
private Map<String, Map<String, List<String>>> condition;
public boolean hasWildcardAction() {
return hasWildcard(action);
}
public boolean hasWildcardResource() {
return hasWildcard(resource);
}
private boolean hasWildcard(Object field) {
if (field == null) return false;
if (field instanceof String) {
return "*".equals(field) || "arn:aws:iam::*".equals(field);
}
if (field instanceof List) {
@SuppressWarnings("unchecked")
List<String> list = (List<String>) field;
return list.contains("*") || list.contains("arn:aws:iam::*");
}
return false;
}
public boolean isAllowEffect() {
return "Allow".equalsIgnoreCase(effect);
}
public boolean isDenyEffect() {
return "Deny".equalsIgnoreCase(effect);
}
}
}
// SecurityGroup.java
package com.example.cfnnag.model;
import lombok.Data;
import java.util.List;
@Data
public class SecurityGroup {
private String groupDescription;
private String vpcId;
private List<SecurityGroupRule> securityGroupIngress;
private List<SecurityGroupRule> securityGroupEgress;
private Map<String, Object> tags;
@Data
public static class SecurityGroupRule {
private String cidrIp;
private String cidrIpv6;
private String description;
private String fromPort;
private String toPort;
private String ipProtocol;
private String sourceSecurityGroupId;
private String sourceSecurityGroupName;
private String sourceSecurityGroupOwnerId;
private String destinationSecurityGroupId;
private String destinationPrefixListId;
public boolean isWorldOpen() {
return "0.0.0.0/0".equals(cidrIp) || "::/0".equals(cidrIpv6);
}
public boolean isMissingDescription() {
return description == null || description.trim().isEmpty();
}
}
}
Rule Engine and Checks
1. Base Rule Interface
// CfnNagRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.CloudFormationTemplate;
import com.example.cfnnag.model.Violation;
import java.util.List;
public interface CfnNagRule {
String getRuleId();
String getRuleDescription();
RuleType getRuleType();
Severity getSeverity();
List<String> getSupportedResourceTypes();
List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource);
boolean isEnabled();
}
2. IAM Rules
// IamWildcardActionRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class IamWildcardActionRule implements CfnNagRule {
@Override
public String getRuleId() {
return "F1";
}
@Override
public String getRuleDescription() {
return "IAM policy should not allow * action";
}
@Override
public RuleType getRuleType() {
return RuleType.IAM;
}
@Override
public Severity getSeverity() {
return Severity.FAIL;
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of(
"AWS::IAM::Policy",
"AWS::IAM::ManagedPolicy",
"AWS::IAM::Role",
"AWS::IAM::User",
"AWS::IAM::Group"
);
}
@Override
@SuppressWarnings("unchecked")
public List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource) {
List<Violation> violations = new ArrayList<>();
try {
Map<String, Object> properties = resource.getProperties();
if (properties == null) return violations;
// Extract policy documents from different resource types
List<IamPolicyDocument> policyDocuments = extractPolicyDocuments(resource);
for (IamPolicyDocument policyDoc : policyDocuments) {
if (policyDoc.getStatements() != null) {
for (IamPolicyDocument.Statement statement : policyDoc.getStatements()) {
if (statement.isAllowEffect() && statement.hasWildcardAction()) {
violations.add(createViolation(resourceId, resource.getType()));
}
}
}
}
} catch (Exception e) {
log.error("Error evaluating IAM wildcard action rule for resource: {}", resourceId, e);
}
return violations;
}
@SuppressWarnings("unchecked")
private List<IamPolicyDocument> extractPolicyDocuments(CloudFormationTemplate.Resource resource) {
List<IamPolicyDocument> policyDocuments = new ArrayList<>();
Map<String, Object> properties = resource.getProperties();
if (properties == null) return policyDocuments;
try {
// Handle different IAM resource types
switch (resource.getType()) {
case "AWS::IAM::Policy":
Object policyDocument = properties.get("PolicyDocument");
if (policyDocument instanceof Map) {
policyDocuments.add(convertToPolicyDocument((Map<String, Object>) policyDocument));
}
break;
case "AWS::IAM::ManagedPolicy":
Object managedPolicyDocument = properties.get("PolicyDocument");
if (managedPolicyDocument instanceof Map) {
policyDocuments.add(convertToPolicyDocument((Map<String, Object>) managedPolicyDocument));
}
break;
case "AWS::IAM::Role":
Object assumeRolePolicyDocument = properties.get("AssumeRolePolicyDocument");
if (assumeRolePolicyDocument instanceof Map) {
policyDocuments.add(convertToPolicyDocument((Map<String, Object>) assumeRolePolicyDocument));
}
// Check for inline policies
Object policies = properties.get("Policies");
if (policies instanceof List) {
for (Object policy : (List<?>) policies) {
if (policy instanceof Map) {
Map<String, Object> policyMap = (Map<String, Object>) policy;
Object policyDoc = policyMap.get("PolicyDocument");
if (policyDoc instanceof Map) {
policyDocuments.add(convertToPolicyDocument((Map<String, Object>) policyDoc));
}
}
}
}
break;
case "AWS::IAM::User":
case "AWS::IAM::Group":
// Check for inline policies
Object userPolicies = properties.get("Policies");
if (userPolicies instanceof List) {
for (Object policy : (List<?>) userPolicies) {
if (policy instanceof Map) {
Map<String, Object> policyMap = (Map<String, Object>) policy;
Object policyDoc = policyMap.get("PolicyDocument");
if (policyDoc instanceof Map) {
policyDocuments.add(convertToPolicyDocument((Map<String, Object>) policyDoc));
}
}
}
}
break;
}
} catch (Exception e) {
log.error("Error extracting policy documents from resource: {}", resource.getType(), e);
}
return policyDocuments;
}
@SuppressWarnings("unchecked")
private IamPolicyDocument convertToPolicyDocument(Map<String, Object> policyDocMap) {
IamPolicyDocument policyDocument = new IamPolicyDocument();
policyDocument.setVersion((String) policyDocMap.get("Version"));
policyDocument.setId((String) policyDocMap.get("Id"));
Object statementsObj = policyDocMap.get("Statement");
if (statementsObj instanceof List) {
List<IamPolicyDocument.Statement> statements = new ArrayList<>();
for (Object stmtObj : (List<?>) statementsObj) {
if (stmtObj instanceof Map) {
statements.add(convertToStatement((Map<String, Object>) stmtObj));
}
}
policyDocument.setStatements(statements);
}
return policyDocument;
}
@SuppressWarnings("unchecked")
private IamPolicyDocument.Statement convertToStatement(Map<String, Object> stmtMap) {
IamPolicyDocument.Statement statement = new IamPolicyDocument.Statement();
statement.setSid((String) stmtMap.get("Sid"));
statement.setEffect((String) stmtMap.get("Effect"));
statement.setPrincipal(stmtMap.get("Principal"));
statement.setNotPrincipal(stmtMap.get("NotPrincipal"));
statement.setAction(stmtMap.get("Action"));
statement.setNotAction(stmtMap.get("NotAction"));
statement.setResource(stmtMap.get("Resource"));
statement.setNotResource(stmtMap.get("NotResource"));
statement.setCondition((Map<String, Map<String, List<String>>>) stmtMap.get("Condition"));
return statement;
}
private Violation createViolation(String resourceId, String resourceType) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("IAM policy contains wildcard (*) actions")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Replace wildcard actions with specific actions",
"Use least privilege principle",
"List specific actions required for the resource"
))
.ruleDocumentation("https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-groups-for-permissions")
.build();
}
@Override
public boolean isEnabled() {
return true;
}
}
// IamWildcardResourceRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class IamWildcardResourceRule implements CfnNagRule {
@Override
public String getRuleId() {
return "F2";
}
@Override
public String getRuleDescription() {
return "IAM policy should not allow * resource";
}
@Override
public RuleType getRuleType() {
return RuleType.IAM;
}
@Override
public Severity getSeverity() {
return Severity.FAIL;
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of(
"AWS::IAM::Policy",
"AWS::IAM::ManagedPolicy",
"AWS::IAM::Role",
"AWS::IAM::User",
"AWS::IAM::Group"
);
}
@Override
public List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource) {
List<Violation> violations = new ArrayList<>();
try {
List<IamPolicyDocument> policyDocuments = extractPolicyDocuments(resource);
for (IamPolicyDocument policyDoc : policyDocuments) {
if (policyDoc.getStatements() != null) {
for (IamPolicyDocument.Statement statement : policyDoc.getStatements()) {
if (statement.isAllowEffect() && statement.hasWildcardResource()) {
violations.add(createViolation(resourceId, resource.getType()));
}
}
}
}
} catch (Exception e) {
log.error("Error evaluating IAM wildcard resource rule for resource: {}", resourceId, e);
}
return violations;
}
// extractPolicyDocuments method similar to IamWildcardActionRule
@SuppressWarnings("unchecked")
private List<IamPolicyDocument> extractPolicyDocuments(CloudFormationTemplate.Resource resource) {
List<IamPolicyDocument> policyDocuments = new ArrayList<>();
Map<String, Object> properties = resource.getProperties();
if (properties == null) return policyDocuments;
try {
// Similar implementation to IamWildcardActionRule
// Extract policy documents from different IAM resource types
// ...
} catch (Exception e) {
log.error("Error extracting policy documents", e);
}
return policyDocuments;
}
private Violation createViolation(String resourceId, String resourceType) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("IAM policy contains wildcard (*) resources")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Replace wildcard resources with specific resource ARNs",
"Use resource constraints and conditions",
"Implement proper resource naming conventions"
))
.ruleDocumentation("https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege")
.build();
}
@Override
public boolean isEnabled() {
return true;
}
}
3. Security Group Rules
// SecurityGroupWorldOpenRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class SecurityGroupWorldOpenRule implements CfnNagRule {
@Override
public String getRuleId() {
return "F3";
}
@Override
public String getRuleDescription() {
return "Security Group should not be open to the world";
}
@Override
public RuleType getRuleType() {
return RuleType.SECURITY_GROUP;
}
@Override
public Severity getSeverity() {
return Severity.FAIL;
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of(
"AWS::EC2::SecurityGroup",
"AWS::EC2::SecurityGroupIngress",
"AWS::EC2::SecurityGroupEgress"
);
}
@Override
@SuppressWarnings("unchecked")
public List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource) {
List<Violation> violations = new ArrayList<>();
try {
Map<String, Object> properties = resource.getProperties();
if (properties == null) return violations;
switch (resource.getType()) {
case "AWS::EC2::SecurityGroup":
checkSecurityGroup(resourceId, resource, properties, violations);
break;
case "AWS::EC2::SecurityGroupIngress":
checkSecurityGroupIngress(resourceId, resource, properties, violations);
break;
case "AWS::EC2::SecurityGroupEgress":
checkSecurityGroupEgress(resourceId, resource, properties, violations);
break;
}
} catch (Exception e) {
log.error("Error evaluating security group world open rule for resource: {}", resourceId, e);
}
return violations;
}
@SuppressWarnings("unchecked")
private void checkSecurityGroup(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
// Check SecurityGroupIngress
Object ingressObj = properties.get("SecurityGroupIngress");
if (ingressObj instanceof List) {
for (Object ingressRuleObj : (List<?>) ingressObj) {
if (ingressRuleObj instanceof Map) {
Map<String, Object> ingressRule = (Map<String, Object>) ingressRuleObj;
if (isWorldOpenRule(ingressRule)) {
violations.add(createViolation(resourceId, resource.getType(), "ingress"));
}
}
}
}
// Check SecurityGroupEgress
Object egressObj = properties.get("SecurityGroupEgress");
if (egressObj instanceof List) {
for (Object egressRuleObj : (List<?>) egressObj) {
if (egressRuleObj instanceof Map) {
Map<String, Object> egressRule = (Map<String, Object>) egressRuleObj;
if (isWorldOpenRule(egressRule)) {
violations.add(createViolation(resourceId, resource.getType(), "egress"));
}
}
}
}
}
@SuppressWarnings("unchecked")
private void checkSecurityGroupIngress(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
if (isWorldOpenRule(properties)) {
violations.add(createViolation(resourceId, resource.getType(), "ingress"));
}
}
@SuppressWarnings("unchecked")
private void checkSecurityGroupEgress(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
if (isWorldOpenRule(properties)) {
violations.add(createViolation(resourceId, resource.getType(), "egress"));
}
}
private boolean isWorldOpenRule(Map<String, Object> rule) {
String cidrIp = (String) rule.get("CidrIp");
String cidrIpv6 = (String) rule.get("CidrIpv6");
return "0.0.0.0/0".equals(cidrIp) || "::/0".equals(cidrIpv6);
}
private Violation createViolation(String resourceId, String resourceType, String direction) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("Security Group " + direction + " rule is open to the world (0.0.0.0/0 or ::/0)")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Restrict security group rules to specific IP ranges",
"Use VPC security groups for internal traffic",
"Implement network ACLs for additional protection"
))
.ruleDocumentation("https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html")
.build();
}
@Override
public boolean isEnabled() {
return true;
}
}
// SecurityGroupMissingDescriptionRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class SecurityGroupMissingDescriptionRule implements CfnNagRule {
@Override
public String getRuleId() {
return "W1";
}
@Override
public String getRuleDescription() {
return "Security Group rules should have descriptions";
}
@Override
public RuleType getRuleType() {
return RuleType.SECURITY_GROUP;
}
@Override
public Severity getSeverity() {
return Severity.WARN;
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of(
"AWS::EC2::SecurityGroup",
"AWS::EC2::SecurityGroupIngress",
"AWS::EC2::SecurityGroupEgress"
);
}
@Override
@SuppressWarnings("unchecked")
public List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource) {
List<Violation> violations = new ArrayList<>();
try {
Map<String, Object> properties = resource.getProperties();
if (properties == null) return violations;
switch (resource.getType()) {
case "AWS::EC2::SecurityGroup":
checkSecurityGroupDescriptions(resourceId, resource, properties, violations);
break;
case "AWS::EC2::SecurityGroupIngress":
case "AWS::EC2::SecurityGroupEgress":
checkRuleDescription(resourceId, resource, properties, violations);
break;
}
} catch (Exception e) {
log.error("Error evaluating security group description rule for resource: {}", resourceId, e);
}
return violations;
}
@SuppressWarnings("unchecked")
private void checkSecurityGroupDescriptions(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
// Check group description
String groupDescription = (String) properties.get("GroupDescription");
if (groupDescription == null || groupDescription.trim().isEmpty()) {
violations.add(createGroupDescriptionViolation(resourceId, resource.getType()));
}
// Check rule descriptions
Object ingressObj = properties.get("SecurityGroupIngress");
if (ingressObj instanceof List) {
for (Object ingressRuleObj : (List<?>) ingressObj) {
if (ingressRuleObj instanceof Map) {
Map<String, Object> ingressRule = (Map<String, Object>) ingressRuleObj;
if (isMissingDescription(ingressRule)) {
violations.add(createRuleDescriptionViolation(resourceId, resource.getType(), "ingress"));
}
}
}
}
Object egressObj = properties.get("SecurityGroupEgress");
if (egressObj instanceof List) {
for (Object egressRuleObj : (List<?>) egressObj) {
if (egressRuleObj instanceof Map) {
Map<String, Object> egressRule = (Map<String, Object>) egressRuleObj;
if (isMissingDescription(egressRule)) {
violations.add(createRuleDescriptionViolation(resourceId, resource.getType(), "egress"));
}
}
}
}
}
private void checkRuleDescription(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
if (isMissingDescription(properties)) {
String direction = resource.getType().contains("Ingress") ? "ingress" : "egress";
violations.add(createRuleDescriptionViolation(resourceId, resource.getType(), direction));
}
}
private boolean isMissingDescription(Map<String, Object> rule) {
String description = (String) rule.get("Description");
return description == null || description.trim().isEmpty();
}
private Violation createGroupDescriptionViolation(String resourceId, String resourceType) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("Security Group is missing group description")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Add a meaningful description to the security group",
"Describe the purpose and scope of the security group"
))
.build();
}
private Violation createRuleDescriptionViolation(String resourceId, String resourceType, String direction) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("Security Group " + direction + " rule is missing description")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Add descriptions to all security group rules",
"Describe the purpose and source of each rule"
))
.build();
}
@Override
public boolean isEnabled() {
return true;
}
}
4. Encryption Rules
// S3BucketEncryptionRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class S3BucketEncryptionRule implements CfnNagRule {
@Override
public String getRuleId() {
return "F4";
}
@Override
public String getRuleDescription() {
return "S3 bucket should have server-side encryption enabled";
}
@Override
public RuleType getRuleType() {
return RuleType.ENCRYPTION;
}
@Override
public Severity getSeverity() {
return Severity.FAIL;
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of("AWS::S3::Bucket");
}
@Override
@SuppressWarnings("unchecked")
public List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource) {
List<Violation> violations = new ArrayList<>();
try {
Map<String, Object> properties = resource.getProperties();
if (properties == null) {
violations.add(createViolation(resourceId, resource.getType()));
return violations;
}
// Check for bucket encryption
Object bucketEncryption = properties.get("BucketEncryption");
if (bucketEncryption == null) {
violations.add(createViolation(resourceId, resource.getType()));
} else if (bucketEncryption instanceof Map) {
Map<String, Object> encryptionMap = (Map<String, Object>) bucketEncryption;
Object serverSideEncryptionRules = encryptionMap.get("ServerSideEncryptionConfiguration");
if (serverSideEncryptionRules == null) {
violations.add(createViolation(resourceId, resource.getType()));
}
}
} catch (Exception e) {
log.error("Error evaluating S3 bucket encryption rule for resource: {}", resourceId, e);
}
return violations;
}
private Violation createViolation(String resourceId, String resourceType) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("S3 bucket does not have server-side encryption enabled")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Enable server-side encryption for the S3 bucket",
"Use AWS managed keys (SSE-S3) or customer managed keys (SSE-KMS)",
"Consider enabling bucket key for SSE-KMS to reduce API calls"
))
.ruleDocumentation("https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html")
.build();
}
@Override
public boolean isEnabled() {
return true;
}
}
// EbsVolumeEncryptionRule.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class EbsVolumeEncryptionRule implements CfnNagRule {
@Override
public String getRuleId() {
return "F5";
}
@Override
public String getRuleDescription() {
return "EBS volume should have encryption enabled";
}
@Override
public RuleType getRuleType() {
return RuleType.ENCRYPTION;
}
@Override
public Severity getSeverity() {
return Severity.FAIL;
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of(
"AWS::EC2::Volume",
"AWS::EC2::LaunchTemplate",
"AWS::AutoScaling::LaunchConfiguration"
);
}
@Override
@SuppressWarnings("unchecked")
public List<Violation> evaluate(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource) {
List<Violation> violations = new ArrayList<>();
try {
Map<String, Object> properties = resource.getProperties();
if (properties == null) return violations;
switch (resource.getType()) {
case "AWS::EC2::Volume":
checkVolumeEncryption(resourceId, resource, properties, violations);
break;
case "AWS::EC2::LaunchTemplate":
checkLaunchTemplateEncryption(resourceId, resource, properties, violations);
break;
case "AWS::AutoScaling::LaunchConfiguration":
checkLaunchConfigurationEncryption(resourceId, resource, properties, violations);
break;
}
} catch (Exception e) {
log.error("Error evaluating EBS volume encryption rule for resource: {}", resourceId, e);
}
return violations;
}
private void checkVolumeEncryption(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
Boolean encrypted = (Boolean) properties.get("Encrypted");
if (encrypted == null || !encrypted) {
violations.add(createViolation(resourceId, resource.getType()));
}
}
@SuppressWarnings("unchecked")
private void checkLaunchTemplateEncryption(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
Map<String, Object> launchTemplateData = (Map<String, Object>) properties.get("LaunchTemplateData");
if (launchTemplateData != null) {
List<Map<String, Object>> blockDeviceMappings = (List<Map<String, Object>>)
launchTemplateData.get("BlockDeviceMappings");
checkBlockDeviceMappings(resourceId, resource.getType(), blockDeviceMappings, violations);
}
}
@SuppressWarnings("unchecked")
private void checkLaunchConfigurationEncryption(String resourceId, CloudFormationTemplate.Resource resource,
Map<String, Object> properties, List<Violation> violations) {
List<Map<String, Object>> blockDeviceMappings = (List<Map<String, Object>>)
properties.get("BlockDeviceMappings");
checkBlockDeviceMappings(resourceId, resource.getType(), blockDeviceMappings, violations);
}
@SuppressWarnings("unchecked")
private void checkBlockDeviceMappings(String resourceId, String resourceType,
List<Map<String, Object>> blockDeviceMappings,
List<Violation> violations) {
if (blockDeviceMappings != null) {
for (Map<String, Object> blockDevice : blockDeviceMappings) {
Map<String, Object> ebs = (Map<String, Object>) blockDevice.get("Ebs");
if (ebs != null) {
Boolean encrypted = (Boolean) ebs.get("Encrypted");
if (encrypted == null || !encrypted) {
violations.add(createViolation(resourceId, resourceType));
break;
}
}
}
}
}
private Violation createViolation(String resourceId, String resourceType) {
return Violation.builder()
.ruleId(getRuleId())
.type(getRuleType())
.severity(getSeverity())
.message("EBS volume does not have encryption enabled")
.logicalResourceId(resourceId)
.resourceType(resourceType)
.ruleDescription(getRuleDescription())
.recommendations(List.of(
"Enable encryption for all EBS volumes",
"Use AWS managed keys or customer managed keys (CMK)",
"Ensure encryption is enabled for root and data volumes"
))
.ruleDocumentation("https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html")
.build();
}
@Override
public boolean isEnabled() {
return true;
}
}
Template Parser and Analysis Engine
// CloudFormationParser.java
package com.example.cfnnag.service;
import com.example.cfnnag.model.CloudFormationTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import java.io.InputStream;
import java.util.Map;
@Slf4j
@Service
public class CloudFormationParser {
private final Yaml yaml;
public CloudFormationParser() {
this.yaml = new Yaml(new SafeConstructor());
}
/**
* Parse CloudFormation template from input stream
*/
public CloudFormationTemplate parseTemplate(InputStream inputStream) {
try {
Object document = yaml.load(inputStream);
if (document instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> templateMap = (Map<String, Object>) document;
return mapToCloudFormationTemplate(templateMap);
} else {
throw new TemplateParseException("Invalid CloudFormation template format");
}
} catch (Exception e) {
log.error("Error parsing CloudFormation template", e);
throw new TemplateParseException("Failed to parse CloudFormation template", e);
}
}
/**
* Convert YAML map to CloudFormationTemplate object
*/
@SuppressWarnings("unchecked")
private CloudFormationTemplate mapToCloudFormationTemplate(Map<String, Object> templateMap) {
CloudFormationTemplate template = new CloudFormationTemplate();
try {
// Set basic properties
template.setAwsTemplateFormatVersion((String) templateMap.get("AWSTemplateFormatVersion"));
template.setDescription((String) templateMap.get("Description"));
// Set parameters
template.setParameters((Map<String, CloudFormationTemplate.Parameter>)
templateMap.get("Parameters"));
// Set resources
template.setResources((Map<String, CloudFormationTemplate.Resource>)
templateMap.get("Resources"));
// Set outputs
template.setOutputs((Map<String, CloudFormationTemplate.Output>)
templateMap.get("Outputs"));
// Set other sections
template.setMetadata((Map<String, Object>) templateMap.get("Metadata"));
template.setMappings((Map<String, Object>) templateMap.get("Mappings"));
template.setConditions((Map<String, Object>) templateMap.get("Conditions"));
template.setTransforms((Map<String, Object>) templateMap.get("Transforms"));
// Validate required sections
if (template.getResources() == null || template.getResources().isEmpty()) {
throw new TemplateParseException("CloudFormation template must contain Resources section");
}
log.info("Successfully parsed CloudFormation template with {} resources",
template.getResources().size());
return template;
} catch (Exception e) {
log.error("Error mapping CloudFormation template", e);
throw new TemplateParseException("Failed to map CloudFormation template", e);
}
}
/**
* Validate CloudFormation template structure
*/
public boolean isValidTemplate(CloudFormationTemplate template) {
if (template == null) return false;
if (template.getResources() == null || template.getResources().isEmpty()) return false;
// Additional validation logic can be added here
return true;
}
public static class TemplateParseException extends RuntimeException {
public TemplateParseException(String message) {
super(message);
}
public TemplateParseException(String message, Throwable cause) {
super(message, cause);
}
}
}
// CfnNagService.java
package com.example.cfnnag.service;
import com.example.cfnnag.model.*;
import com.example.cfnnag.rules.CfnNagRule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class CfnNagService {
private final CloudFormationParser cloudFormationParser;
private final List<CfnNagRule> cfnNagRules;
public CfnNagService(CloudFormationParser cloudFormationParser,
List<CfnNagRule> cfnNagRules) {
this.cloudFormationParser = cloudFormationParser;
this.cfnNagRules = cfnNagRules;
}
/**
* Analyze CloudFormation template
*/
public CfnNagResponse analyzeTemplate(CfnNagRequest request) {
log.info("Starting CFN-NAG analysis");
try (InputStream inputStream = request.getTemplate().getInputStream()) {
// Parse CloudFormation template
CloudFormationTemplate template = cloudFormationParser.parseTemplate(inputStream);
if (!cloudFormationParser.isValidTemplate(template)) {
return CfnNagResponse.builder()
.success(false)
.message("Invalid CloudFormation template")
.timestamp(LocalDateTime.now())
.build();
}
// Perform analysis
List<Violation> allViolations = analyzeTemplateResources(template, request.getOptions());
// Filter by severity
List<Violation> filteredViolations = filterViolationsBySeverity(
allViolations, request.getOptions().getMinSeverity()
);
// Generate summary
AnalysisSummary summary = generateSummary(template, filteredViolations, request.getOptions());
// Determine success based on options
boolean success = determineSuccess(filteredViolations, request.getOptions());
log.info("CFN-NAG analysis completed: {} violations found", filteredViolations.size());
return CfnNagResponse.builder()
.success(success)
.message(success ? "Analysis completed successfully" : "Violations found")
.timestamp(LocalDateTime.now())
.summary(summary)
.violations(filteredViolations)
.build();
} catch (Exception e) {
log.error("CFN-NAG analysis failed", e);
return CfnNagResponse.builder()
.success(false)
.message("Analysis failed: " + e.getMessage())
.timestamp(LocalDateTime.now())
.build();
}
}
/**
* Analyze all resources in the template
*/
private List<Violation> analyzeTemplateResources(CloudFormationTemplate template,
CfnNagRequest.AnalysisOptions options) {
List<Violation> allViolations = new ArrayList<>();
if (template.getResources() == null) {
return allViolations;
}
for (Map.Entry<String, CloudFormationTemplate.Resource> entry : template.getResources().entrySet()) {
String resourceId = entry.getKey();
CloudFormationTemplate.Resource resource = entry.getValue();
List<Violation> resourceViolations = analyzeResource(template, resourceId, resource, options);
allViolations.addAll(resourceViolations);
}
return allViolations;
}
/**
* Analyze single resource
*/
private List<Violation> analyzeResource(CloudFormationTemplate template, String resourceId,
CloudFormationTemplate.Resource resource,
CfnNagRequest.AnalysisOptions options) {
List<Violation> violations = new ArrayList<>();
// Run all applicable rules
for (CfnNagRule rule : cfnNagRules) {
if (rule.isEnabled() && isRuleApplicable(rule, resource)) {
try {
List<Violation> ruleViolations = rule.evaluate(template, resourceId, resource);
violations.addAll(ruleViolations);
} catch (Exception e) {
log.error("Rule {} failed for resource {}", rule.getRuleId(), resourceId, e);
}
}
}
return violations;
}
/**
* Check if a rule is applicable to the resource
*/
private boolean isRuleApplicable(CfnNagRule rule, CloudFormationTemplate.Resource resource) {
return rule.getSupportedResourceTypes().contains(resource.getType());
}
/**
* Filter violations by minimum severity
*/
private List<Violation> filterViolationsBySeverity(List<Violation> violations, Severity minSeverity) {
return violations.stream()
.filter(violation -> {
switch (minSeverity) {
case FAIL:
return violation.isFail();
case WARN:
return violation.isFail() || violation.isWarn();
case INFO:
return true; // Include all
default:
return true;
}
})
.collect(Collectors.toList());
}
/**
* Generate analysis summary
*/
private AnalysisSummary generateSummary(CloudFormationTemplate template,
List<Violation> violations,
CfnNagRequest.AnalysisOptions options) {
int failCount = (int) violations.stream().filter(Violation::isFail).count();
int warnCount = (int) violations.stream().filter(Violation::isWarn).count();
int infoCount = (int) violations.stream().filter(Violation::isInfo).count();
int totalViolations = violations.size();
int totalResources = template.getResources() != null ? template.getResources().size() : 0;
// Calculate compliance score (0-100)
double complianceScore = calculateComplianceScore(totalResources, violations);
// Determine status
String status = determineStatus(failCount, warnCount, options);
return AnalysisSummary.builder()
.totalResources(totalResources)
.failViolations(failCount)
.warnViolations(warnCount)
.infoViolations(infoCount)
.totalViolations(totalViolations)
.status(status)
.complianceScore(complianceScore)
.build();
}
/**
* Calculate compliance score (0-100)
*/
private double calculateComplianceScore(int totalResources, List<Violation> violations) {
if (totalResources == 0) return 100.0;
// Weight violations by severity
double penalty = 0;
for (Violation violation : violations) {
switch (violation.getSeverity()) {
case FAIL:
penalty += 10;
break;
case WARN:
penalty += 5;
break;
case INFO:
penalty += 1;
break;
}
}
double maxScore = totalResources * 10; // Maximum possible penalty
double actualPenalty = Math.min(penalty, maxScore);
return Math.max(0, 100 - (actualPenalty / maxScore * 100));
}
/**
* Determine analysis status
*/
private String determineStatus(int failCount, int warnCount,
CfnNagRequest.AnalysisOptions options) {
if (failCount > 0) {
return "FAIL";
} else if (options.isFailOnWarnings() && warnCount > 0) {
return "FAIL";
} else if (warnCount > 0) {
return "WARN";
} else {
return "PASS";
}
}
/**
* Determine overall success
*/
private boolean determineSuccess(List<Violation> violations,
CfnNagRequest.AnalysisOptions options) {
boolean hasFailViolations = violations.stream().anyMatch(Violation::isFail);
boolean hasWarnViolations = violations.stream().anyMatch(Violation::isWarn);
if (hasFailViolations) {
return false;
}
if (options.isFailOnWarnings() && hasWarnViolations) {
return false;
}
return true;
}
/**
* Generate report in different formats
*/
public String generateReport(CfnNagResponse response, String format) {
switch (format.toLowerCase()) {
case "json":
return generateJsonReport(response);
case "yaml":
return generateYamlReport(response);
case "html":
return generateHtmlReport(response);
case "text":
default:
return generateTextReport(response);
}
}
private String generateTextReport(CfnNagResponse response) {
StringBuilder report = new StringBuilder();
AnalysisSummary summary = response.getSummary();
report.append("CFN-NAG Analysis Report\n");
report.append("=======================\n\n");
report.append(String.format("Status: %s\n", summary.getStatus()));
report.append(String.format("Compliance Score: %.1f/100\n", summary.getComplianceScore()));
report.append(String.format("Total Resources: %d\n", summary.getTotalResources()));
report.append(String.format("Fail Violations: %d\n", summary.getFailViolations()));
report.append(String.format("Warn Violations: %d\n", summary.getWarnViolations()));
report.append(String.format("Info Violations: %d\n\n", summary.getInfoViolations()));
// Group by resource type
response.getViolations().stream()
.collect(Collectors.groupingBy(Violation::getResourceType))
.forEach((resourceType, violations) -> {
report.append(resourceType).append(":\n");
violations.forEach(violation -> {
report.append(String.format(" [%s] %s: %s\n",
violation.getSeverity(),
violation.getLogicalResourceId(),
violation.getMessage()));
});
report.append("\n");
});
return report.toString();
}
private String generateJsonReport(CfnNagResponse response) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(response);
} catch (Exception e) {
log.error("Error generating JSON report", e);
return "{\"error\": \"Failed to generate JSON report\"}";
}
}
private String generateYamlReport(CfnNagResponse response) {
try {
org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml();
return yaml.dump(response);
} catch (Exception e) {
log.error("Error generating YAML report", e);
return "error: Failed to generate YAML report";
}
}
private String generateHtmlReport(CfnNagResponse response) {
// HTML report implementation similar to kube-score
// ... (implementation details)
return "<html>CFN-NAG Report</html>";
}
}
REST Controller
// CfnNagController.java
package com.example.cfnnag.controller;
import com.example.cfnnag.model.CfnNagRequest;
import com.example.cfnnag.model.CfnNagResponse;
import com.example.cfnnag.service.CfnNagService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/cfn-nag")
public class CfnNagController {
private final CfnNagService cfnNagService;
public CfnNagController(CfnNagService cfnNagService) {
this.cfnNagService = cfnNagService;
}
@PostMapping(value = "/analyze", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<CfnNagResponse> analyzeTemplate(@ModelAttribute CfnNagRequest request) {
log.info("Received CFN-NAG analysis request");
try {
CfnNagResponse response = cfnNagService.analyzeTemplate(request);
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Analysis request failed", e);
return ResponseEntity.badRequest().body(
CfnNagResponse.builder()
.success(false)
.message("Analysis failed: " + e.getMessage())
.build()
);
}
}
@PostMapping(value = "/analyze/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Map<String, Object>> analyzeTemplateFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "failOnWarnings", defaultValue = "true") boolean failOnWarnings,
@RequestParam(value = "outputFormat", defaultValue = "json") String outputFormat) {
log.info("Received file analysis request: {}", file.getOriginalFilename());
try {
CfnNagRequest request = new CfnNagRequest();
request.setTemplate(file);
request.getOptions().setFailOnWarnings(failOnWarnings);
request.getOptions().setOutputFormat(outputFormat);
CfnNagResponse response = cfnNagService.analyzeTemplate(request);
return ResponseEntity.ok(Map.of(
"success", response.isSuccess(),
"summary", response.getSummary(),
"violations", response.getViolations(),
"timestamp", response.getTimestamp()
));
} catch (Exception e) {
log.error("File analysis failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@GetMapping("/report")
public ResponseEntity<String> generateReport(
@RequestParam String format,
@RequestBody CfnNagResponse analysisResult) {
try {
String report = cfnNagService.generateReport(analysisResult, format);
HttpHeaders headers = new HttpHeaders();
switch (format.toLowerCase()) {
case "json":
headers.setContentType(MediaType.APPLICATION_JSON);
break;
case "yaml":
headers.setContentType(MediaType.parseMediaType("application/yaml"));
break;
case "html":
headers.setContentType(MediaType.TEXT_HTML);
break;
default:
headers.setContentType(MediaType.TEXT_PLAIN);
}
return ResponseEntity.ok()
.headers(headers)
.body(report);
} catch (Exception e) {
log.error("Report generation failed", e);
return ResponseEntity.badRequest().body("Report generation failed: " + e.getMessage());
}
}
@GetMapping("/rules")
public ResponseEntity<Map<String, Object>> listRules() {
// Implementation to list all available rules
return ResponseEntity.ok(Map.of(
"totalRules", 0,
"rules", java.util.List.of()
));
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
return ResponseEntity.ok(Map.of(
"status", "healthy",
"service", "cfn-nag",
"timestamp", java.time.LocalDateTime.now()
));
}
}
Testing
1. Unit Tests
// CfnNagServiceTest.java
package com.example.cfnnag.service;
import com.example.cfnnag.model.CfnNagRequest;
import com.example.cfnnag.model.CfnNagResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class CfnNagServiceTest {
@Mock
private CloudFormationParser cloudFormationParser;
@Mock
private MultipartFile multipartFile;
@InjectMocks
private CfnNagService cfnNagService;
@Test
void testAnalyzeTemplate_Success() throws Exception {
// Setup
String templateContent = """
AWSTemplateFormatVersion: '2010-09-09'
Resources:
MyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-test-bucket
""";
InputStream inputStream = new ByteArrayInputStream(templateContent.getBytes(StandardCharsets.UTF_8));
when(multipartFile.getInputStream()).thenReturn(inputStream);
CfnNagRequest request = new CfnNagRequest();
request.setTemplate(multipartFile);
// Execute
CfnNagResponse response = cfnNagService.analyzeTemplate(request);
// Verify
assertNotNull(response);
// Add more assertions based on your implementation
}
}
// IamWildcardActionRuleTest.java
package com.example.cfnnag.rules;
import com.example.cfnnag.model.CloudFormationTemplate;
import com.example.cfnnag.model.Violation;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class IamWildcardActionRuleTest {
private final IamWildcardActionRule rule = new IamWildcardActionRule();
@Test
void testEvaluate_WildcardAction() {
// Setup
CloudFormationTemplate template = new CloudFormationTemplate();
String resourceId = "MyPolicy";
CloudFormationTemplate.Resource resource = new CloudFormationTemplate.Resource();
resource.setType("AWS::IAM::Policy");
Map<String, Object> properties = Map.of(
"PolicyDocument", Map.of(
"Statement", List.of(
Map.of(
"Effect", "Allow",
"Action", "*",
"Resource", "arn:aws:s3:::my-bucket/*"
)
)
)
);
resource.setProperties(properties);
// Execute
List<Violation> violations = rule.evaluate(template, resourceId, resource);
// Verify
assertFalse(violations.isEmpty());
assertTrue(violations.stream().anyMatch(v -> v.getMessage().contains("wildcard")));
}
}
2. Integration Test
// CfnNagIntegrationTest.java
package com.example.cfnnag.integration;
import com.example.cfnnag.controller.CfnNagController;
import com.example.cfnnag.model.CfnNagRequest;
import com.example.cfnnag.model.Severity;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
class CfnNagIntegrationTest {
@Autowired
private CfnNagController cfnNagController;
@Test
void testFullAnalysisWorkflow() {
// Setup
String templateContent = """
AWSTemplateFormatVersion: '2010-09-09'
Description: Test CloudFormation template
Resources:
MyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: "*"
Resource: "*"
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Test security group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
""";
MockMultipartFile file = new MockMultipartFile(
"file", "template.yaml", "text/yaml", templateContent.getBytes()
);
// Execute
var response = cfnNagController.analyzeTemplateFile(file, true, "json");
// Verify
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
// Add more specific assertions
}
}
Production Considerations
1. Configuration
// CfnNagConfig.java
package com.example.cfnnag.config;
import com.example.cfnnag.rules.CfnNagRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class CfnNagConfig {
@Bean
public List<CfnNagRule> cfnNagRules(List<CfnNagRule> rules) {
// All CfnNagRule implementations are automatically injected
return rules;
}
}
2. Error Handling
// GlobalExceptionHandler.java
package com.example.cfnnag.controller;
import com.example.cfnnag.service.CloudFormationParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CloudFormationParser.TemplateParseException.class)
public ResponseEntity<Map<String, Object>> handleTemplateParseException(
CloudFormationParser.TemplateParseException e) {
log.error("Template parse error", e);
return ResponseEntity.badRequest().body(Map.of(
"error", "Template parse failed",
"message", e.getMessage()
));
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, Object>> handleMaxSizeException(MaxUploadSizeExceededException e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "File too large",
"message", "Maximum upload size exceeded"
));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception e) {
log.error("Unexpected error", e);
return ResponseEntity.internalServerError().body(Map.of(
"error", "Internal server error",
"message", "An unexpected error occurred"
));
}
}
Best Practices
- Performance:
- Use streaming parsing for large CloudFormation templates
- Implement rule caching for frequently analyzed templates
- Use parallel processing for multiple rule evaluations
- Extensibility:
- Easy to add new rules via CfnNagRule interface
- Configurable rule sets and severity levels
- Support for custom rule directories
- Security:
- Validate CloudFormation template structure
- Limit file upload sizes
- Sanitize output for different formats
- Compliance:
- Support for industry standards (HIPAA, PCI-DSS, etc.)
- Configurable compliance frameworks
- Detailed remediation guidance
Conclusion
This CFN-NAG implementation provides:
- Comprehensive CloudFormation template analysis with multiple security checks
- Extensible rule engine for adding new security and compliance rules
- Multiple output formats for different use cases
- Production-ready features including error handling and validation
- REST API for easy integration with CI/CD pipelines
The solution can be extended with:
- Custom rule plugins for organization-specific policies
- Integration with AWS Config for continuous compliance
- Historical analysis and trend reporting
- Team-specific rule sets and exemptions
- Automated remediation suggestions