From 8bfa41516e4e0c92cc2f95fe8bf507fe4c4943b8 Mon Sep 17 00:00:00 2001 From: Satyaki Ghosh Date: Wed, 30 Sep 2026 00:44:16 +0000 Subject: [PATCH 1/2] Fix E1018 false positive on Fn::Split over Fn::GetStackOutput CloudFormation accepts Fn::GetStackOutput as the source of Fn::Split (confirmed by deploying a template in the CDK weak cross-stack string-list shape), but the published Fn::Split operand list predates the function and omits it, so the validator reported E1018 on every such template. Add Fn::GetStackOutput to the allowed Split sources in template-model so both engines inherit the fix, add a unit test and a good-corpus fixture in the deploy-verified shape, and regenerate the snapshots (engine parity verified: rego == cel == composite on all 718 templates). --- .../expected/validation_reports6.json | 118 ++++++++---- .../expected/validation_reports7.json | 170 ++++-------------- .../expected/validation_reports8.json | 131 ++++++++++++++ .../functions/split_get_stack_output.yaml | 49 +++++ .../src/intrinsic_arg_shapes.rs | 33 +++- 5 files changed, 329 insertions(+), 172 deletions(-) create mode 100644 src/resources/templates/good/functions/split_get_stack_output.yaml diff --git a/src/resources/expected/validation_reports6.json b/src/resources/expected/validation_reports6.json index 6bf9cbbc..d30f217d 100644 --- a/src/resources/expected/validation_reports6.json +++ b/src/resources/expected/validation_reports6.json @@ -828,6 +828,85 @@ } ] }, + "good/functions/split_get_stack_output.yaml": { + "filePath": "good/functions/split_get_stack_output.yaml", + "status": "OK", + "metadata": { + "resourcesScanned": 3, + "counts": { + "fatal": 0, + "errors": 0, + "warnings": 0, + "informational": 3, + "debug": 0 + }, + "suppressed": 0, + "strict": false, + "severityLevel": "DEBUG" + }, + "diagnostics": [ + { + "ruleId": "I9040", + "severity": "INFO", + "message": "Resource 'Bucket' of type 'AWS::S3::Bucket' supports Tags but none are configured", + "source": "ENGINE", + "entity": { + "logicalId": "Bucket", + "entityType": "Resource", + "resourceType": "AWS::S3::Bucket" + }, + "propertyPath": "Properties.Tags", + "suggestedFix": "Add Tags to improve resource organization and cost tracking", + "category": "Best Practice", + "startLine": 10, + "startColumn": 5, + "endLine": 10, + "endColumn": 15, + "ruleDescription": "Resource should have Tags", + "phase": "LINT" + }, + { + "ruleId": "I9040", + "severity": "INFO", + "message": "Resource 'AllHeaders' of type 'AWS::SSM::Parameter' supports Tags but none are configured", + "source": "ENGINE", + "entity": { + "logicalId": "AllHeaders", + "entityType": "Resource", + "resourceType": "AWS::SSM::Parameter" + }, + "propertyPath": "Properties.Tags", + "suggestedFix": "Add Tags to improve resource organization and cost tracking", + "category": "Best Practice", + "startLine": 26, + "startColumn": 5, + "endLine": 26, + "endColumn": 15, + "ruleDescription": "Resource should have Tags", + "phase": "LINT" + }, + { + "ruleId": "I9040", + "severity": "INFO", + "message": "Resource 'FirstHeader' of type 'AWS::SSM::Parameter' supports Tags but none are configured", + "source": "ENGINE", + "entity": { + "logicalId": "FirstHeader", + "entityType": "Resource", + "resourceType": "AWS::SSM::Parameter" + }, + "propertyPath": "Properties.Tags", + "suggestedFix": "Add Tags to improve resource organization and cost tracking", + "category": "Best Practice", + "startLine": 40, + "startColumn": 5, + "endLine": 40, + "endColumn": 15, + "ruleDescription": "Resource should have Tags", + "phase": "LINT" + } + ] + }, "good/functions/sub.yaml": { "filePath": "good/functions/sub.yaml", "status": "OK", @@ -16333,44 +16412,5 @@ "phase": "LINT" } ] - }, - "good/sam/function_dlq_valid.yaml": { - "filePath": "good/sam/function_dlq_valid.yaml", - "status": "OK", - "metadata": { - "resourcesScanned": 1, - "counts": { - "fatal": 0, - "errors": 0, - "warnings": 0, - "informational": 1, - "debug": 0 - }, - "suppressed": 0, - "strict": false, - "severityLevel": "DEBUG" - }, - "diagnostics": [ - { - "ruleId": "I9040", - "severity": "INFO", - "message": "Resource 'Fn' of type 'AWS::Serverless::Function' supports Tags but none are configured", - "source": "ENGINE", - "entity": { - "logicalId": "Fn", - "entityType": "Resource", - "resourceType": "AWS::Serverless::Function" - }, - "propertyPath": "Properties.Tags", - "suggestedFix": "Add Tags to improve resource organization and cost tracking", - "category": "Best Practice", - "startLine": 5, - "startColumn": 5, - "endLine": 5, - "endColumn": 15, - "ruleDescription": "Resource should have Tags", - "phase": "LINT" - } - ] } } diff --git a/src/resources/expected/validation_reports7.json b/src/resources/expected/validation_reports7.json index e01a396e..f6b6f24f 100644 --- a/src/resources/expected/validation_reports7.json +++ b/src/resources/expected/validation_reports7.json @@ -1,4 +1,43 @@ { + "good/sam/function_dlq_valid.yaml": { + "filePath": "good/sam/function_dlq_valid.yaml", + "status": "OK", + "metadata": { + "resourcesScanned": 1, + "counts": { + "fatal": 0, + "errors": 0, + "warnings": 0, + "informational": 1, + "debug": 0 + }, + "suppressed": 0, + "strict": false, + "severityLevel": "DEBUG" + }, + "diagnostics": [ + { + "ruleId": "I9040", + "severity": "INFO", + "message": "Resource 'Fn' of type 'AWS::Serverless::Function' supports Tags but none are configured", + "source": "ENGINE", + "entity": { + "logicalId": "Fn", + "entityType": "Resource", + "resourceType": "AWS::Serverless::Function" + }, + "propertyPath": "Properties.Tags", + "suggestedFix": "Add Tags to improve resource organization and cost tracking", + "category": "Best Practice", + "startLine": 5, + "startColumn": 5, + "endLine": 5, + "endColumn": 15, + "ruleDescription": "Resource should have Tags", + "phase": "LINT" + } + ] + }, "good/sam/function_image_valid.yaml": { "filePath": "good/sam/function_image_valid.yaml", "status": "OK", @@ -39176,136 +39215,5 @@ "phase": "LINT" } ] - }, - "quickstart/openshift_master.yaml": { - "filePath": "quickstart/openshift_master.yaml", - "status": "OK", - "metadata": { - "resourcesScanned": 2, - "counts": { - "fatal": 0, - "errors": 0, - "warnings": 0, - "informational": 6, - "debug": 0 - }, - "suppressed": 0, - "strict": false, - "severityLevel": "DEBUG" - }, - "diagnostics": [ - { - "ruleId": "I3011", - "severity": "INFO", - "message": "'DeletionPolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", - "source": "CFN_LINT", - "entity": { - "logicalId": "OpenShiftStack", - "entityType": "Resource", - "resourceType": "AWS::CloudFormation::Stack" - }, - "category": "Best Practice", - "startLine": 184, - "startColumn": 3, - "endLine": 184, - "endColumn": 17, - "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", - "phase": "LINT" - }, - { - "ruleId": "I3011", - "severity": "INFO", - "message": "'UpdateReplacePolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", - "source": "CFN_LINT", - "entity": { - "logicalId": "OpenShiftStack", - "entityType": "Resource", - "resourceType": "AWS::CloudFormation::Stack" - }, - "category": "Best Practice", - "startLine": 184, - "startColumn": 3, - "endLine": 184, - "endColumn": 17, - "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", - "phase": "LINT" - }, - { - "ruleId": "I3011", - "severity": "INFO", - "message": "'DeletionPolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", - "source": "CFN_LINT", - "entity": { - "logicalId": "VPCStack", - "entityType": "Resource", - "resourceType": "AWS::CloudFormation::Stack" - }, - "category": "Best Practice", - "startLine": 242, - "startColumn": 3, - "endLine": 242, - "endColumn": 11, - "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", - "phase": "LINT" - }, - { - "ruleId": "I3011", - "severity": "INFO", - "message": "'UpdateReplacePolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", - "source": "CFN_LINT", - "entity": { - "logicalId": "VPCStack", - "entityType": "Resource", - "resourceType": "AWS::CloudFormation::Stack" - }, - "category": "Best Practice", - "startLine": 242, - "startColumn": 3, - "endLine": 242, - "endColumn": 11, - "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", - "phase": "LINT" - }, - { - "ruleId": "I9040", - "severity": "INFO", - "message": "Resource 'OpenShiftStack' of type 'AWS::CloudFormation::Stack' supports Tags but none are configured", - "source": "ENGINE", - "entity": { - "logicalId": "OpenShiftStack", - "entityType": "Resource", - "resourceType": "AWS::CloudFormation::Stack" - }, - "propertyPath": "Properties.Tags", - "suggestedFix": "Add Tags to improve resource organization and cost tracking", - "category": "Best Practice", - "startLine": 185, - "startColumn": 5, - "endLine": 185, - "endColumn": 15, - "ruleDescription": "Resource should have Tags", - "phase": "LINT" - }, - { - "ruleId": "I9040", - "severity": "INFO", - "message": "Resource 'VPCStack' of type 'AWS::CloudFormation::Stack' supports Tags but none are configured", - "source": "ENGINE", - "entity": { - "logicalId": "VPCStack", - "entityType": "Resource", - "resourceType": "AWS::CloudFormation::Stack" - }, - "propertyPath": "Properties.Tags", - "suggestedFix": "Add Tags to improve resource organization and cost tracking", - "category": "Best Practice", - "startLine": 243, - "startColumn": 5, - "endLine": 243, - "endColumn": 15, - "ruleDescription": "Resource should have Tags", - "phase": "LINT" - } - ] } } diff --git a/src/resources/expected/validation_reports8.json b/src/resources/expected/validation_reports8.json index b3b9936e..ab54b9b5 100644 --- a/src/resources/expected/validation_reports8.json +++ b/src/resources/expected/validation_reports8.json @@ -1,4 +1,135 @@ { + "quickstart/openshift_master.yaml": { + "filePath": "quickstart/openshift_master.yaml", + "status": "OK", + "metadata": { + "resourcesScanned": 2, + "counts": { + "fatal": 0, + "errors": 0, + "warnings": 0, + "informational": 6, + "debug": 0 + }, + "suppressed": 0, + "strict": false, + "severityLevel": "DEBUG" + }, + "diagnostics": [ + { + "ruleId": "I3011", + "severity": "INFO", + "message": "'DeletionPolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", + "source": "CFN_LINT", + "entity": { + "logicalId": "OpenShiftStack", + "entityType": "Resource", + "resourceType": "AWS::CloudFormation::Stack" + }, + "category": "Best Practice", + "startLine": 184, + "startColumn": 3, + "endLine": 184, + "endColumn": 17, + "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", + "phase": "LINT" + }, + { + "ruleId": "I3011", + "severity": "INFO", + "message": "'UpdateReplacePolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", + "source": "CFN_LINT", + "entity": { + "logicalId": "OpenShiftStack", + "entityType": "Resource", + "resourceType": "AWS::CloudFormation::Stack" + }, + "category": "Best Practice", + "startLine": 184, + "startColumn": 3, + "endLine": 184, + "endColumn": 17, + "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", + "phase": "LINT" + }, + { + "ruleId": "I3011", + "severity": "INFO", + "message": "'DeletionPolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", + "source": "CFN_LINT", + "entity": { + "logicalId": "VPCStack", + "entityType": "Resource", + "resourceType": "AWS::CloudFormation::Stack" + }, + "category": "Best Practice", + "startLine": 242, + "startColumn": 3, + "endLine": 242, + "endColumn": 11, + "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", + "phase": "LINT" + }, + { + "ruleId": "I3011", + "severity": "INFO", + "message": "'UpdateReplacePolicy' is a required property (The default action when replacing/removing a resource is to delete it. Set explicit values for stateful resource)", + "source": "CFN_LINT", + "entity": { + "logicalId": "VPCStack", + "entityType": "Resource", + "resourceType": "AWS::CloudFormation::Stack" + }, + "category": "Best Practice", + "startLine": 242, + "startColumn": 3, + "endLine": 242, + "endColumn": 11, + "ruleDescription": "Check stateful resources have a set UpdateReplacePolicy/DeletionPolicy", + "phase": "LINT" + }, + { + "ruleId": "I9040", + "severity": "INFO", + "message": "Resource 'OpenShiftStack' of type 'AWS::CloudFormation::Stack' supports Tags but none are configured", + "source": "ENGINE", + "entity": { + "logicalId": "OpenShiftStack", + "entityType": "Resource", + "resourceType": "AWS::CloudFormation::Stack" + }, + "propertyPath": "Properties.Tags", + "suggestedFix": "Add Tags to improve resource organization and cost tracking", + "category": "Best Practice", + "startLine": 185, + "startColumn": 5, + "endLine": 185, + "endColumn": 15, + "ruleDescription": "Resource should have Tags", + "phase": "LINT" + }, + { + "ruleId": "I9040", + "severity": "INFO", + "message": "Resource 'VPCStack' of type 'AWS::CloudFormation::Stack' supports Tags but none are configured", + "source": "ENGINE", + "entity": { + "logicalId": "VPCStack", + "entityType": "Resource", + "resourceType": "AWS::CloudFormation::Stack" + }, + "propertyPath": "Properties.Tags", + "suggestedFix": "Add Tags to improve resource organization and cost tracking", + "category": "Best Practice", + "startLine": 243, + "startColumn": 5, + "endLine": 243, + "endColumn": 15, + "ruleDescription": "Resource should have Tags", + "phase": "LINT" + } + ] + }, "quickstart/test.yaml": { "filePath": "quickstart/test.yaml", "status": "OK", diff --git a/src/resources/templates/good/functions/split_get_stack_output.yaml b/src/resources/templates/good/functions/split_get_stack_output.yaml new file mode 100644 index 00000000..4348bbb0 --- /dev/null +++ b/src/resources/templates/good/functions/split_get_stack_output.yaml @@ -0,0 +1,49 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: >- + Fn::Split over Fn::GetStackOutput. This is the shape a consumer stack takes + when a string-list output of another stack is read as a weak cross-stack + reference; CloudFormation deploys it and resolves the list. +Resources: + # Split result used directly as a list-typed property + Bucket: + Type: AWS::S3::Bucket + Properties: + CorsConfiguration: + CorsRules: + - AllowedMethods: + - GET + AllowedOrigins: + - "*" + ExposedHeaders: + Fn::Split: + - "||" + - Fn::GetStackOutput: + StackName: producer-stack + OutputName: ExportedHeaders + # Split result re-joined into a comma-delimited string + AllHeaders: + Type: AWS::SSM::Parameter + Properties: + Type: StringList + Value: + Fn::Join: + - "," + - Fn::Split: + - "||" + - Fn::GetStackOutput: + StackName: producer-stack + Region: us-east-1 + OutputName: ExportedHeaders + # Single element selected out of the split result + FirstHeader: + Type: AWS::SSM::Parameter + Properties: + Type: String + Value: + Fn::Select: + - 0 + - Fn::Split: + - "||" + - Fn::GetStackOutput: + StackName: producer-stack + OutputName: ExportedHeaders diff --git a/src/template-model/src/intrinsic_arg_shapes.rs b/src/template-model/src/intrinsic_arg_shapes.rs index 20efd8e1..8cd61321 100644 --- a/src/template-model/src/intrinsic_arg_shapes.rs +++ b/src/template-model/src/intrinsic_arg_shapes.rs @@ -17,8 +17,22 @@ const SELECT_SOURCE_FUNCTIONS: &[&str] = &[FN_FIND_IN_MAP, FN_GET_ATT, FN_GET_AZ const SELECT_INDEX_FUNCTIONS: &[&str] = &[FN_REF, FN_FIND_IN_MAP]; const SELECT_INDEX_FUNCTIONS_EXT: &[&str] = &[FN_REF, FN_FIND_IN_MAP, FN_LENGTH]; -const SPLIT_SOURCE_FUNCTIONS: &[&str] = - &[FN_BASE64, FN_FIND_IN_MAP, FN_GET_ATT, FN_GET_AZS, FN_IF, FN_IMPORT_VALUE, FN_JOIN, FN_SELECT, FN_SUB, FN_REF]; +// Fn::GetStackOutput is accepted by CloudFormation as a Split source (confirmed +// by deploying a template that splits one) even though the published Fn::Split +// operand list predates that function and still omits it. +const SPLIT_SOURCE_FUNCTIONS: &[&str] = &[ + FN_BASE64, + FN_FIND_IN_MAP, + FN_GET_ATT, + FN_GET_AZS, + FN_GET_STACK_OUTPUT, + FN_IF, + FN_IMPORT_VALUE, + FN_JOIN, + FN_SELECT, + FN_SUB, + FN_REF, +]; const SUB_VARIABLE_FUNCTIONS: &[&str] = &[ FN_BASE64, @@ -302,6 +316,21 @@ mod tests { assert!(validate_intrinsic_arg_shapes(&arena, &[]).is_empty()); } + #[test] + fn split_source_get_stack_output_is_allowed() { + let mut arena = Arena::new(); + let delimiter = alloc_string(&mut arena, "||"); + let stack_name = alloc_string(&mut arena, "producer-stack"); + let output_name = alloc_string(&mut arena, "ExportedList"); + let source = alloc_intrinsic( + &mut arena, + IntrinsicFn::GetStackOutput(vec![("StackName".into(), stack_name), ("OutputName".into(), output_name)]), + ); + alloc_intrinsic(&mut arena, IntrinsicFn::Split(delimiter, source)); + + assert!(validate_intrinsic_arg_shapes(&arena, &[]).is_empty()); + } + #[test] fn sub_variable_cidr_fires() { let mut arena = Arena::new(); From 3b101a4b62e4ba22ad6bdd91e1be6164887042bf Mon Sep 17 00:00:00 2001 From: Satyaki Ghosh Date: Wed, 30 Sep 2026 15:28:07 +0000 Subject: [PATCH 2/2] remove comments --- src/resources/expected/validation_reports6.json | 12 ++++++------ .../good/functions/split_get_stack_output.yaml | 8 +------- src/template-model/src/intrinsic_arg_shapes.rs | 3 --- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/resources/expected/validation_reports6.json b/src/resources/expected/validation_reports6.json index d30f217d..aeb29d9a 100644 --- a/src/resources/expected/validation_reports6.json +++ b/src/resources/expected/validation_reports6.json @@ -858,9 +858,9 @@ "propertyPath": "Properties.Tags", "suggestedFix": "Add Tags to improve resource organization and cost tracking", "category": "Best Practice", - "startLine": 10, + "startLine": 6, "startColumn": 5, - "endLine": 10, + "endLine": 6, "endColumn": 15, "ruleDescription": "Resource should have Tags", "phase": "LINT" @@ -878,9 +878,9 @@ "propertyPath": "Properties.Tags", "suggestedFix": "Add Tags to improve resource organization and cost tracking", "category": "Best Practice", - "startLine": 26, + "startLine": 21, "startColumn": 5, - "endLine": 26, + "endLine": 21, "endColumn": 15, "ruleDescription": "Resource should have Tags", "phase": "LINT" @@ -898,9 +898,9 @@ "propertyPath": "Properties.Tags", "suggestedFix": "Add Tags to improve resource organization and cost tracking", "category": "Best Practice", - "startLine": 40, + "startLine": 34, "startColumn": 5, - "endLine": 40, + "endLine": 34, "endColumn": 15, "ruleDescription": "Resource should have Tags", "phase": "LINT" diff --git a/src/resources/templates/good/functions/split_get_stack_output.yaml b/src/resources/templates/good/functions/split_get_stack_output.yaml index 4348bbb0..89590c49 100644 --- a/src/resources/templates/good/functions/split_get_stack_output.yaml +++ b/src/resources/templates/good/functions/split_get_stack_output.yaml @@ -1,10 +1,6 @@ AWSTemplateFormatVersion: "2010-09-09" -Description: >- - Fn::Split over Fn::GetStackOutput. This is the shape a consumer stack takes - when a string-list output of another stack is read as a weak cross-stack - reference; CloudFormation deploys it and resolves the list. +Description: Fn::Split over Fn::GetStackOutput, the shape of a weak string-list cross-stack reference Resources: - # Split result used directly as a list-typed property Bucket: Type: AWS::S3::Bucket Properties: @@ -20,7 +16,6 @@ Resources: - Fn::GetStackOutput: StackName: producer-stack OutputName: ExportedHeaders - # Split result re-joined into a comma-delimited string AllHeaders: Type: AWS::SSM::Parameter Properties: @@ -34,7 +29,6 @@ Resources: StackName: producer-stack Region: us-east-1 OutputName: ExportedHeaders - # Single element selected out of the split result FirstHeader: Type: AWS::SSM::Parameter Properties: diff --git a/src/template-model/src/intrinsic_arg_shapes.rs b/src/template-model/src/intrinsic_arg_shapes.rs index 8cd61321..c1e292d9 100644 --- a/src/template-model/src/intrinsic_arg_shapes.rs +++ b/src/template-model/src/intrinsic_arg_shapes.rs @@ -17,9 +17,6 @@ const SELECT_SOURCE_FUNCTIONS: &[&str] = &[FN_FIND_IN_MAP, FN_GET_ATT, FN_GET_AZ const SELECT_INDEX_FUNCTIONS: &[&str] = &[FN_REF, FN_FIND_IN_MAP]; const SELECT_INDEX_FUNCTIONS_EXT: &[&str] = &[FN_REF, FN_FIND_IN_MAP, FN_LENGTH]; -// Fn::GetStackOutput is accepted by CloudFormation as a Split source (confirmed -// by deploying a template that splits one) even though the published Fn::Split -// operand list predates that function and still omits it. const SPLIT_SOURCE_FUNCTIONS: &[&str] = &[ FN_BASE64, FN_FIND_IN_MAP,