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
1,071 changes: 813 additions & 258 deletions scripts/audit_rule_categorization.py

Large diffs are not rendered by default.

1,900 changes: 1,583 additions & 317 deletions scripts/compare_cfnlint.py

Large diffs are not rendered by default.

38,090 changes: 17,826 additions & 20,264 deletions scripts/snapshots/report_cel_detailed.md

Large diffs are not rendered by default.

38,090 changes: 17,826 additions & 20,264 deletions scripts/snapshots/report_rego_detailed.md

Large diffs are not rendered by default.

146 changes: 67 additions & 79 deletions scripts/snapshots/rule_categorization_audit.md

Large diffs are not rendered by default.

1,424 changes: 1,424 additions & 0 deletions scripts/tests/test_audit_rule_categorization.py

Large diffs are not rendered by default.

1,755 changes: 1,755 additions & 0 deletions scripts/tests/test_compare_cfnlint.py

Large diffs are not rendered by default.

42 changes: 34 additions & 8 deletions src/cel-engine/src/rules/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ use template_model::resolver::RefKind;
use template_model::{PSEUDO_PARAMETERS, SemanticModel, is_custom_resource_type, is_known_region};
use validation_engine::make_resource_diagnostic;

/// Build a diagnostic property path pointing at the GetAtt resource-name element
/// (index 0). Appends `.Fn::GetAtt.0` unless sourcePath already ends with
/// `Fn::GetAtt`, in which case `.0` is sufficient.
fn getatt_target_path(source_path: &str) -> String {
if source_path.is_empty() {
"Fn::GetAtt.0".to_string()
} else if source_path.ends_with("Fn::GetAtt") {
format!("{}.0", source_path)
} else {
format!("{}.Fn::GetAtt.0", source_path)
}
}

/// Build a diagnostic property path pointing at the GetAtt attribute element
/// (index 1). Appends `.Fn::GetAtt.1` unless sourcePath already ends with
/// `Fn::GetAtt`, in which case `.1` is sufficient.
fn getatt_attr_path(source_path: &str) -> String {
if source_path.is_empty() {
"Fn::GetAtt.1".to_string()
} else if source_path.ends_with("Fn::GetAtt") {
format!("{}.1", source_path)
} else {
format!("{}.Fn::GetAtt.1", source_path)
}
}

pub fn register(reg: &mut NativeRuleRegistry) {
reg.add(Category::Intrinsic, eval_intrinsics);
reg.add(Category::Intrinsic, eval_intrinsic_params);
Expand Down Expand Up @@ -88,7 +114,7 @@ fn eval_intrinsics(ctx: &EvalContext) -> Vec<Diagnostic> {
&format!("Fn::GetAtt references non-existent resource '{}'", target),
m,
name,
"",
&getatt_target_path(source_path),
Some("Check that the GetAtt target resource exists in the template"),
));
} else if !attr.is_empty()
Expand All @@ -109,7 +135,7 @@ fn eval_intrinsics(ctx: &EvalContext) -> Vec<Diagnostic> {
&format!("'{}' is not one of {}", attr, render_str_list(valid_list)),
m,
name,
source_path,
&getatt_attr_path(source_path),
Some("Check the resource type documentation for valid GetAtt attributes"),
));
}
Expand Down Expand Up @@ -162,17 +188,17 @@ fn eval_intrinsics(ctx: &EvalContext) -> Vec<Diagnostic> {
}
}

