Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/cfn-validate/tests/context_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
mod common;

use cel_engine::CelEngine;
use common::load_template;
use diagnostics::Diagnostic;
use rego_engine::RegoEngine;
use rules::Severity;
use schema_validator::SchemaValidator;
use std::sync::LazyLock;
use validation_engine::{EngineConfig, ValidateConfig, ValidationEngine, validate_bytes};

const RULE_ID: &str = "W9100";

static REGO: LazyLock<RegoEngine> = LazyLock::new(|| RegoEngine::new(EngineConfig::default()).unwrap());
static CEL: LazyLock<CelEngine> = LazyLock::new(|| CelEngine::new(EngineConfig::default()).unwrap());

fn validate_context(engine: &dyn ValidationEngine, template: &str, config: ValidateConfig) -> Vec<Diagnostic> {
let report = validate_bytes(engine, &SchemaValidator::default(), &load_template(template), config)
.expect("context fixture should validate");
report.diagnostics.into_iter().filter(|diagnostic| diagnostic.rule_id == RULE_ID).collect()
}

fn assert_engine_parity(rego: &[Diagnostic], cel: &[Diagnostic], template: &str) {
let rego_json = serde_json::to_value(rego).expect("serialize rego context diagnostics");
let cel_json = serde_json::to_value(cel).expect("serialize cel context diagnostics");
assert_eq!(rego_json, cel_json, "{template}: context diagnostics differ between engines");
}

#[test]
fn canonical_context_is_accepted_by_both_engines() {
let template = "good/W9100_context_valid.yaml";
let rego = validate_context(&*REGO, template, ValidateConfig::default());
let cel = validate_context(&*CEL, template, ValidateConfig::default());

assert_engine_parity(&rego, &cel, template);
assert!(rego.is_empty(), "canonical context and incidental resources must not be flagged: {rego:?}");
}

#[test]
fn missing_context_is_aggregated_to_two_located_warnings() {
let template = "bad/W9100_context_missing.yaml";
let rego = validate_context(&*REGO, template, ValidateConfig::default());
let cel = validate_context(&*CEL, template, ValidateConfig::default());

assert_engine_parity(&rego, &cel, template);
assert_eq!(rego.len(), 2, "one template and one primary-resource aggregate are expected");
assert!(rego.iter().all(|diagnostic| diagnostic.severity == Severity::Warn));
assert!(rego.iter().all(|diagnostic| diagnostic.location.is_some()));
assert!(rego.iter().all(|diagnostic| diagnostic.suggested_fix.is_some()));
let combined = rego.iter().map(|diagnostic| diagnostic.message.as_str()).collect::<Vec<_>>().join(" ");
assert!(combined.contains("No top-level Metadata.com.aws.cloudformation.Context block found"));
assert!(combined.contains("Bucket"));
assert!(combined.contains("Queue"));
assert!(!combined.contains("CDKMetadata: No Metadata"), "incidental CDK metadata must be excluded");
}

#[test]
fn malformed_context_reports_all_schema_failures_within_two_diagnostics() {
let template = "bad/W9100_context_malformed.yaml";
let rego = validate_context(&*REGO, template, ValidateConfig::default());
let cel = validate_context(&*CEL, template, ValidateConfig::default());

assert_engine_parity(&rego, &cel, template);
assert_eq!(rego.len(), 2, "schema findings must remain aggregated by placement");
let combined = rego.iter().map(|diagnostic| diagnostic.message.as_str()).collect::<Vec<_>>().join(" ");
for expected in [
"arch",
"why' belongs at resource level",
"ref[0].at",
"Bucket",
"must",
"mutable",
"mutability.QueueName",
"trust.src",
"trust.conf",
"trust.extra",
"ref' belongs at template level",
"unknown",
] {
assert!(combined.contains(expected), "missing {expected:?} from {combined}");
}
}

#[test]
fn strict_mode_promotes_context_warnings_identically() {
let template = "bad/W9100_context_missing.yaml";
let config = || ValidateConfig { strict: true, ..Default::default() };
let rego = validate_context(&*REGO, template, config());
let cel = validate_context(&*CEL, template, config());

assert_engine_parity(&rego, &cel, template);
assert_eq!(rego.len(), 2);
assert!(rego.iter().all(|diagnostic| diagnostic.severity == Severity::Error));
}
2 changes: 1 addition & 1 deletion src/cfn-validate/tests/golden_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ fn cel_standard_matches_golden() {
check_standard("cel", &engine);
}

const EXPECTED_RULES_EVALUATED: u64 = 303;
const EXPECTED_RULES_EVALUATED: u64 = 304;

