use diagnostics::{DetailLevel, Diagnostic, ValidationReport};
use rego_engine::RegoEngine;
use rules::{FilterConfig, IdRange, RuleFilterConfig, RuleOrigin, Severity, rule_number};
use schema_validator::SchemaValidator;
use std::sync::LazyLock;
use template_model::{PseudoParameterOverrides, SemanticModel};
use validation_engine::guard::resolve_guard_config;
use validation_engine::{EngineConfig, ExternalRuleSource, ValidateConfig, ValidationEngine, validate_bytes};
static SHARED_ENGINE: LazyLock<RegoEngine> = LazyLock::new(|| RegoEngine::new(EngineConfig::default()).unwrap());
static SHARED_SV: LazyLock<SchemaValidator> = LazyLock::new(SchemaValidator::default);
fn validate_fixture(path: &str) -> ValidationReport {
let full = format!("../resources/templates/{}", path);
let bytes = std::fs::read(&full).unwrap_or_else(|e| panic!("Failed to read {}: {}", full, e));
validate_bytes(&*SHARED_ENGINE, &SHARED_SV, &bytes, ValidateConfig::default())
.unwrap_or_else(|e| panic!("Failed to validate {}: {}", full, e))
}
fn validate_with_config(path: &str, config: ValidateConfig) -> ValidationReport {
let full = format!("../resources/templates/{}", path);
let bytes = std::fs::read(&full).unwrap_or_else(|e| panic!("Failed to read {}: {}", full, e));
validate_bytes(&*SHARED_ENGINE, &SHARED_SV, &bytes, config)
.unwrap_or_else(|e| panic!("Failed to validate {}: {}", full, e))
}
fn has_rule(report: &ValidationReport, rule_id: &str) -> bool {
report.diagnostics.iter().any(|d| d.rule_id == rule_id)
}
fn no_errors(report: &ValidationReport) -> bool {
report.metadata.counts.fatal == 0 && report.metadata.counts.errors == 0
}
#[test]
fn e2e_all_good_fixtures_no_errors() {
for fixture in [
"good/minimal.yaml",
"good/generic.yaml",
"good/functions_findinmap.yaml",
"good/resources_codepipeline.yaml",
"good/vpc_subnets.yaml",
"good/ecs_fargate.yaml",
"good/ecs_fargate_valid.yaml",
"good/cloudfront_valid.yaml",
"good/iam_valid.yaml",
"good/both_forms.yaml",
"good/complex_conditions.yaml",
"good/deletion_policies.yaml",
"good/mappings_valid.yaml",
"good/stepfunctions_valid.yaml",
"good/codepipeline_artifact_counts.yaml",
"good/ssm_document_valid.yaml",
"good/aurora_dbinstance.yaml",
"good/lambda_zipfile.yaml",
"good/dynamodb_provisioned.yaml",
"good/sqs_fifo_valid.yaml",
"good/lambda_snapstart.yaml",
"good/ecs_awsvpc_valid.yaml",
"good/dynamodb_valid_attributes.yaml",
"good/simple_sub_prefix.yaml",
] {
let report = validate_fixture(fixture);
assert!(
no_errors(&report),
"Expected no errors in {}, got: {:?}",
fixture,
report
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error || d.severity == Severity::Fatal)
.collect::<Vec<_>>()
);
}
}
#[test]
fn e2e_bad_circular_deps() {
let report = validate_fixture("bad/resources_circular_dependency.yaml");
assert!(
has_rule(&report, "F3004") || has_rule(&report, "F0000"),
"Expected circular dependency error, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_integration_ref_no_value() {
let report = validate_fixture("integration/ref-no-value.yaml");
let allowed_resources = ["IamRole1", "IamRole2", "IamRole3", "CloudFront1", "CloudFront2"];
assert!(
report.diagnostics.iter().all(|d| d.severity != Severity::Error
|| d.resource_logical_id().is_some_and(|id| allowed_resources.contains(&id))),
"Unexpected resource with errors, got: {:?}",
report.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect::<Vec<_>>()
);
}
#[test]
fn e2e_integration_dynamic_references() {
let report = validate_fixture("integration/dynamic-references.yaml");
assert!(
report.diagnostics.iter().all(|d| d.severity != Severity::Error
|| d.resource_logical_id() == Some("SESEventSourceMappingBadDynamicReference")),
"Expected errors only on bad dynamic ref resource, got: {:?}",
report.diagnostics.iter().filter(|d| d.severity == Severity::Error).collect::<Vec<_>>()
);
}
#[test]
fn e2e_suppress_exact_id() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { ids: vec!["W9008".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_with_config("good/minimal.yaml", config);
assert!(!has_rule(&report, "W9008"));
}
#[test]
fn e2e_suppress_prefix() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { id_patterns: vec!["^W".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_with_config("good/generic.yaml", config);
assert!(!report.diagnostics.iter().any(|d| d.rule_id.starts_with('W')));
}
#[test]
fn e2e_include_only() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig { ids: vec!["F3004".into(), "F0000".into()], ..Default::default() },
RuleFilterConfig::default(),
),
..Default::default()
};
let report = validate_with_config("bad/resources_circular_dependency.yaml", config);
for d in &report.diagnostics {
assert!(d.rule_id == "F3004" || d.rule_id == "F0000", "Expected only F3004/F0000, got {}", d.rule_id);
}
}
#[test]
fn e2e_severity_filter() {
let config = ValidateConfig { severity_level: Severity::Error, ..Default::default() };
let report = validate_with_config("good/generic.yaml", config);
for d in &report.diagnostics {
assert_eq!(d.severity, Severity::Error, "Expected only errors, got {:?}", d);
}
}
#[test]
fn e2e_change_severity() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { id_patterns: vec!["^W".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_with_config("good/generic.yaml", config);
assert!(!report.diagnostics.iter().any(|d| d.rule_id.starts_with('W') && d.severity == Severity::Warn));
}
#[test]
fn e2e_json_output() {
let report = validate_fixture("good/minimal.yaml");
let json = serde_json::to_string_pretty(&report).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_ne!(parsed.get("diagnostics"), None, "expected 'diagnostics' key in report");
assert_ne!(parsed.get("metadata"), None, "expected 'metadata' key in report");
}
#[test]
fn e2e_diagnostics_sorted() {
let report = validate_fixture("good/generic.yaml");
for w in report.diagnostics.windows(2) {
let key = |d: &Diagnostic| {
(
std::cmp::Reverse(d.severity),
rule_number(&d.rule_id),
d.rule_id.clone(),
d.location.as_ref().map_or(0, |l| l.start_line),
d.location.as_ref().map_or(0, |l| l.start_column),
d.property_path.clone(),
d.message.clone(),
)
};
assert!(key(&w[0]) <= key(&w[1]), "Diagnostics not sorted: {:?} > {:?}", w[0], w[1]);
}
}
#[test]
fn e2e_engine_reusable() {
let bytes1 = std::fs::read("../resources/templates/good/minimal.yaml").unwrap();
let bytes2 = std::fs::read("../resources/templates/good/generic.yaml").unwrap();
let r1 = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, &bytes1, ValidateConfig::default()).unwrap();
let r2 = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, &bytes2, ValidateConfig::default()).unwrap();
assert!(no_errors(&r1));
assert!(no_errors(&r2));
}
#[test]
fn e2e_bad_security_issues() {
let report = validate_fixture("bad/security_issues.yaml");
assert!(report.diagnostics.iter().all(|d| d.rule_id != "W9501" && d.rule_id != "W9511"));
}
#[test]
fn e2e_bad_unknown_properties() {
let report = validate_fixture("bad/unknown_properties.yaml");
assert!(has_rule(&report, "F3006"), "Expected F3006 for unknown AWS type, got: {:?}", report.diagnostics);
assert!(has_rule(&report, "F3002"), "Expected F3002 for unknown property, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_rules_evaluated_nonzero() {
let config = ValidateConfig { ..Default::default() };
let report = validate_with_config("good/minimal.yaml", config);
assert!(
report.metadata.rules_evaluated > 0,
"Expected rules_evaluated > 0, got {}",
report.metadata.rules_evaluated
);
}
#[test]
fn e2e_all_diagnostics_have_rule_ids() {
let report = validate_fixture("bad/generic.yaml");
for d in &report.diagnostics {
assert!(!d.rule_id.is_empty(), "Diagnostic has empty rule_id: {:?}", d);
}
}
#[test]
fn e2e_schema_validates_nested_properties() {
let input = r#"
AWSTemplateFormatVersion: "2010-09-09"
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: test
NotARealProperty: bad
"#;
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input.as_bytes(), ValidateConfig::default()).unwrap();
assert!(
report.diagnostics.iter().any(|d| d.rule_id == "F3002"),
"Expected F3002 for unknown property, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_schema_validates_enum() {
let input = r#"
AWSTemplateFormatVersion: "2010-09-09"
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: InvalidValue
"#;
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input.as_bytes(), ValidateConfig::default()).unwrap();
assert!(
report.diagnostics.iter().any(|d| d.rule_id == "W3030"),
"Expected W3030 for invalid enum, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_getatt_dotted_attribute_on_object_attribute_is_still_invalid() {
let input = r#"
AWSTemplateFormatVersion: "2010-09-09"
Resources:
Bucket:
Type: AWS::S3::Bucket
Param:
Type: AWS::SSM::Parameter
Properties:
Type: String
Value: !GetAtt Bucket.Tags.0
"#;
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input.as_bytes(), ValidateConfig::default()).unwrap();
assert!(
has_rule(&report, "E9004"),
"Expected E9004 for a dotted GetAtt into an object/array attribute, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_getatt_provisioned_product_outputs_member_is_valid() {
let ok = r#"
AWSTemplateFormatVersion: "2010-09-09"
Resources:
PP:
Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
Properties:
ProductName: p
ProvisioningArtifactName: v1
UseOutput:
Type: AWS::SNS::Topic
Properties:
DisplayName: !GetAtt PP.Outputs.MyKey
"#;
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, ok.as_bytes(), ValidateConfig::default()).unwrap();
assert!(
!has_rule(&report, "E9004"),
"A provisioned product Outputs.<key> member must not be flagged, got: {:?}",
report.diagnostics
);
let bad = r#"
AWSTemplateFormatVersion: "2010-09-09"
Resources:
PP:
Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
Properties:
ProductName: p
ProvisioningArtifactName: v1
UseBad:
Type: AWS::SNS::Topic
Properties:
DisplayName: !GetAtt PP.NotARealAttr
"#;
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, bad.as_bytes(), ValidateConfig::default()).unwrap();
assert!(
has_rule(&report, "E9004"),
"An invalid provisioned product attribute must still be flagged, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_bad_ecs_fargate_invalid_subnet() {
let report = validate_fixture("bad/ecs_fargate_mismatch.yaml");
assert!(
!has_rule(&report, "E3054"),
"E3054 must not fire for an awsvpc TaskDefinition, got: {:?}",
report.diagnostics
);
assert!(has_rule(&report, "E1154"), "Expected E1154 for invalid subnet id, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_bad_subnet_overlap() {
let report = validate_fixture("bad/subnet_overlap.yaml");
assert!(has_rule(&report, "E3060"), "Expected E3060 for subnet overlap, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_bad_rds_public() {
let report = validate_fixture("bad/rds_public.yaml");
assert!(has_rule(&report, "W9011"), "Expected W9011 for RDS PubliclyAccessible, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_diagnostics_have_source_locations() {
let report = validate_fixture("bad/generic.yaml");
let with_resource: Vec<_> = report.diagnostics.iter().filter(|d| d.resource_logical_id().is_some()).collect();
assert!(!with_resource.is_empty(), "Expected diagnostics with resource_id for bad/generic.yaml");
let with_location = with_resource.iter().filter(|d| d.location.as_ref().is_some_and(|l| l.start_line > 0)).count();
assert!(
with_location > 0,
"Expected diagnostics with source locations, got none out of {} with resource_id",
with_resource.len()
);
}
#[test]
fn e2e_rego_input_has_outputs_and_mappings() {
let input = b"AWSTemplateFormatVersion: '2010-09-09'\nMappings:\n M:\n k1:\n k2: val\nResources:\n R:\n Type: AWS::S3::Bucket\nOutputs:\n Out:\n Value: !Ref R\n";
let model = SemanticModel::from_bytes(input).unwrap();
let rego_input = serde_json::to_value(model.to_diagnostic_json()).unwrap();
assert!(rego_input.get("outputs").is_some(), "Rego input missing outputs");
assert!(rego_input.get("mappings").is_some(), "Rego input missing mappings");
}
#[test]
fn e2e_findinmap_bad_map() {
let report = validate_fixture("bad/findinmap_bad.yaml");
assert!(
has_rule(&report, "F1012") || has_rule(&report, "F0000"),
"Expected FindInMap error, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_suggested_fix_on_required_property() {
let input = b"AWSTemplateFormatVersion: '2010-09-09'\nResources:\n Role:\n Type: AWS::IAM::Role\n";
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input, ValidateConfig::default()).unwrap();
let found = report.diagnostics.iter().find(|d| d.rule_id == "F3003");
assert!(found.is_some(), "Expected F3003 for missing required property");
assert!(found.unwrap().suggested_fix.is_some(), "F3003 should have suggested_fix, got: {:?}", found);
}
#[test]
fn e2e_related_locations_on_cross_resource() {
let report = validate_fixture("bad/subnet_overlap.yaml");
let e3060 = report.diagnostics.iter().find(|d| d.rule_id == "E3060").expect("Expected E3060 for subnet overlap");
assert!(
!e3060.related_resources.as_ref().is_none_or(|v| v.is_empty()),
"E3060 should have related_resources for cross-resource diagnostic"
);
}
#[test]
fn e2e_list_rules_comprehensive() {
let rules = SHARED_ENGINE.list_rules();
assert!(!rules.is_empty(), "list_rules should return non-empty");
let ids: Vec<&str> = rules.iter().map(|r| r.id.as_str()).collect();
for expected in ["F0001", "F1010", "W9008"] {
assert!(ids.contains(&expected), "list_rules missing {} in {:?}", expected, ids);
}
for expected in ["F3016", "F0018", "E3601", "E3702", "I3042"] {
assert!(ids.contains(&expected), "list_rules missing {} in {:?}", expected, ids);
}
for expected in ["E3010", "E3013", "F3032", "E3051", "E5001", "I2530", "E1150", "E1151", "E1152", "E1154"] {
assert!(ids.contains(&expected), "list_rules missing {} in {:?}", expected, ids);
}
}
#[test]
fn e2e_fargate_cpu_memory_valid() {
let report = validate_fixture("good/ecs_fargate_valid.yaml");
assert!(
!has_rule(&report, "E3047"),
"Valid Fargate combo should not trigger E3047, got: {:?}",
report.diagnostics.iter().filter(|d| d.rule_id == "E3047").collect::<Vec<_>>()
);
}
#[test]
fn e2e_fargate_cpu_memory_invalid() {
let report = validate_fixture("bad/fargate_bad_cpu_memory.yaml");
assert!(has_rule(&report, "E3047"), "Invalid Fargate combo should trigger E3047, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_sg_port_range() {
let report = validate_fixture("bad/sg_bad_port_range.yaml");
assert!(has_rule(&report, "E9002"), "FromPort > ToPort should trigger E9002, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_iam_bad_statement() {
let report = validate_fixture("bad/iam_bad_statement.yaml");
assert!(has_rule(&report, "E3510"), "Bad IAM statement should trigger E3510, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_cloudfront_bad_origin() {
let report = validate_fixture("bad/cloudfront_bad_origin.yaml");
assert!(has_rule(&report, "E3057"), "Bad TargetOriginId should trigger E3057, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_pipeline_bad_artifacts() {
let report = validate_fixture("bad/codepipeline_bad_artifacts.yaml");
assert!(has_rule(&report, "E3701"), "Bad artifact ref should trigger E3701, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_deprecated_type() {
let report = validate_fixture("bad/deprecated_type.yaml");
assert!(has_rule(&report, "W9009"), "Deprecated type should trigger W9009, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_elb_http_443() {
let report = validate_fixture("bad/elb_http_443.yaml");
assert!(!report.diagnostics.is_empty());
}
#[test]
fn e2e_region_restricted() {
let input =
b"AWSTemplateFormatVersion: '2010-09-09'\nResources:\n R:\n Type: AWS::APS::Scraper\n Properties: {}\n";
let config = ValidateConfig {
pseudo_parameter_overrides: PseudoParameterOverrides {
region: Some("cn-north-1".to_string()),
..Default::default()
},
..Default::default()
};
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input, config).unwrap();
assert!(
has_rule(&report, "F3006"),
"APS::Scraper in cn-north-1 should trigger F3006 (region-availability), got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_region_none_skips() {
let report = validate_fixture("good/minimal.yaml");
assert!(!has_rule(&report, "F3006"));
}
#[test]
fn e2e_sagemaker_instance_types() {
let report = validate_fixture("bad/sagemaker_instance_types.yaml");
for rule in ["E3640", "E3642", "E3643", "E3644"] {
assert!(
has_rule(&report, rule),
"Expected {rule} for invalid SageMaker instance type, got: {:?}",
report.diagnostics
);
}
}
#[test]
fn e2e_opensearch_instance_type() {
let report = validate_fixture("bad/opensearch_instance_type.yaml");
let e3653 = report.diagnostics.iter().filter(|d| d.rule_id == "E3653").count();
assert_eq!(e3653, 1, "Expected exactly one E3653 (only the invalid domain), got: {:?}", report.diagnostics);
}
#[test]
fn e2e_invalid_deletion_policy() {
let report = validate_fixture("bad/invalid_deletion_policy.yaml");
assert!(has_rule(&report, "F3016"), "Invalid DeletionPolicy should trigger F3016, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_invalid_update_replace_policy() {
let report = validate_fixture("bad/invalid_update_replace_policy.yaml");
assert!(
has_rule(&report, "F0018"),
"Invalid UpdateReplacePolicy should trigger F0018, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_invalid_mapping_structure() {
let report = validate_fixture("bad/invalid_mapping_structure.yaml");
assert!(
has_rule(&report, "F0050") || has_rule(&report, "F0017"),
"Invalid mapping structure should trigger F0050 or F0017, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_undefined_condition() {
let report = validate_fixture("bad/undefined_condition.yaml");
assert!(has_rule(&report, "E8002"), "Undefined condition should trigger E8002, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_stepfunctions_bad_start_at() {
let report = validate_fixture("bad/stepfunctions_bad_start_at.yaml");
assert!(has_rule(&report, "E3601"), "Invalid StartAt should trigger E3601, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_codepipeline_bad_artifact_counts() {
let report = validate_fixture("bad/codepipeline_bad_artifact_counts.yaml");
assert!(has_rule(&report, "E3702"), "Wrong artifact count should trigger E3702, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_hardcoded_partition() {
let report = validate_fixture("bad/hardcoded_partition.yaml");
assert!(
!has_rule(&report, "I3042"),
"Plain-string ARN should NOT trigger I3042 (only Fn::Sub does), got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_lambda_runtime_from_data() {
let input = b"AWSTemplateFormatVersion: '2010-09-09'\nResources:\n F:\n Type: AWS::Lambda::Function\n Properties:\n Runtime: python3.7\n Handler: index.handler\n Role: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/role\n Code:\n ZipFile: |\n def handler(event, context): pass\n";
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input, ValidateConfig::default()).unwrap();
assert!(
has_rule(&report, "E2531"),
"python3.7 should trigger E2531 (blocked for new function creation), got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_schema_violations_from_multiple_services() {
let input = b"AWSTemplateFormatVersion: '2010-09-09'\nResources:\n Bucket:\n Type: AWS::S3::Bucket\n Properties:\n NotReal: bad\n VPC:\n Type: AWS::EC2::VPC\n Properties:\n NotReal: bad\n";
let report = validate_bytes(&*SHARED_ENGINE, &SHARED_SV, input, ValidateConfig::default()).unwrap();
let f3002_resources: Vec<&str> =
report.diagnostics.iter().filter(|d| d.rule_id == "F3002").filter_map(|d| d.resource_logical_id()).collect();
assert!(
f3002_resources.contains(&"Bucket") && f3002_resources.contains(&"VPC"),
"F3002 should flag unknown properties from both S3 and EC2, got: {:?}",
f3002_resources
);
}
#[test]
fn e2e_if_wrong_arity() {
let report = validate_fixture("bad/if_wrong_arity.yaml");
assert!(
has_rule(&report, "F0013"),
"Fn::If with 2 elements should trigger parse error, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_equals_wrong_arity() {
let report = validate_fixture("bad/equals_wrong_arity.yaml");
assert!(
has_rule(&report, "E8003"),
"Fn::Equals with 3 elements should trigger E8003 shape error, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_good_codepipeline_artifact_counts() {
let report = validate_fixture("good/codepipeline_artifact_counts.yaml");
assert!(
!has_rule(&report, "E3702"),
"Valid artifact counts should not trigger E3702, got: {:?}",
report.diagnostics.iter().filter(|d| d.rule_id == "E3702").collect::<Vec<_>>()
);
}
#[test]
fn e2e_category_derived_from_rule_id() {
let report = validate_fixture("bad/iam_wildcard_all_types.yaml");
let sec = report.diagnostics.iter().find(|d| d.category.as_deref() == Some("Security"));
assert!(sec.is_some(), "Expected at least one security diagnostic");
}
#[test]
fn e2e_suppress_category_security() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { categories: vec!["security".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_with_config("bad/security_issues.yaml", config);
assert!(
!report.diagnostics.iter().any(|d| d.category.as_deref() == Some("security")),
"security rules should be suppressed by category"
);
}
#[test]
fn e2e_w1020_simple_sub_triggers() {
let report = validate_fixture("bad/simple_sub_param.yaml");
assert!(
!has_rule(&report, "W1020"),
"Simple Sub with one variable should NOT trigger W1020 (only zero-variable Subs do)"
);
}
#[test]
fn e2e_w1020_prefix_sub_no_trigger() {
let report = validate_fixture("good/simple_sub_prefix.yaml");
assert!(!has_rule(&report, "W1020"), "Sub with prefix should not trigger W1020");
}
#[test]
fn e2e_e1029_nested_intrinsic_syntax() {
let report = validate_fixture("bad/sub_nested_intrinsic.yaml");
assert!(!has_rule(&report, "E1029"), "${{! is valid literal escape syntax in Fn::Sub, not an error");
}
#[test]
fn e2e_aurora_exclusions() {
let report = validate_fixture("bad/aurora_with_allocated_storage.yaml");
assert!(!report.diagnostics.is_empty(), "Expected some diagnostics for Aurora with AllocatedStorage");
}
#[test]
fn e2e_aurora_valid() {
let report = validate_fixture("good/aurora_dbinstance.yaml");
assert!(!has_rule(&report, "E3070"), "Valid Aurora should not trigger E3070");
}
#[test]
fn e2e_lambda_zipfile_runtime() {
let report = validate_fixture("bad/lambda_zipfile_java.yaml");
assert!(has_rule(&report, "E3677"), "Expected E3677 for ZipFile with java runtime");
}
#[test]
fn e2e_lambda_zipfile_valid() {
let report = validate_fixture("good/lambda_zipfile.yaml");
assert!(!has_rule(&report, "E3677"), "Valid Lambda ZipFile should not trigger E3677");
}
#[test]
fn e2e_dynamodb_billing_mode() {
let report = validate_fixture("bad/dynamodb_provisioned_no_throughput.yaml");
assert!(has_rule(&report, "F3003"), "Expected F3003 for PROVISIONED without throughput (required property)");
}
#[test]
fn e2e_dynamodb_provisioned_valid() {
let report = validate_fixture("good/dynamodb_provisioned.yaml");
assert!(!has_rule(&report, "E3073"), "Valid DynamoDB should not trigger E3073");
}
#[test]
fn e2e_sqs_fifo_queue_name() {
let report = validate_fixture("bad/sqs_fifo_no_suffix.yaml");
assert!(has_rule(&report, "E3501"), "Expected E3501 for FIFO queue without .fifo suffix");
}
#[test]
fn e2e_e3039_dynamodb_attribute_mismatch() {
let report = validate_fixture("bad/dynamodb_attribute_mismatch.yaml");
assert!(
has_rule(&report, "E3039"),
"Expected E3039 for KeySchema attr not in AttributeDefinitions, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3039_dynamodb_valid_attributes() {
let report = validate_fixture("good/dynamodb_valid_attributes.yaml");
assert!(
!has_rule(&report, "E3039"),
"Valid DynamoDB should not trigger E3039, got: {:?}",
report.diagnostics.iter().filter(|d| d.rule_id == "E3039").collect::<Vec<_>>()
);
}
#[test]
fn e2e_e3044_fargate_daemon() {
let report = validate_fixture("bad/fargate_daemon.yaml");
assert!(has_rule(&report, "E3044"), "Fargate DAEMON should trigger E3044, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_e3501_sqs_fifo_valid() {
let report = validate_fixture("good/sqs_fifo_valid.yaml");
assert!(
!has_rule(&report, "E3501"),
"Valid FIFO queue should not trigger E3501, got: {:?}",
report.diagnostics.iter().filter(|d| d.rule_id == "E3501").collect::<Vec<_>>()
);
}
#[test]
fn e2e_e3700_pipeline_no_source_first_stage() {
let report = validate_fixture("bad/pipeline_no_source_first_stage.yaml");
assert!(
has_rule(&report, "E3700"),
"Pipeline without Source in first stage should trigger E3700, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_snapstart_python_runtime_supported() {
let report = validate_fixture("bad/lambda_snapstart_bad_runtime.yaml");
assert!(
!has_rule(&report, "E2530"),
"E2530 must not fire for a supported python runtime, got: {:?}",
report.diagnostics
);
assert!(has_rule(&report, "W2530"), "Expected W2530 for SnapStart without Version, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_e3053_awsvpc_port_mismatch() {
let report = validate_fixture("bad/ecs_awsvpc_port_mismatch.yaml");
assert!(
has_rule(&report, "E3053"),
"awsvpc HostPort != ContainerPort should trigger E3053, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3053_awsvpc_valid() {
let report = validate_fixture("good/ecs_awsvpc_valid.yaml");
assert!(
!has_rule(&report, "E3053"),
"Valid awsvpc should not trigger E3053, got: {:?}",
report.diagnostics.iter().filter(|d| d.rule_id == "E3053").collect::<Vec<_>>()
);
}
#[test]
fn e2e_e3512_resource_policy_no_statement() {
let report = validate_fixture("bad/resource_policy_no_statement.yaml");
assert!(
has_rule(&report, "E3512"),
"Resource policy without Statement should trigger E3512, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_w2533_lambda_zip_no_handler() {
let report = validate_fixture("bad/lambda_zip_no_handler.yaml");
assert!(
has_rule(&report, "W2533"),
"Zip deployment without Handler/Runtime should trigger W2533, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3050_iam_ref_with_path() {
let report = validate_fixture("bad/iam_ref_with_path.yaml");
assert!(
has_rule(&report, "E3050"),
"Ref to IAM role with non-default Path should trigger E3050, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3061_s3_tiering_bad_days() {
let report = validate_fixture("bad/s3_tiering_bad_days.yaml");
assert!(
has_rule(&report, "E3061"),
"S3 tiering days below minimum should trigger E3061, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e9014_invalid_event_patterns() {
let report = validate_fixture("bad/E9014_invalid_event_patterns.yaml");
let event_pattern_findings = report.diagnostics.iter().filter(|diagnostic| diagnostic.rule_id == "E9014").count();
assert_eq!(
event_pattern_findings, 34,
"Expected one E9014 per rejected pattern shape, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e9014_valid_and_unresolved_event_patterns() {
let report = validate_fixture("good/E9014_valid_event_patterns.yaml");
assert!(!has_rule(&report, "E9014"), "Expected no E9014, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_e3505_sqs_default_visibility_timeout() {
let report = validate_fixture("bad/E3505_sqs_default_visibility_timeout.yaml");
assert!(
has_rule(&report, "E3505"),
"A queue at the default visibility timeout must be compared to the function timeout, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3505_sqs_default_visibility_timeout_covers_function() {
let report = validate_fixture("good/E3505_sqs_default_visibility_timeout_covers_function.yaml");
assert!(!has_rule(&report, "E3505"), "Expected no E3505, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_e3505_conditional_service_defaults() {
let report = validate_fixture("bad/E3505_conditional_service_defaults.yaml");
let timeout_violations = report.diagnostics.iter().filter(|diagnostic| diagnostic.rule_id == "E3505").count();
assert_eq!(
timeout_violations, 8,
"Expected eight distinct conditional/default timeout violations, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3505_unresolved_dynamic_and_covering_timeouts() {
let report = validate_fixture("good/E3505_unresolved_dynamic_and_covering.yaml");
assert!(!has_rule(&report, "E3505"), "Expected no E3505, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_e3505_lambda_sqs_timeout() {
let report = validate_fixture("bad/lambda_sqs_timeout.yaml");
assert!(
has_rule(&report, "E3505"),
"SQS VisibilityTimeout < Lambda Timeout should trigger E3505, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3513_ecr_policy_no_statement() {
let report = validate_fixture("bad/ecr_policy_no_statement.yaml");
assert!(
has_rule(&report, "E3513"),
"ECR policy without Statement should trigger E3513, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_w2530_snapstart_no_version() {
let report = validate_fixture("bad/lambda_snapstart_no_version.yaml");
assert!(
has_rule(&report, "W2530"),
"SnapStart without Lambda::Version should trigger W2530, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_i3100_previous_gen_instance() {
let report = validate_fixture("bad/previous_gen_instance.yaml");
assert!(
has_rule(&report, "I3100"),
"Previous gen instance type should trigger I3100, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_input_size_limit() {
let big = vec![b' '; 11 * 1024 * 1024];
let result = SemanticModel::from_bytes(&big);
let err = match result {
Err(e) => e,
Ok(_) => panic!("Expected error for >10MB input"),
};
assert!(err.message.contains("exceeds maximum size"), "Expected size limit message, got: {}", err.message);
}
#[test]
fn e2e_e3051_ssm_document_invalid() {
let report = validate_fixture("bad/ssm_document_invalid.yaml");
assert!(
has_rule(&report, "E3051"),
"SSM Document without schemaVersion should trigger E3051, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_e3051_ssm_document_valid() {
let report = validate_fixture("good/ssm_document_valid.yaml");
assert!(
!has_rule(&report, "E3051"),
"Valid SSM Document should not trigger E3051, got: {:?}",
report.diagnostics.iter().filter(|d| d.rule_id == "E3051").collect::<Vec<_>>()
);
}
#[test]
fn e2e_e5001_module_with_tags() {
let report = validate_fixture("bad/module_with_tags.yaml");
assert!(has_rule(&report, "E5001"), "Module with Tags should trigger E5001, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_i2530_lambda_no_snapstart() {
let report = validate_fixture("bad/lambda_no_snapstart.yaml");
assert!(
has_rule(&report, "I2530"),
"Java21 Lambda without SnapStart should trigger I2530, got: {:?}",
report.diagnostics
);
}
#[test]
fn e2e_standard_detail_level() {
let report = validate_fixture("bad/generic.yaml");
let standard = report.to_report(DetailLevel::Standard);
assert!(!standard.diagnostics.is_empty());
assert_eq!(standard.metadata.counts.errors, report.metadata.counts.errors);
}
#[test]
fn e2e_output_level_warning() {
let config = ValidateConfig { severity_level: Severity::Warn, ..Default::default() };
let report = validate_with_config("bad/generic.yaml", config);
for d in &report.diagnostics {
assert!(
d.severity == Severity::Fatal || d.severity == Severity::Error || d.severity == Severity::Warn,
"Severity::Warn threshold should exclude info/debug, got {:?}",
d.severity
);
}
}
#[test]
fn e2e_output_level_error_only() {
let config = ValidateConfig { severity_level: Severity::Error, ..Default::default() };
let report = validate_with_config("bad/generic.yaml", config);
for d in &report.diagnostics {
assert!(
d.severity == Severity::Fatal || d.severity == Severity::Error,
"Severity::Error threshold should only include errors/fatal, got {:?}",
d.severity
);
}
}
#[test]
fn e2e_strict_mode_in_metadata() {
let config = ValidateConfig { strict: true, ..Default::default() };
let report = validate_with_config("good/minimal.yaml", config);
assert!(report.metadata.strict, "strict mode should be enabled");
}
#[test]
fn e2e_strict_mode_default_false() {
let report = validate_fixture("good/minimal.yaml");
assert!(!report.metadata.strict, "default mode should not be strict");
}
#[test]
fn e2e_include_range_filter() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig {
id_ranges: vec![IdRange { prefix: "E".into(), start: 3000, end: 3099 }],
..Default::default()
},
RuleFilterConfig::default(),
),
..Default::default()
};
let report = validate_with_config("bad/generic.yaml", config);
for d in &report.diagnostics {
let num: u32 = d.rule_id[1..].parse().unwrap_or(0);
assert!(
d.rule_id.starts_with('E') && (3000..=3099).contains(&num),
"Range filter E3000..E3099 should only include matching rules, got {}",
d.rule_id
);
}
}
#[test]
fn e2e_exclude_range_filter() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig {
id_ranges: vec![IdRange { prefix: "E".into(), start: 3000, end: 3099 }],
..Default::default()
},
),
..Default::default()
};
let report = validate_with_config("bad/generic.yaml", config);
for d in &report.diagnostics {
if d.rule_id.starts_with('E') {
let num: u32 = d.rule_id[1..].parse().unwrap_or(0);
assert!(
!(3000..=3099).contains(&num),
"Exclude range E3000..E3099 should remove matching rules, got {}",
d.rule_id
);
}
}
}
#[test]
fn e2e_exclude_category_skips_package() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { categories: vec!["best-practice".into(), "security".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_with_config("bad/security_issues.yaml", config);
for d in &report.diagnostics {
assert!(
d.category.as_deref() != Some("best-practice") && d.category.as_deref() != Some("security"),
"Excluded categories should not appear, got category={:?}",
d.category
);
}
}
#[test]
fn e2e_exclude_schema_category() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { categories: vec!["schema".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_with_config("bad/unknown_properties.yaml", config);
assert!(
!report.diagnostics.iter().any(|d| d.category.as_deref() == Some("schema")),
"Schema category should be excluded"
);
}
#[test]
fn e2e_regex_include_filter() {
let config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig { id_patterns: vec!["^E0\\d+$".into()], ..Default::default() },
RuleFilterConfig::default(),
),
..Default::default()
};
let report = validate_with_config("bad/generic.yaml", config);
for d in &report.diagnostics {
assert!(d.rule_id.starts_with("E0"), "Regex ^E0\\d+$ should only include E0xxx rules, got {}", d.rule_id);
}
}
#[test]
fn e2e_diagnostics_have_rule_description() {
let report = validate_fixture("bad/generic.yaml");
let with_desc = report.diagnostics.iter().filter(|d| d.rule_description.is_some()).count();
assert!(with_desc > 0, "Expected some diagnostics to have rule_description populated");
}
#[test]
fn e2e_no_general_category() {
let report = validate_fixture("bad/generic.yaml");
for d in &report.diagnostics {
assert_ne!(
d.category.as_deref(),
Some("general"),
"No diagnostic should have category 'general', got rule_id={} category={:?}",
d.rule_id,
d.category
);
}
}
#[test]
fn e2e_no_general_category_schema() {
let report = validate_fixture("bad/unknown_properties.yaml");
for d in &report.diagnostics {
assert_ne!(
d.category.as_deref(),
Some("general"),
"No diagnostic should have category 'general', got rule_id={} category={:?}",
d.rule_id,
d.category
);
}
}
#[test]
fn e2e_json_omits_null_optional_fields() {
let report = validate_fixture("good/minimal.yaml");
let json = serde_json::to_string_pretty(&report).unwrap();
assert!(!json.contains("\"suggested_fix\""), "suggested_fix should be omitted when None");
assert!(!json.contains("\"documentation_url\""), "documentation_url should be omitted when None");
assert!(!json.contains("\"condition_scenario\""), "condition_scenario should be omitted when None");
}
#[test]
fn e2e_report_metadata_has_output_level() {
let report = validate_fixture("good/minimal.yaml");
assert_eq!(report.metadata.severity_level, Severity::Info);
}
#[test]
fn e2e_custom_rule_source() {
let custom_rego = r#"
package custom_test
import rego.v1
violation contains make_diag("C0001", "WARN", name, "Custom rule triggered") if {
some name in object.keys(input.resources)
}
"#;
let config = EngineConfig {
custom_rules: vec![ExternalRuleSource { name: "custom/test.rego".into(), content: custom_rego.into() }],
..Default::default()
};
let engine = RegoEngine::new(config).unwrap();
let bytes = std::fs::read("../resources/templates/good/minimal.yaml").unwrap();
let report = validate_bytes(&engine, &SHARED_SV, &bytes, ValidateConfig::default()).unwrap();
assert!(has_rule(&report, "C0001"), "Custom rule C0001 should fire for resources, got: {:?}", report.diagnostics);
}
#[test]
fn e2e_w2511_iam_wildcard_all_types() {
let report = validate_fixture("bad/iam_wildcard_all_types.yaml");
let w2512 = report.diagnostics.iter().filter(|d| d.rule_id == "W2512").count();
assert!(w2512 >= 1, "Expected at least 1 W2512 (NotAction on User), got {}", w2512);
}
const GUARD_S3_VERSIONING: &str = r#"
rule s3_versioning_check {
AWS::S3::Bucket {
Properties.VersioningConfiguration.Status == "Enabled"
<<S3 bucket must have versioning enabled>>
}
}
"#;
const VERSIONED_AND_UNVERSIONED_BUCKETS: &[u8] = b"AWSTemplateFormatVersion: '2010-09-09'
Resources:
Versioned:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
Suspended:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Suspended
Unconfigured:
Type: AWS::S3::Bucket
";
#[test]
fn e2e_guard_rule_source() {
let config = EngineConfig {
guard_rules: vec![ExternalRuleSource {
name: "s3_versioning.guard".into(),
content: GUARD_S3_VERSIONING.into(),
}],
..Default::default()
};
let engine = RegoEngine::new(config).unwrap();
let report =
validate_bytes(&engine, &SHARED_SV, VERSIONED_AND_UNVERSIONED_BUCKETS, ValidateConfig::default()).unwrap();
let mut failing: Vec<(&str, &str)> = report
.diagnostics
.iter()
.filter(|d| d.rule_id == "s3_versioning_check")
.map(|d| (d.resource_logical_id().unwrap_or_default(), d.property_path.as_deref().unwrap_or_default()))
.collect();
failing.sort();
assert_eq!(
failing,
vec![("Suspended", "Properties.VersioningConfiguration.Status"), ("Unconfigured", "")],
"the versioned bucket passes; the other two fail at the deepest node the check reached - the \
mismatched value, and the resource itself when it has no Properties at all"
);
for d in report.diagnostics.iter().filter(|d| d.rule_id == "s3_versioning_check") {
assert_eq!(d.severity, Severity::Error, "Guard rules report Error severity");
assert_eq!(d.category.as_deref(), Some("guard:s3_versioning"), "category is guard:<file stem>");
assert_eq!(d.message, "S3 bucket must have versioning enabled");
assert!(d.location.is_some(), "a Guard finding on a resource carries a source span");
}
let rules = engine.list_rules();
let guard_rule = rules.iter().find(|r| r.id == "s3_versioning_check").expect("Guard rule appears in list_rules");
assert_eq!(guard_rule.category.as_deref(), Some("guard:s3_versioning"));
assert_eq!(guard_rule.description, "S3 bucket must have versioning enabled");
}
#[test]
fn e2e_guard_rule_pack() {
let guard_rules = resolve_guard_config(&["../guard-translator/tests/fixtures/pack".into()])
.expect("the pack directory resolves to its .guard files");
let engine = RegoEngine::new(EngineConfig { guard_rules, ..Default::default() }).expect("pack loads");
let mut guard_categories: Vec<String> =
engine.list_rules().into_iter().filter(|r| r.origin == RuleOrigin::Guard).filter_map(|r| r.category).collect();
guard_categories.sort();
guard_categories.dedup();
assert_eq!(
guard_categories,
vec!["guard:elb_https", "guard:s3_versioning"],
"every file in the pack contributes its own guard:<file stem> category"
);
}
#[test]
fn e2e_guard_rule_filtering() {
let config = EngineConfig {
guard_rules: vec![ExternalRuleSource {
name: "s3_versioning.guard".into(),
content: GUARD_S3_VERSIONING.into(),
}],
..Default::default()
};
let engine = RegoEngine::new(config).unwrap();
let unfiltered =
validate_bytes(&engine, &SHARED_SV, VERSIONED_AND_UNVERSIONED_BUCKETS, ValidateConfig::default()).unwrap();
assert!(
unfiltered.diagnostics.iter().any(|d| d.rule_id == "s3_versioning_check"),
"sanity: the Guard rule fires before the category filter is applied"
);
let validate_config = ValidateConfig {
filters: FilterConfig::new(
RuleFilterConfig::default(),
RuleFilterConfig { categories: vec!["guard:s3_versioning".into()], ..Default::default() },
),
..Default::default()
};
let report = validate_bytes(&engine, &SHARED_SV, VERSIONED_AND_UNVERSIONED_BUCKETS, validate_config).unwrap();
assert!(
!report.diagnostics.iter().any(|d| d.rule_id == "s3_versioning_check"),
"Guard rule should be filtered out by category exclusion"
);
}
#[test]
fn e6101_non_string_getatt_in_output() {
let report = validate_fixture("integration/getatt-types.yaml");
let e6101_count = report.diagnostics.iter().filter(|d| d.rule_id == "F6101").count();
assert!(e6101_count >= 5, "Expected at least 5 F6101 in getatt-types.yaml, got {}", e6101_count);
}
#[test]
fn e1015_invalid_getatt_attribute_type() {
let report = validate_fixture("integration/getatt-types.yaml");
assert!(!has_rule(&report, "E9003"), "E9003 is disabled - CloudFormation auto-converts non-string GetAtt values");
}
#[test]
fn e6101_rego_getatt_return_type_builtin() {
let report = validate_fixture("integration/getatt-types.yaml");
let e6101_outputs: Vec<_> =
report.diagnostics.iter().filter(|d| d.rule_id == "F6101").map(|d| d.message.clone()).collect();
assert!(
e6101_outputs.iter().any(|m| m.contains("InstanceCount") && m.contains("integer")),
"Expected F6101 for integer InstanceCount, got: {:?}",
e6101_outputs
);
}
#[test]
fn w2511_silent_on_invalid_policy_version() {
let report = validate_fixture("bad/resources/iam/iam_policy.yaml");
let w2511: Vec<_> = report.diagnostics.iter().filter(|d| d.rule_id == "W2511").collect();
assert!(w2511.is_empty(), "W2511 must not fire on an invalid Version value, got: {:?}", w2511);
}
#[test]
fn w2511_warns_on_2008_policy_version() {
let report = validate_fixture("bad/override/include.yaml");
assert!(has_rule(&report, "W2511"), "expected W2511 for a policy pinned to Version '2008-10-17'");
}