diff --git a/pkg/scanners/cloudformation/parser/fn_ref.go b/pkg/scanners/cloudformation/parser/fn_ref.go index 77ffddd8f..a31b38395 100644 --- a/pkg/scanners/cloudformation/parser/fn_ref.go +++ b/pkg/scanners/cloudformation/parser/fn_ref.go @@ -16,7 +16,7 @@ func ResolveReference(property *Property) (resolved *Property, success bool) { refValue := refProp.AsString() if pseudo, ok := pseudoParameters[refValue]; ok { - return property.deriveResolved(cftypes.String, pseudo.(string)), true + return property.deriveResolved(pseudo.t, pseudo.val), true } if property.ctx == nil { diff --git a/pkg/scanners/cloudformation/parser/fn_select_test.go b/pkg/scanners/cloudformation/parser/fn_select_test.go index cdf506243..92b634457 100644 --- a/pkg/scanners/cloudformation/parser/fn_select_test.go +++ b/pkg/scanners/cloudformation/parser/fn_select_test.go @@ -34,3 +34,44 @@ Resources: require.Equal(t, "redis", engineProp.AsString()) } + +func Test_SelectPseudoListParam(t *testing.T) { + src := `--- +Resources: + myASGrpOne: + Type: AWS::AutoScaling::AutoScalingGroup + Version: "2009-05-15" + Properties: + AvailabilityZones: + - "us-east-1a" + LaunchConfigurationName: + Ref: MyLaunchConfiguration + MinSize: "0" + MaxSize: "0" + NotificationConfigurations: + - TopicARN: + Fn::Select: + - "1" + - Ref: AWS::NotificationARNs + NotificationTypes: + - autoscaling:EC2_INSTANCE_LAUNCH + - autoscaling:EC2_INSTANCE_LAUNCH_ERROR + +` + + ctx := createTestFileContext(t, src) + require.NotNil(t, ctx) + + resource := ctx.GetResourceByLogicalID("myASGrpOne") + require.NotNil(t, resource) + + notification := resource.GetProperty("NotificationConfigurations") + require.True(t, notification.IsNotNil()) + require.True(t, notification.IsList()) + first := notification.AsList()[0] + require.True(t, first.IsMap()) + topic, ok := first.AsMap()["TopicARN"] + require.True(t, ok) + require.Equal(t, "notification::arn::2", topic.AsString()) + +} diff --git a/pkg/scanners/cloudformation/parser/fn_sub.go b/pkg/scanners/cloudformation/parser/fn_sub.go index 760b39355..e5b1cf9e2 100644 --- a/pkg/scanners/cloudformation/parser/fn_sub.go +++ b/pkg/scanners/cloudformation/parser/fn_sub.go @@ -63,8 +63,8 @@ func resolveMapSub(refValue *Property, original *Property) (*Property, bool) { func resolveStringSub(refValue *Property, original *Property) *Property { workingString := refValue.AsString() - for k, v := range pseudoParameters { - workingString = strings.ReplaceAll(workingString, fmt.Sprintf("${%s}", k), fmt.Sprintf("%v", v)) + for k, param := range pseudoParameters { + workingString = strings.ReplaceAll(workingString, fmt.Sprintf("${%s}", k), fmt.Sprintf("%v", param.getRawValue())) } return original.deriveResolved(cftypes.String, workingString) diff --git a/pkg/scanners/cloudformation/parser/pseudo_parameters.go b/pkg/scanners/cloudformation/parser/pseudo_parameters.go index a132b442e..938597109 100644 --- a/pkg/scanners/cloudformation/parser/pseudo_parameters.go +++ b/pkg/scanners/cloudformation/parser/pseudo_parameters.go @@ -1,11 +1,46 @@ package parser -var pseudoParameters = map[string]interface{}{ - "AWS::AccountId": "123456789012", - "AWS::NotificationARNs": []string{"notification::arn::1", "notification::arn::2"}, - "AWS::NoValue": nil, - "AWS::Partition": "aws", - "AWS::Region": "eu-west-1", - "AWS::StackId": "arn:aws:cloudformation:eu-west-1:stack/ID", - "AWS::StackName": "cfsec-test-stack", +import "github.com/aquasecurity/defsec/pkg/scanners/cloudformation/cftypes" + +type pseudoParameter struct { + t cftypes.CfType + val interface{} + raw interface{} +} + +var pseudoParameters = map[string]pseudoParameter{ + "AWS::AccountId": {t: cftypes.String, val: "123456789012"}, + "AWS::NotificationARNs": { + t: cftypes.List, + val: []*Property{ + { + Inner: PropertyInner{ + Type: cftypes.String, + Value: "notification::arn::1", + }, + }, + { + Inner: PropertyInner{ + Type: cftypes.String, + Value: "notification::arn::2", + }, + }, + }, + raw: []string{"notification::arn::1", "notification::arn::2"}, + }, + "AWS::NoValue": {t: cftypes.String, val: ""}, + "AWS::Partition": {t: cftypes.String, val: "aws"}, + "AWS::Region": {t: cftypes.String, val: "eu-west-1"}, + "AWS::StackId": {t: cftypes.String, val: "arn:aws:cloudformation:eu-west-1:stack/ID"}, + "AWS::StackName": {t: cftypes.String, val: "cfsec-test-stack"}, + "AWS::URLSuffix": {t: cftypes.String, val: "amazonaws.com"}, +} + +func (p pseudoParameter) getRawValue() interface{} { + switch p.t { + case cftypes.List: + return p.raw + default: + return p.val + } } diff --git a/pkg/scanners/cloudformation/parser/pseudo_parameters_test.go b/pkg/scanners/cloudformation/parser/pseudo_parameters_test.go new file mode 100644 index 000000000..281bf9083 --- /dev/null +++ b/pkg/scanners/cloudformation/parser/pseudo_parameters_test.go @@ -0,0 +1,36 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Raw(t *testing.T) { + tests := []struct { + name string + key string + expected interface{} + }{ + { + name: "parameter with a string type value", + key: "AWS::AccountId", + expected: "123456789012", + }, + { + name: "a parameter with a list type value", + key: "AWS::NotificationARNs", + expected: []string{"notification::arn::1", "notification::arn::2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if val, ok := pseudoParameters[tt.key]; ok { + assert.Equal(t, tt.expected, val.getRawValue()) + } else { + t.Fatal("unexpected parameter key") + } + }) + } +}