Skip to content

Commit 68f925a

Browse files
authored
Create new struct Entity and add logical ids to all diagnostics if available (#206)
1 parent fa35b77 commit 68f925a

60 files changed

Lines changed: 45654 additions & 28222 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/compare_benchmarks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,13 +319,14 @@ def _per_template_dir(engine, fmt, binding):
319319
def _diag_sort_key(d):
320320
"""Stable ordering for pairing diagnostics between binding outputs — identity
321321
that should be binding-invariant (rule id + source span + message)."""
322+
entity = d.get("entity") or {}
322323
return (
323324
d.get("ruleId") or "",
324325
d.get("startLine") or 0,
325326
d.get("startColumn") or 0,
326327
d.get("endLine") or 0,
327328
d.get("endColumn") or 0,
328-
d.get("resourceId") or "",
329+
entity.get("logicalId") or "",
329330
d.get("propertyPath") or "",
330331
d.get("message") or "",
331332
)

scripts/compare_cfnlint.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,17 @@ def load_engine_results():
380380
rule_id = d.get("ruleId", "")
381381
severity = d.get("severity", "")
382382
severity = _ENGINE_SEV_MAP.get(severity, severity)
383-
resource_id = d.get("resourceId", "")
383+
entity = d.get("entity") or {}
384+
resource_id = entity.get("logicalId", "") if entity.get("entityType") == "Resource" else ""
385+
resource_type = entity.get("resourceType", "")
384386
resource_path = d.get("propertyPath", "")
385-
if resource_path.startswith("Outputs/"):
387+
if rule_id == "F0000":
388+
# cfn-lint's E0000 parse-error records never carry a Path, so the
389+
# engine's richer identity (entity + duplicated-key path) would
390+
# defeat both matching passes; compare on the bare rule instead.
391+
resource_id = ""
392+
resource_path = ""
393+
elif resource_path.startswith("Outputs/"):
386394
resource_path = resource_path.replace("/", ".")
387395
resource_id = ""
388396
elif resource_id and resource_path.startswith("Outputs."):
@@ -394,7 +402,7 @@ def load_engine_results():
394402
"severity": severity,
395403
"message": d.get("message", ""),
396404
"resource_id": resource_id,
397-
"resource_type": d.get("resourceType", ""),
405+
"resource_type": resource_type,
398406
"resource_path": resource_path,
399407
"line": d.get("startLine", 0),
400408
"end_line": d.get("endLine", 0),

scripts/snapshots/report_cel_detailed.md

Lines changed: 170 additions & 109 deletions
Large diffs are not rendered by default.

scripts/snapshots/report_rego_detailed.md

Lines changed: 170 additions & 109 deletions
Large diffs are not rendered by default.

src/bindings-jvm/README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,27 @@ data class RuleFilterConfig(
125125
val idRanges: List<IdRange> = emptyList(), // numeric ranges, e.g. IdRange("E", 3000, 3099)
126126
val idPatterns: List<String> = emptyList(), // regex patterns matched against rule IDs
127127
val resourceIds: List<ResourceIdFilter> = emptyList(), // a rule (or every rule) on a logical resource ID
128+
val logicalIds: List<LogicalIdFilter> = emptyList(), // a rule (or every rule) on a named template entity
128129
val resourceTypes: List<ResourceTypeFilter> = emptyList(), // a rule (or every rule) on a resource type
129130
val services: List<ServiceFilter> = emptyList(), // a rule (or every rule) on a service, e.g. "AWS::AutoScaling"
130131
)
131132

132-
// resourceIds / resourceTypes / services each carry a nullable ruleId:
133+
// resourceIds / logicalIds / resourceTypes / services each carry a nullable ruleId:
133134
// set it to scope the filter to one rule, or leave it null for every rule on the target.
134135
data class ResourceIdFilter(val ruleId: String? = null, val resourceId: String)
136+
data class LogicalIdFilter(val ruleId: String? = null, val logicalId: String, val entityType: EntityType? = null)
135137
data class ResourceTypeFilter(val ruleId: String? = null, val resourceType: String)
136138
data class ServiceFilter(val ruleId: String? = null, val service: String)
137139
```
138140

139141
The `service` is matched verbatim against the `service-provider::service-name` prefix of the resource type — its first
140142
two `::`-delimited segments (e.g. `AWS::AutoScaling` in `AWS::AutoScaling::LaunchConfiguration`).
141143

144+
The `resourceIds` dimension matches only diagnostics attributed to a resource; `logicalIds` additionally matches
145+
diagnostics on parameters, outputs, mappings, conditions, and template rules (for resource diagnostics the two carry
146+
the same value). A non-null `entityType` scopes a `LogicalIdFilter` to entities of one type, so `MyThing` as a
147+
`PARAMETER` is matched without touching a same-named entity of another type.
148+
142149
### PseudoParameterOverrides
143150

144151
Override CloudFormation pseudo-parameters used during intrinsic function resolution. All fields optional — when `null`,
@@ -209,7 +216,7 @@ data class StandardReport(
209216
```
210217

211218
`DetailedReport` has the same structure but its diagnostics include additional fields: `documentationUrl`,
212-
`ruleDescription`, `phase` (`PARSE` | `SCHEMA` | `LINT`), `section`, and `context` (`ViolationContext` with
219+
`ruleDescription`, `phase` (`PARSE` | `SCHEMA` | `LINT`), and `context` (`ViolationContext` with
213220
`actualValue`, `expectedConstraint`, `resolutionSource`, etc.).
214221

215222
### StandardDiagnostic
@@ -220,9 +227,8 @@ data class StandardDiagnostic(
220227
val severity: Severity, // FATAL, ERROR, WARN, INFO, DEBUG
221228
val message: String,
222229
val source: RuleOrigin, // SCHEMA, CFN_LINT, ENGINE, CUSTOM, GUARD
223-
val resourceId: String?, // logical resource ID
224-
val resourceType: String?, // e.g. "AWS::S3::Bucket"
225-
val propertyPath: String?, // e.g. "Properties/BucketName"
230+
val entity: Entity?, // the named template entity the finding targets, if any
231+
val propertyPath: String?, // e.g. "Properties.BucketName", or section-absolute like "Parameters/MyParam/Type"
226232
val suggestedFix: String?,
227233
val category: String?,
228234
val startLine: UInt?,
@@ -232,4 +238,17 @@ data class StandardDiagnostic(
232238
val relatedResources: List<RelatedResource>?,
233239
val conditionScenario: Map<String, Boolean>?, // condition truth assignment that triggers this
234240
)
241+
242+
// The named template entity a diagnostic is attributed to. The entity type is the
243+
// singular form of the top-level template section the entity is declared in.
244+
data class Entity(
245+
val logicalId: String, // logical ID as declared in the template
246+
val entityType: EntityType,
247+
val resourceType: String? = null, // CloudFormation type, when the entity is a resource whose type is known
248+
)
249+
250+
enum class EntityType {
251+
RESOURCE, PARAMETER, OUTPUT, MAPPING, METADATA,
252+
RULE, CONDITION, TRANSFORM, FORMAT_VERSION, DESCRIPTION,
253+
}
235254
```

src/bindings-wasm/README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,27 @@ interface RuleFilterConfig {
105105
idRanges?: IdRange[]; // numeric ranges, e.g. { prefix: "E", start: 3000, end: 3099 }
106106
idPatterns?: string[]; // regex patterns matched against rule IDs
107107
resourceIds?: ResourceIdFilter[]; // a rule (or every rule) on a logical resource ID
108+
logicalIds?: LogicalIdFilter[]; // a rule (or every rule) on a named template entity
108109
resourceTypes?: ResourceTypeFilter[]; // a rule (or every rule) on a resource type
109110
services?: ServiceFilter[]; // a rule (or every rule) on a service, e.g. "AWS::AutoScaling"
110111
}
111112

112-
// resourceIds / resourceTypes / services each carry an optional ruleId:
113+
// resourceIds / logicalIds / resourceTypes / services each carry an optional ruleId:
113114
// set it to scope the filter to one rule, or omit it for every rule on the target.
114115
interface ResourceIdFilter { ruleId?: string; resourceId: string; }
116+
interface LogicalIdFilter { ruleId?: string; logicalId: string; entityType?: EntityType; }
115117
interface ResourceTypeFilter { ruleId?: string; resourceType: string; }
116118
interface ServiceFilter { ruleId?: string; service: string; }
117119
```
118120

119121
The `service` is matched verbatim against the `service-provider::service-name` prefix of the resource type — its first
120122
two `::`-delimited segments (e.g. `AWS::AutoScaling` in `AWS::AutoScaling::LaunchConfiguration`).
121123

124+
The `resourceIds` dimension matches only diagnostics attributed to a resource; `logicalIds` additionally matches
125+
diagnostics on parameters, outputs, mappings, conditions, and template rules (for resource diagnostics the two carry
126+
the same value). An optional `entityType` scopes a `LogicalIdFilter` to entities of one type, so `MyThing` as a
127+
`"Parameter"` is matched without touching a same-named entity of another type.
128+
122129
### PseudoParameterOverrides
123130

124131
Override CloudFormation pseudo-parameters used during intrinsic function resolution. All fields optional — when
@@ -200,7 +207,7 @@ interface StandardReport {
200207
```
201208

202209
`DetailedReport` has the same structure but its diagnostics include additional fields: `documentationUrl`,
203-
`ruleDescription`, `phase` (`PARSE` | `SCHEMA` | `LINT`), `section`, and `context` (`ViolationContext` with
210+
`ruleDescription`, `phase` (`PARSE` | `SCHEMA` | `LINT`), and `context` (`ViolationContext` with
204211
`actualValue`, `expectedConstraint`, `resolutionSource`, etc.).
205212

206213
### StandardDiagnostic
@@ -211,9 +218,8 @@ interface StandardDiagnostic {
211218
severity: Severity; // "FATAL" | "ERROR" | "WARN" | "INFO" | "DEBUG"
212219
message: string;
213220
source: RuleOrigin; // "SCHEMA" | "CFN_LINT" | "ENGINE" | "CUSTOM" | "GUARD"
214-
resourceId?: string; // logical resource ID
215-
resourceType?: string; // e.g. "AWS::S3::Bucket"
216-
propertyPath?: string; // e.g. "Properties/BucketName"
221+
entity?: Entity; // the named template entity the finding targets, if any
222+
propertyPath?: string; // e.g. "Properties.BucketName", or section-absolute like "Parameters/MyParam/Type"
217223
suggestedFix?: string;
218224
category?: string;
219225
startLine?: number;
@@ -223,4 +229,15 @@ interface StandardDiagnostic {
223229
relatedResources?: RelatedResource[];
224230
conditionScenario?: Record<string, boolean>; // condition truth assignment that triggers this diagnostic
225231
}
232+
233+
// The named template entity a diagnostic is attributed to. The entity type is the
234+
// singular form of the top-level template section the entity is declared in.
235+
interface Entity {
236+
logicalId: string; // logical ID as declared in the template
237+
entityType: EntityType;
238+
resourceType?: string; // CloudFormation type, when the entity is a resource whose type is known
239+
}
240+
241+
type EntityType = "Resource" | "Parameter" | "Output" | "Mapping" | "Metadata"
242+
| "Rule" | "Condition" | "Transform" | "FormatVersion" | "Description";
226243
```

src/bindings-wasm/ts/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ export type {
1919
RuleOrigin,
2020
IdRange,
2121
ResourceIdFilter,
22+
LogicalIdFilter,
2223
ResourceTypeFilter,
2324
ServiceFilter,
2425
RuleFilterConfig,
2526
RuleInfo,
2627
SourceSpan,
28+
Entity,
29+
EntityType,
2730
ResourceRef,
2831
RelatedResource,
2932
ViolationContext,

src/cel-engine/src/engine.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use log::info;
22
use std::collections::HashMap;
33
use std::sync::Arc;
44

5-
use diagnostics::{Diagnostic, PhaseMetric, ResourceRef, UNKNOWN_SPAN, phase_metric};
5+
use diagnostics::{Diagnostic, Entity, PhaseMetric, UNKNOWN_SPAN, phase_metric};
66
use guard_translator::{ensure_translatable, pack_name_from_path, parse_guard};
77
use rules::{RuleInfo, RuleMetadataEntry, RuleOrigin, Severity, build_rule_metadata_map, is_valid_custom_rule_id};
88
use template_model::SemanticModel;
@@ -251,14 +251,7 @@ fn emit_custom_diagnostic(
251251
rule_id: rule.rule_id.clone(),
252252
severity: rule.severity,
253253
message: msg.to_string(),
254-
resource: if rid.is_empty() {
255-
None
256-
} else {
257-
Some(ResourceRef {
258-
id: Some(rid.to_string()),
259-
resource_type: model.resources.get(rid).map(|r| r.resource_type.clone()),
260-
})
261-
},
254+
entity: Entity::resource(rid, model.resources.get(rid).map(|r| r.resource_type.clone())),
262255
property_path: rule.prop_path.clone(),
263256
suggested_fix: rule.suggested_fix.clone(),
264257
documentation_url: None,
@@ -268,7 +261,6 @@ fn emit_custom_diagnostic(
268261
condition_scenario: None,
269262
rule_description: None,
270263
phase: None,
271-
section: None,
272264
context: None,
273265
source: rule.source,
274266
});

src/cel-engine/src/rules/best_practices.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ fn eval_best_practices(ctx: &EvalContext) -> Vec<Diagnostic> {
360360
&format!("Parameter {} used as {}, therefore NoEcho should be True", target, prop),
361361
m,
362362
"",
363-
&format!("Parameters.{}", target),
363+
&format!("Parameters/{}", target),
364364
None,
365365
));
366366
}

src/cel-engine/src/rules/conditions.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,17 @@ fn eval_unreachable_if_branches(ctx: &EvalContext) -> Vec<Diagnostic> {
131131
}
132132

133133
// Also check Output values for unreachable Fn::If branches. An output is not
134-
// a resource, so anchor the diagnostic at the full "Outputs/<name>/Value"
135-
// path (matching how output locations are addressed elsewhere) rather than a
136-
// bare "Value" under the output's logical name.
134+
// a resource, so leave the resource slot empty and anchor the diagnostic at
135+
// the full "Outputs/<name>/Value" path (matching how output locations are
136+
// addressed elsewhere) rather than a bare "Value" under the output's logical
137+
// name.
137138
for (name, output) in &m.outputs {
138139
let base_assumptions: Vec<(String, bool)> = match &output.condition {
139140
Some(cond) => vec![(cond.clone(), true)],
140141
None => vec![],
141142
};
142143
let path_prefix = format!("Outputs/{}/Value", name);
143-
find_unreachable_branches(&mut out, m, name, &output.value, &path_prefix, &base_assumptions);
144+
find_unreachable_branches(&mut out, m, "", &output.value, &path_prefix, &base_assumptions);
144145
}
145146
out
146147
}

0 commit comments

Comments
 (0)