if let Some(refs) = res.get("findInMapRefs").and_then(|r| r.as_array()) {
for map_ref in refs {
if let Some(map_name) = map_ref.as_str()
&& !m.mappings.contains_key(map_name)
{
if let Some(refs) = res.get("findInMapRefPaths").and_then(|r| r.as_array()) {
for entry in refs {
let map_name = entry.get("target").and_then(|target| target.as_str()).unwrap_or("");
let path = entry.get("path").and_then(|path| path.as_str()).unwrap_or("");
if !map_name.is_empty() && !m.mappings.contains_key(map_name) {
out.push(make_resource_diagnostic(
"F1012",
&format!("Fn::FindInMap references non-existent mapping '{}'", map_name),
m,
name,
"",
path,
None,
));
}
Expand Down
30 changes: 24 additions & 6 deletions src/cel-engine/src/rules/references.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{EvalContext, NativeRuleRegistry};
use diagnostics::Diagnostic;
use rules::Category;
use template_model::SemanticModel;
use template_model::consts::{
EDGE_KIND_GET_ATT, EDGE_KIND_REF, EDGE_KIND_SUB, FIELD_CONDITION_CONTEXT, FIELD_KIND, FIELD_OUTGOING_REFS,
FIELD_RESOURCES, FIELD_SOURCE_PATH, FIELD_TARGET, KEY_DEPENDS_ON, OUTPUT_PSEUDO_RESOURCE_PREFIX,
Expand All @@ -21,13 +22,27 @@ fn path_inside_fn_if_branch(path: &str) -> bool {
segments.windows(2).any(|w| w[0] == "Fn::If" && (w[1] == "1" || w[1] == "2"))
}

fn depends_on_path(model: &SemanticModel, resource_id: &str, dependency_index: usize) -> String {
let indexed_path = format!("{}.{}", KEY_DEPENDS_ON, dependency_index);
if model
.graph
.outgoing(resource_id)
.iter()
.any(|edge| matches!(&edge.kind, RefKind::DependsOn) && edge.source_path == indexed_path)
{
indexed_path
} else {
KEY_DEPENDS_ON.to_string()
}
}

fn eval_references(ctx: &EvalContext) -> Vec<Diagnostic> {
let mut out = Vec::new();
let m = ctx.model;
let input = ctx.input;

for (name, res) in &m.resources {
for dep in &res.depends_on {
for (dependency_index, dep) in res.depends_on.iter().enumerate() {
if !m.resources.contains_key(dep.as_str()) && !m.sam_implicit_resources.contains(dep.as_str()) {
// A dynamic reference cannot name a resource: DependsOn takes
// literal logical IDs only, so say that rather than implying a
Expand All @@ -37,13 +52,14 @@ fn eval_references(ctx: &EvalContext) -> Vec<Diagnostic> {
} else {
format!("DependsOn target '{}' does not exist as a resource", dep)
};
out.push(make_resource_diagnostic("E3005", &message, m, name, "", None));
let property_path = depends_on_path(m, name, dependency_index);
out.push(make_resource_diagnostic("E3005", &message, m, name, &property_path, None));
}
}
}

for (name, res) in &m.resources {
for dep in &res.depends_on {
for (dependency_index, dep) in res.depends_on.iter().enumerate() {
if let Some(dep_res) = m.resources.get(dep.as_str())
&& let Some(ref dep_cond) = dep_res.condition
{
Expand All @@ -56,12 +72,13 @@ fn eval_references(ctx: &EvalContext) -> Vec<Diagnostic> {
None => false, // unconditional resource depends on conditional
};
if !implies {
let property_path = depends_on_path(m, name, dependency_index);
out.push(make_resource_diagnostic(
"E3005",
&format!("'{}' will not exist when condition '{}' is False", dep, dep_cond),
m,
name,
KEY_DEPENDS_ON,
&property_path,
Some(&format!("Add a Condition to '{}' that implies '{}'", name, dep_cond)),
));
}
Expand All @@ -71,7 +88,7 @@ fn eval_references(ctx: &EvalContext) -> Vec<Diagnostic> {
}

for (name, res) in &m.resources {
for dep in &res.depends_on {
for (dep_idx, dep) in res.depends_on.iter().enumerate() {
for edge in m.graph.outgoing(name) {
if edge.target == *dep {
let kind_str = match &edge.kind {
Expand All @@ -80,12 +97,13 @@ fn eval_references(ctx: &EvalContext) -> Vec<Diagnostic> {
RefKind::Sub { var: _ } => EDGE_KIND_SUB,
_ => continue,
};
let anchor = depends_on_path(m, name, dep_idx);
out.push(make_resource_diagnostic(
"W3005",
&format!("'{}' dependency already enforced by a '{}' at '{}'", dep, kind_str, edge.source_path),
m,
name,
KEY_DEPENDS_ON,
&anchor,
Some("Remove the DependsOn entry"),
));
}
Expand Down
16 changes: 0 additions & 16 deletions src/cel-engine/src/rules/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,6 @@ fn eval_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
}
}

for name in m.resources_of_type("AWS::SQS::Queue") {
if let Some(serde_json::Value::Bool(true)) = resolve_concrete(m, name, "Properties.FifoQueue")
&& let Some(serde_json::Value::String(qname)) = resolve_concrete(m, name, "Properties.QueueName")
&& !qname.ends_with(".fifo")
{
out.push(make_resource_diagnostic(
"E2504",
&format!("FIFO queue name '{}' must end with '.fifo'", qname),
m,
name,
"Properties.QueueName",
None,
));
}
}

out
}

Expand Down
Loading