Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions pkg/iac/detection/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func init() {
Schema string `json:"$schema"`
Handler string `json:"handler"`
Parameters map[string]any `json:"parameters"`
Resources []any `json:"resources"`
Resources any `json:"resources"`
}{}

data, err := io.ReadAll(r)
Expand All @@ -168,7 +168,17 @@ func init() {
return false
}

return len(sniff.Parameters) > 0 || len(sniff.Resources) > 0
hasResources := false
if sniff.Resources != nil {
switch resources := sniff.Resources.(type) {
case []any:
hasResources = len(resources) > 0
case map[string]any:
hasResources = len(resources) > 0
}
}

return len(sniff.Parameters) > 0 || hasResources
}

matchers[FileTypeDockerfile] = func(name string, _ io.ReadSeeker) bool {
Expand Down
24 changes: 24 additions & 0 deletions pkg/iac/detection/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,30 @@ rules:
FileTypeAnsible,
},
},
{
name: "Azure ARM resources defined as an object",
path: "test.json",
r: strings.NewReader(`{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"languageVersion": "2.0",
"contentVersion": "1.0.0.0",
"resources": {
"myacc": {
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2025-06-01",
"name": "my-acc-test",
"location": "location",
"kind": "Storage"
}
}
}`),
expected: []FileType{
FileTypeJSON,
FileTypeCloudFormation,
FileTypeAzureARM,
FileTypeAnsible,
},
},
{
name: "Azure ARM template with parameters",
path: "test.json",
Expand Down
63 changes: 55 additions & 8 deletions pkg/iac/scanners/azure/arm/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ func TestParser_Parse(t *testing.T) {
filename := "example.json"

tests := []struct {
name string
input string
want func(fsys fs.FS) azure.Deployment
wantDeployment bool
name string
input string
want func(fsys fs.FS) azure.Deployment
}{
{
name: "basic param",
Expand Down Expand Up @@ -65,7 +64,6 @@ func TestParser_Parse(t *testing.T) {
},
}
},
wantDeployment: true,
},
{
name: "storageAccount",
Expand Down Expand Up @@ -186,8 +184,57 @@ func TestParser_Parse(t *testing.T) {
},
}
},

wantDeployment: true,
},
{
name: "resources defined as an object",
input: `{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"languageVersion": "2.0",
"contentVersion": "1.0.0.0",
"resources": {
"myacc": {
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2025-06-01",
"name": "my-acc-test",
"location": "location",
"kind": "Storage"
}
}
}`,
want: func(fsys fs.FS) azure.Deployment {
rootMetadata := createMetadata(fsys, filename, 0, 0, "", nil).WithInternal(resolver.NewResolver())
fileMetadata := createMetadata(fsys, filename, 1, 14, "", &rootMetadata)
resourceMetadata := createMetadata(fsys, filename, 6, 12, "resources.myacc", &fileMetadata)
return azure.Deployment{
Metadata: fileMetadata,
TargetScope: azure.ScopeResourceGroup,
Resources: []azure.Resource{
{
Metadata: resourceMetadata,
APIVersion: azure.NewValue(
"2025-06-01",
createMetadata(fsys, filename, 8, 8, "resources.myacc.apiVersion", &resourceMetadata),
),
Type: azure.NewValue(
"Microsoft.Storage/storageAccounts",
createMetadata(fsys, filename, 7, 7, "resources.myacc.type", &resourceMetadata),
),
Kind: azure.NewValue(
"Storage",
createMetadata(fsys, filename, 11, 11, "resources.myacc.kind", &resourceMetadata),
),
Name: azure.NewValue(
"my-acc-test",
createMetadata(fsys, filename, 9, 9, "resources.myacc.name", &resourceMetadata),
),
Location: azure.NewValue(
"location",
createMetadata(fsys, filename, 10, 10, "resources.myacc.location", &resourceMetadata),
),
},
},
}
},
},
}

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

if !tt.wantDeployment {
if tt.want == nil {
assert.Empty(t, got)
return
}
Expand Down
32 changes: 31 additions & 1 deletion pkg/iac/scanners/azure/arm/parser/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"fmt"
"io/fs"
"iter"
Expand All @@ -29,10 +30,39 @@ type Template struct {
Parameters map[string]Parameter `json:"parameters"`
Variables map[string]azure.Value `json:"variables"`
Functions []Function `json:"functions"`
Resources []Resource `json:"resources"`
Resources Resources `json:"resources"`
Outputs map[string]azure.Value `json:"outputs"`
}

// Resources is a collection of Resource items that can be represented in ARM
// templates either as an array or as an object (e.g., language v2). This custom
// unmarshaler normalizes both forms into a flat slice.
type Resources []Resource

func (r *Resources) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
switch dec.PeekKind() {
case '[':
var arr []Resource
if err := json.UnmarshalDecode(dec, &arr); err != nil {
return err
}
*r = arr
case '{':
var m map[string]Resource
if err := json.UnmarshalDecode(dec, &m); err != nil {
return err
}
res := make([]Resource, 0, len(m))
for _, v := range m {
res = append(res, v)
}
*r = res
default:
return errors.New("unexpected JSON token for resources")
}
return nil
}

type Parameter struct {
Metadata *types.Metadata
Type azure.Value `json:"type"`
Expand Down
Loading