Skip to content

Commit 92d3465

Browse files
authored
feat(misconf): support for ARM resources defined as an object (#9959)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
1 parent 37b5da8 commit 92d3465

File tree

4 files changed

+122
-11
lines changed

4 files changed

+122
-11
lines changed

pkg/iac/detection/detect.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func init() {
145145
Schema string `json:"$schema"`
146146
Handler string `json:"handler"`
147147
Parameters map[string]any `json:"parameters"`
148-
Resources []any `json:"resources"`
148+
Resources any `json:"resources"`
149149
}{}
150150

151151
data, err := io.ReadAll(r)
@@ -168,7 +168,17 @@ func init() {
168168
return false
169169
}
170170

171-
return len(sniff.Parameters) > 0 || len(sniff.Resources) > 0
171+
hasResources := false
172+
if sniff.Resources != nil {
173+
switch resources := sniff.Resources.(type) {
174+
case []any:
175+
hasResources = len(resources) > 0
176+
case map[string]any:
177+
hasResources = len(resources) > 0
178+
}
179+
}
180+
181+
return len(sniff.Parameters) > 0 || hasResources
172182
}
173183

174184
matchers[FileTypeDockerfile] = func(name string, _ io.ReadSeeker) bool {

pkg/iac/detection/detect_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,30 @@ rules:
452452
FileTypeAnsible,
453453
},
454454
},
455+
{
456+
name: "Azure ARM resources defined as an object",
457+
path: "test.json",
458+
r: strings.NewReader(`{
459+
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
460+
"languageVersion": "2.0",
461+
"contentVersion": "1.0.0.0",
462+
"resources": {
463+
"myacc": {
464+
"type": "Microsoft.Storage/storageAccounts",
465+
"apiVersion": "2025-06-01",
466+
"name": "my-acc-test",
467+
"location": "location",
468+
"kind": "Storage"
469+
}
470+
}
471+
}`),
472+
expected: []FileType{
473+
FileTypeJSON,
474+
FileTypeCloudFormation,
475+
FileTypeAzureARM,
476+
FileTypeAnsible,
477+
},
478+
},
455479
{
456480
name: "Azure ARM template with parameters",
457481
path: "test.json",

pkg/iac/scanners/azure/arm/parser/parser_test.go

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ func TestParser_Parse(t *testing.T) {
2525
filename := "example.json"
2626

2727
tests := []struct {
28-
name string
29-
input string
30-
want func(fsys fs.FS) azure.Deployment
31-
wantDeployment bool
28+
name string
29+
input string
30+
want func(fsys fs.FS) azure.Deployment
3231
}{
3332
{
3433
name: "basic param",
@@ -65,7 +64,6 @@ func TestParser_Parse(t *testing.T) {
6564
},
6665
}
6766
},
68-
wantDeployment: true,
6967
},
7068
{
7169
name: "storageAccount",
@@ -186,8 +184,57 @@ func TestParser_Parse(t *testing.T) {
186184
},
187185
}
188186
},
189-
190-
wantDeployment: true,
187+
},
188+
{
189+
name: "resources defined as an object",
190+
input: `{
191+
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
192+
"languageVersion": "2.0",
193+
"contentVersion": "1.0.0.0",
194+
"resources": {
195+
"myacc": {
196+
"type": "Microsoft.Storage/storageAccounts",
197+
"apiVersion": "2025-06-01",
198+
"name": "my-acc-test",
199+
"location": "location",
200+
"kind": "Storage"
201+
}
202+
}
203+
}`,
204+
want: func(fsys fs.FS) azure.Deployment {
205+
rootMetadata := createMetadata(fsys, filename, 0, 0, "", nil).WithInternal(resolver.NewResolver())
206+
fileMetadata := createMetadata(fsys, filename, 1, 14, "", &rootMetadata)
207+
resourceMetadata := createMetadata(fsys, filename, 6, 12, "resources.myacc", &fileMetadata)
208+
return azure.Deployment{
209+
Metadata: fileMetadata,
210+
TargetScope: azure.ScopeResourceGroup,
211+
Resources: []azure.Resource{
212+
{
213+
Metadata: resourceMetadata,
214+
APIVersion: azure.NewValue(
215+
"2025-06-01",
216+
createMetadata(fsys, filename, 8, 8, "resources.myacc.apiVersion", &resourceMetadata),
217+
),
218+
Type: azure.NewValue(
219+
"Microsoft.Storage/storageAccounts",
220+
createMetadata(fsys, filename, 7, 7, "resources.myacc.type", &resourceMetadata),
221+
),
222+
Kind: azure.NewValue(
223+
"Storage",
224+
createMetadata(fsys, filename, 11, 11, "resources.myacc.kind", &resourceMetadata),
225+
),
226+
Name: azure.NewValue(
227+
"my-acc-test",
228+
createMetadata(fsys, filename, 9, 9, "resources.myacc.name", &resourceMetadata),
229+
),
230+
Location: azure.NewValue(
231+
"location",
232+
createMetadata(fsys, filename, 10, 10, "resources.myacc.location", &resourceMetadata),
233+
),
234+
},
235+
},
236+
}
237+
},
191238
},
192239
}
193240

@@ -200,7 +247,7 @@ func TestParser_Parse(t *testing.T) {
200247
got, err := p.ParseFS(t.Context(), ".")
201248
require.NoError(t, err)
202249

203-
if !tt.wantDeployment {
250+
if tt.want == nil {
204251
assert.Empty(t, got)
205252
return
206253
}

pkg/iac/scanners/azure/arm/parser/template.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json/jsontext"
66
"encoding/json/v2"
7+
"errors"
78
"fmt"
89
"io/fs"
910
"iter"
@@ -29,10 +30,39 @@ type Template struct {
2930
Parameters map[string]Parameter `json:"parameters"`
3031
Variables map[string]azure.Value `json:"variables"`
3132
Functions []Function `json:"functions"`
32-
Resources []Resource `json:"resources"`
33+
Resources Resources `json:"resources"`
3334
Outputs map[string]azure.Value `json:"outputs"`
3435
}
3536

37+
// Resources is a collection of Resource items that can be represented in ARM
38+
// templates either as an array or as an object (e.g., language v2). This custom
39+
// unmarshaler normalizes both forms into a flat slice.
40+
type Resources []Resource
41+
42+
func (r *Resources) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
43+
switch dec.PeekKind() {
44+
case '[':
45+
var arr []Resource
46+
if err := json.UnmarshalDecode(dec, &arr); err != nil {
47+
return err
48+
}
49+
*r = arr
50+
case '{':
51+
var m map[string]Resource
52+
if err := json.UnmarshalDecode(dec, &m); err != nil {
53+
return err
54+
}
55+
res := make([]Resource, 0, len(m))
56+
for _, v := range m {
57+
res = append(res, v)
58+
}
59+
*r = res
60+
default:
61+
return errors.New("unexpected JSON token for resources")
62+
}
63+
return nil
64+
}
65+
3666
type Parameter struct {
3767
Metadata *types.Metadata
3868
Type azure.Value `json:"type"`

0 commit comments

Comments
 (0)