Skip to content

Commit 208141d

Browse files
committed
refactor(misconf): propagate metadata through cloudformation Property chain
1 parent b1626a3 commit 208141d

5 files changed

Lines changed: 172 additions & 174 deletions

File tree

pkg/iac/adapters/cloudformation/aws/athena/athena_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Resources:
2525
WorkGroupConfiguration:
2626
EnforceWorkGroupConfiguration: true
2727
ResultConfiguration:
28-
EncryptionOption: SSE_KMS
28+
EncryptionConfiguration:
29+
EncryptionOption: SSE_KMS
2930
`,
3031
expected: athena.Athena{
3132
Workgroups: []athena.Workgroup{

pkg/iac/scanners/cloudformation/parser/property.go

Lines changed: 16 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -226,64 +226,33 @@ func (p *Property) resolveValue() (*Property, bool) {
226226
}
227227

228228
func (p *Property) GetStringProperty(path string, defaultValue ...string) iacTypes.StringValue {
229-
defVal := ""
230-
if len(defaultValue) > 0 {
231-
defVal = defaultValue[0]
232-
}
233-
234229
if p.IsUnresolved() {
235230
return iacTypes.StringUnresolvable(p.Metadata())
236231
}
237-
238-
prop := p.GetProperty(path)
239-
if prop.IsNotString() {
240-
return p.StringDefault(defVal)
241-
}
242-
return prop.AsStringValue()
232+
return p.GetProperty(path).AsStringValue(firstOrZero(defaultValue))
243233
}
244234

245235
func (p *Property) StringDefault(defaultValue string) iacTypes.StringValue {
246236
return iacTypes.StringDefault(defaultValue, p.Metadata())
247237
}
248238

249239
func (p *Property) GetBoolProperty(path string, defaultValue ...bool) iacTypes.BoolValue {
250-
defVal := false
251-
if len(defaultValue) > 0 {
252-
defVal = defaultValue[0]
253-
}
254-
255240
if p.IsUnresolved() {
256241
return iacTypes.BoolUnresolvable(p.Metadata())
257242
}
258243

259244
prop := p.GetProperty(path)
260-
261245
if prop.isFunction() {
262246
prop, _ = prop.resolveValue()
263247
}
264-
265-
if prop.IsNotBool() {
266-
return p.inferBool(prop, defVal)
267-
}
268-
return prop.AsBoolValue()
248+
return prop.AsBoolValue(firstOrZero(defaultValue))
269249
}
270250

271251
func (p *Property) GetIntProperty(path string, defaultValue ...int) iacTypes.IntValue {
272-
defVal := 0
273-
if len(defaultValue) > 0 {
274-
defVal = defaultValue[0]
275-
}
276-
277252
if p.IsUnresolved() {
278253
return iacTypes.IntUnresolvable(p.Metadata())
279254
}
280-
281-
prop := p.GetProperty(path)
282-
283-
if prop.IsNotInt() {
284-
return p.IntDefault(defVal)
285-
}
286-
return prop.AsIntValue()
255+
return p.GetProperty(path).AsIntValue(firstOrZero(defaultValue))
287256
}
288257

289258
func (p *Property) BoolDefault(defaultValue bool) iacTypes.BoolValue {
@@ -294,6 +263,10 @@ func (p *Property) IntDefault(defaultValue int) iacTypes.IntValue {
294263
return iacTypes.IntDefault(defaultValue, p.Metadata())
295264
}
296265

266+
func (p *Property) nullProperty() *Property {
267+
return &Property{rng: p.rng, parentRange: p.parentRange, logicalId: p.logicalId}
268+
}
269+
297270
func (p *Property) GetProperty(path string) *Property {
298271
pathParts := strings.Split(path, ".")
299272

@@ -305,29 +278,23 @@ func (p *Property) GetProperty(path string) *Property {
305278
}
306279

307280
if property.IsNotMap() {
308-
return nil
281+
return property.nullProperty()
309282
}
310283

311-
for n, p := range property.AsMap() {
312-
if n == first {
313-
property = p
314-
break
315-
}
284+
child, ok := property.AsMap()[first]
285+
if !ok {
286+
return property.nullProperty()
316287
}
317288

318-
if len(pathParts) == 1 || property == nil {
319-
return property
320-
}
321-
322-
if nestedProperty := property.GetProperty(strings.Join(pathParts[1:], ".")); nestedProperty != nil {
323-
if nestedProperty.isFunction() {
324-
resolved, _ := nestedProperty.resolveValue()
289+
if len(pathParts) == 1 {
290+
if child.isFunction() {
291+
resolved, _ := child.resolveValue()
325292
return resolved
326293
}
327-
return nestedProperty
294+
return child
328295
}
329296

330-
return &Property{}
297+
return child.GetProperty(strings.Join(pathParts[1:], "."))
331298
}
332299

333300
func (p *Property) deriveResolved(propType cftypes.CfType, propValue any) *Property {
@@ -341,39 +308,6 @@ func (p *Property) ParentRange() iacTypes.Range {
341308
return p.parentRange
342309
}
343310

344-
func (p *Property) inferBool(prop *Property, defaultValue bool) iacTypes.BoolValue {
345-
if prop.IsString() {
346-
if prop.EqualTo("true", IgnoreCase) {
347-
return iacTypes.Bool(true, prop.Metadata())
348-
}
349-
if prop.EqualTo("yes", IgnoreCase) {
350-
return iacTypes.Bool(true, prop.Metadata())
351-
}
352-
if prop.EqualTo("1", IgnoreCase) {
353-
return iacTypes.Bool(true, prop.Metadata())
354-
}
355-
if prop.EqualTo("false", IgnoreCase) {
356-
return iacTypes.Bool(false, prop.Metadata())
357-
}
358-
if prop.EqualTo("no", IgnoreCase) {
359-
return iacTypes.Bool(false, prop.Metadata())
360-
}
361-
if prop.EqualTo("0", IgnoreCase) {
362-
return iacTypes.Bool(false, prop.Metadata())
363-
}
364-
}
365-
366-
if prop.IsInt() {
367-
if prop.EqualTo(0) {
368-
return iacTypes.Bool(false, prop.Metadata())
369-
}
370-
if prop.EqualTo(1) {
371-
return iacTypes.Bool(true, prop.Metadata())
372-
}
373-
}
374-
375-
return p.BoolDefault(defaultValue)
376-
}
377311

378312
func (p *Property) String() string {
379313
r := ""

pkg/iac/scanners/cloudformation/parser/property_helpers.go

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import (
88
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
99
)
1010

11+
func firstOrZero[T any](values []T) T {
12+
if len(values) > 0 {
13+
return values[0]
14+
}
15+
var zero T
16+
return zero
17+
}
18+
1119
func (p *Property) IsNil() bool {
1220
return p == nil || p.Value == nil
1321
}
@@ -92,10 +100,16 @@ func (p *Property) AsString() string {
92100
return p.Value.(string)
93101
}
94102

95-
func (p *Property) AsStringValue() iacTypes.StringValue {
96-
if p.unresolved {
103+
func (p *Property) AsStringValue(defaultValue ...string) iacTypes.StringValue {
104+
if p.IsNil() {
105+
return p.StringDefault(firstOrZero(defaultValue))
106+
}
107+
if p.IsUnresolved() {
97108
return iacTypes.StringUnresolvable(p.Metadata())
98109
}
110+
if !p.IsString() {
111+
return p.StringDefault(firstOrZero(defaultValue))
112+
}
99113
return iacTypes.StringExplicit(p.AsString(), p.Metadata())
100114
}
101115

@@ -116,28 +130,47 @@ func (p *Property) AsInt() int {
116130
return p.Value.(int)
117131
}
118132

119-
func (p *Property) AsIntValue() iacTypes.IntValue {
120-
if p.unresolved {
133+
func (p *Property) AsIntValue(defaultValue ...int) iacTypes.IntValue {
134+
if p.IsNil() {
135+
return p.IntDefault(firstOrZero(defaultValue))
136+
}
137+
if p.IsUnresolved() {
121138
return iacTypes.IntUnresolvable(p.Metadata())
122139
}
140+
if !p.IsInt() {
141+
return p.IntDefault(firstOrZero(defaultValue))
142+
}
123143
return iacTypes.IntExplicit(p.AsInt(), p.Metadata())
124144
}
125145

146+
var boolTrueStrings = map[string]struct{}{
147+
"true": {}, "yes": {}, "1": {},
148+
}
149+
126150
func (p *Property) AsBool() bool {
127151
if p.isFunction() {
128152
if prop, success := p.resolveValue(); success && prop != p {
129153
return prop.AsBool()
130154
}
131155
return false
132156
}
133-
if !p.IsBool() {
134-
return false
157+
switch p.Type {
158+
case cftypes.Bool:
159+
return p.Value.(bool)
160+
case cftypes.String:
161+
_, ok := boolTrueStrings[strings.ToLower(p.AsString())]
162+
return ok
163+
case cftypes.Int:
164+
return p.AsInt() != 0
135165
}
136-
return p.Value.(bool)
166+
return false
137167
}
138168

139-
func (p *Property) AsBoolValue() iacTypes.BoolValue {
140-
if p.unresolved {
169+
func (p *Property) AsBoolValue(defaultValue ...bool) iacTypes.BoolValue {
170+
if p.IsNil() {
171+
return p.BoolDefault(firstOrZero(defaultValue))
172+
}
173+
if p.IsUnresolved() {
141174
return iacTypes.BoolUnresolvable(p.Metadata())
142175
}
143176
return iacTypes.Bool(p.AsBool(), p.Metadata())
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package parser
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
11+
func mustParseYAML(t *testing.T, source string) FileContexts {
12+
t.Helper()
13+
files, err := parseFile(t, source, "cf.yaml")
14+
require.NoError(t, err)
15+
return files
16+
}
17+
18+
func Test_Property_AsBoolValue_StringAndIntInference(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
value string
22+
expected bool
23+
}{
24+
{name: "bool true", value: "true", expected: true},
25+
{name: "bool false", value: "false", expected: false},
26+
{name: "string true", value: `"true"`, expected: true},
27+
{name: "string yes", value: `"yes"`, expected: true},
28+
{name: "string 1", value: `"1"`, expected: true},
29+
{name: "string TRUE uppercase", value: `"TRUE"`, expected: true},
30+
{name: "string false", value: `"false"`, expected: false},
31+
{name: "string no", value: `"no"`, expected: false},
32+
{name: "string 0", value: `"0"`, expected: false},
33+
{name: "int 1", value: "1", expected: true},
34+
{name: "int 0", value: "0", expected: false},
35+
}
36+
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
source := `---
40+
Resources:
41+
MyResource:
42+
Type: AWS::S3::Bucket
43+
Properties:
44+
Flag: ` + tt.value
45+
46+
prop := mustParseYAML(t, source)[0].Resources["MyResource"].GetProperty("Flag")
47+
assert.Equal(t, tt.expected, prop.AsBoolValue().IsTrue())
48+
})
49+
}
50+
}
51+
52+
func Test_Resource_GetBoolProperty_MissingKeyUsesDefault(t *testing.T) {
53+
source := `---
54+
Resources:
55+
MyResource:
56+
Type: AWS::S3::Bucket
57+
Properties:
58+
Other: value`
59+
60+
resource := mustParseYAML(t, source)[0].Resources["MyResource"]
61+
62+
assert.False(t, resource.GetBoolProperty("Missing").IsTrue())
63+
assert.True(t, resource.GetBoolProperty("Missing", true).IsTrue())
64+
}
65+
66+
67+
68+
func Test_Resource_GetProperty_MetadataPropagation(t *testing.T) {
69+
source := `---
70+
Resources:
71+
MyResource:
72+
Type: AWS::S3::Bucket
73+
Properties:
74+
Name: myvalue
75+
Nested:
76+
DeepKey: deepvalue`
77+
78+
resource := mustParseYAML(t, source)[0].Resources["MyResource"]
79+
80+
existing := resource.GetStringProperty("Name")
81+
assert.Equal(t, "myvalue", existing.Value())
82+
assert.Equal(t, resource.GetProperty("Name").Metadata().Range(), existing.GetMetadata().Range())
83+
84+
deep := resource.GetStringProperty("Nested.DeepKey")
85+
assert.Equal(t, "deepvalue", deep.Value())
86+
assert.Equal(t, resource.GetProperty("Nested.DeepKey").Metadata().Range(), deep.GetMetadata().Range())
87+
88+
missing := resource.GetStringProperty("Missing")
89+
assert.Equal(t, "", missing.Value())
90+
assert.Equal(t, resource.Range(), missing.GetMetadata().Range())
91+
92+
nestedMissing := resource.GetStringProperty("Nested.Missing")
93+
assert.Equal(t, "", nestedMissing.Value())
94+
assert.Equal(t, resource.GetProperty("Nested").Metadata().Range(), nestedMissing.GetMetadata().Range())
95+
}

0 commit comments

Comments
 (0)