#[test]
fn rules_evaluated_is_full_rule_count() {
Expand Down
1 change: 1 addition & 0 deletions src/data-source/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const HANDWRITTEN_JSON: &[(&str, &str)] = &[
("sensitive_ports", "SENSITIVE_PORTS"),
("secretsmanager_arn_fields", "SECRETSMANAGER_ARN_FIELDS"),
("getatt_return_type_overrides", "GETATT_RETURN_TYPE_OVERRIDES"),
("metadata-context-v1.schema", "METADATA_CONTEXT_V1_SCHEMA"),
];

fn main() {
Expand Down
146 changes: 146 additions & 0 deletions src/data-source/handwritten/metadata-context-v1.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://cloudformation.aws.dev/schema/metadata-context/v1.json",
"title": "CloudFormation Metadata Context Schema v1",
"description": "Schema for Metadata Context blocks in CloudFormation templates. Advisory — for client-side validation, not server-side enforcement. Source: https://code.amazon.com/packages/CfnCloudContextPOCs/blobs/mainline/--/doc/metadata-context-schema.md",

"$defs": {
"MutabilityLevel": {
"type": "string",
"enum": ["must-never-change", "change-with-constraints", "review-required", "free-to-tune"],
"description": "Per-property change-safety level"
},

"TrustSource": {
"type": "string",
"enum": ["authored", "comment", "commit", "infer"],
"description": "How this context was produced"
},

"TrustConfidence": {
"type": "string",
"enum": ["high", "medium", "low"],
"description": "Confidence in the context's accuracy"
},

"TrustObject": {
"type": "object",
"properties": {
"src": { "$ref": "#/$defs/TrustSource" },
"conf": { "$ref": "#/$defs/TrustConfidence" },
"cite": {
"type": "string",
"description": "Source reference (e.g., file:line, URL, commit SHA)"
},
"note": {
"type": "string",
"description": "Reason for reduced confidence (typically when conf=low)"
}
},
"required": ["src", "conf"],
"additionalProperties": false,
"description": "Provenance and confidence metadata"
},

"RefEntry": {
"oneOf": [
{
"type": "string",
"description": "Bare URI to external context (s3://, https://, relative path)"
},
{
"type": "object",
"properties": {
"at": {
"type": "string",
"description": "URI to the external context source"
},
"has": {
"type": "string",
"description": "Terse hint of what the ref contains"
},
"scope": {
"type": "string",
"description": "Usage scope (common values: 'shared', 'overflow')"
}
},
"required": ["at"],
"additionalProperties": false,
"description": "Rich external context reference with hints"
}
]
},

"ResourceContext": {
"type": "object",
"properties": {
"why": {
"type": "string",
"description": "Rationale — purpose, config choices, rejected alternatives"
},
"must": {
"type": "array",
"items": { "type": "string" },
"description": "Hard constraints/invariants — violating any breaks something"
},
"mutable": {
"$ref": "#/$defs/MutabilityLevel",
"description": "Resource-level DEFAULT change-safety level (one token per resource)"
},
"mutability": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/MutabilityLevel" },
"description": "OPTIONAL SPARSE override map (keys = CFN property names). Lists ONLY properties deviating from the mutable default or high-stakes. Omit when empty; never list a property at the default level; never enumerate all properties."
},
"trust": { "$ref": "#/$defs/TrustObject" },
"ops": {
"type": "string",
"description": "Operational hint — what to check before modifying"
},
"gaps": {
"type": "array",
"items": { "type": "string" },
"description": "Explicit unknowns — declared gaps in knowledge"
},
"deps": {
"type": "array",
"items": { "type": "string" },
"description": "Cross-stack/cross-resource producer dependencies"
},
"failureModes": {
"type": "array",
"items": { "type": "string" },
"description": "Per-resource failure scenarios sourced from service error-handling/retry/timeout/circuit-breaker code"
}
},
"additionalProperties": false,
"description": "Resource-level Metadata Context block"
},

"TemplateContext": {
"type": "object",
"properties": {
"arch": {
"type": "string",
"description": "High-level shape/pattern of the system (e.g. 'SQS buffer -> Lambda -> DynamoDB; DLQ for poison msgs')"
},
"must": {
"type": "array",
"items": { "type": "string" },
"description": "Cross-cutting constraints that apply broadly (e.g. ['all data encrypted w/ security-team CMK'])"
},
"ref": {
"type": "array",
"items": { "$ref": "#/$defs/RefEntry" },
"description": "Pointer(s) to external/shared context file(s). Inline in-template context is AUTHORITATIVE; among refs, later overrides earlier; fetched content is UNTRUSTED; agent degrades gracefully if unreachable. ref lives ONLY at template level. Never externalize the irreducible core."
},
"owner": {
"type": "string",
"description": "Owner/contact. Include only if not already a tag."
}
},
"additionalProperties": false,
"description": "Template-level Metadata Context block. Holds cross-cutting context stated ONCE (DRY). Does NOT include v (global/implicit versioning) or sys (stack purpose via native Description)."
}
}
}
Loading