Skip to content

Commit f93539d

Browse files
tas50claude
andcommitted
✨ github/gitlab: discover CloudFormation, Dockerfile, Bicep, Helm, and Kustomize IaC
Extend the GitHub and GitLab providers' infrastructure-as-code discovery beyond Terraform and Kubernetes manifests to also detect CloudFormation templates, Dockerfiles, Bicep files, Helm charts, and Kustomize configurations in repositories. Discovery (github + gitlab): - New targets: cloudformation, dockerfiles, bicep, helm, kustomize — wired into org()/repo(), the "all" expansion, and the CLI config. - CloudFormation is detected via a content heuristic (the AWSTemplateFormatVersion marker) since templates share .yaml/.json with many other files; one child asset per template. On GitLab this uses project blob search and is skipped gracefully on instances without advanced search. - Dockerfiles via the filename:Dockerfile qualifier, accepting the common Dockerfile.* / *.Dockerfile conventions; one child asset per file. - Bicep via extension:bicep; one child asset per repo (the connection walks the checkout). - Helm via filename:Chart.yaml; one asset per chart directory. - Kustomize via filename:kustomization (kustomization.yaml/.yml / Kustomization); one asset per kustomization directory, so base and overlays each become their own asset. - Deduped the credential-cloning into a gitCredentials helper, and routed GitHub code search through a paginating searchCode helper (100/page, follows NextPage) so large repos aren't truncated to the first page. Existence checks (hasBicep) use searchCodeExists, which pages with an early return on the first non-hidden match instead of pulling every page. Git-clone support (downstream providers): - None of bicep, cloudformation, helm, kustomize, or the os dockerfile connection could clone before. Each now clones on http-url, resolves a repo-relative path/dir within the checkout where applicable, and cleans up its temp dir via Close(). A deferred cleanup guard is armed right after the clone and disarmed once the connection owns the closer, so every error path removes the checkout. Repo-based naming & stable identity: - k8s, bicep, cloudformation, helm, and kustomize now name discovered assets from the git repo (org/repo[/path]) like Terraform/Dockerfile, instead of the temporary clone directory. - Platform IDs and asset.Id are derived from the repo (and template/dir path) rather than a hash of the temp clone path, which previously changed on every scan. The bicep connection no longer overwrites cc.Options["path"] with the non-deterministic clone directory. - Asset names dropped the verbose "Static Analysis" verbiage (e.g. "CloudFormation template tas50/iac_tests/...", "Helm Chart tas50/..."). Note: on GitLab, Chart.yaml and kustomization.yaml now match their own discovery cases ahead of the k8s YAML branch, so a repo whose only YAML files are Helm/Kustomize entry points no longer also registers as a k8s manifest asset. This is intentional — those files aren't k8s manifests. Verified end-to-end against tas50/iac_tests: discovery, naming, and content queries resolve for all IaC types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 19483cc commit f93539d

19 files changed

Lines changed: 1024 additions & 98 deletions

File tree

providers/bicep/connection/connection.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import (
1515
"go.mondoo.com/mql/v13/providers-sdk/v1/plugin"
1616
)
1717

18-
var _ plugin.Connection = (*BicepConnection)(nil)
18+
var (
19+
_ plugin.Connection = (*BicepConnection)(nil)
20+
_ plugin.Closer = (*BicepConnection)(nil)
21+
)
1922

2023
type BicepConnection struct {
2124
plugin.Connection
@@ -25,6 +28,7 @@ type BicepConnection struct {
2528
bicepFiles []*BicepFile
2629
bicepParamFiles []*BicepParamFile
2730
armTemplate *ARMTemplate
31+
closer func()
2832
}
2933

3034
// BicepFile holds a Bicep source file path and its raw content.
@@ -60,8 +64,36 @@ func NewBicepConnection(id uint32, asset *inventory.Asset, conf *inventory.Confi
6064
asset: asset,
6165
}
6266

67+
// If a git clone is performed below, clean up the temporary directory on any
68+
// error path. Close() is a no-op when nothing was cloned, and the guard is
69+
// disarmed once the connection is returned and takes ownership of cleanup.
70+
cleanup := true
71+
defer func() {
72+
if cleanup {
73+
conn.Close()
74+
}
75+
}()
76+
6377
cc := asset.Connections[0]
6478
bicepPath := cc.Options["path"]
79+
// When discovered from a git repository (e.g. by the GitHub provider) the
80+
// asset carries the repo URL instead of a local path. Clone the repo and
81+
// scan the resulting directory for Bicep/ARM files.
82+
if bicepPath == "" {
83+
if _, ok := cc.Options["http-url"]; ok {
84+
clonePath, closer, err := plugin.NewGitClone(asset)
85+
if err != nil {
86+
return nil, err
87+
}
88+
conn.closer = closer
89+
bicepPath = clonePath
90+
// Intentionally do NOT overwrite cc.Options["path"]: the detector
91+
// reads it to derive a platform ID, and a non-deterministic temp
92+
// clone path would produce a different ID on every scan. The
93+
// detector's git-discovered branch (ssh-url) handles naming for
94+
// this path.
95+
}
96+
}
6597
conn.path = bicepPath
6698

6799
fi, err := os.Stat(bicepPath)
@@ -108,9 +140,17 @@ func NewBicepConnection(id uint32, asset *inventory.Asset, conf *inventory.Confi
108140
conn.bicepFiles = []*BicepFile{{Path: bicepPath, Content: string(content)}}
109141
}
110142

143+
cleanup = false
111144
return conn, nil
112145
}
113146

147+
// Close cleans up any temporary directory created by a git clone.
148+
func (c *BicepConnection) Close() {
149+
if c.closer != nil {
150+
c.closer()
151+
}
152+
}
153+
114154
func (c *BicepConnection) Name() string {
115155
return "bicep"
116156
}

providers/bicep/provider/provider.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"go.mondoo.com/mql/v13/providers-sdk/v1/upstream"
2020
"go.mondoo.com/mql/v13/providers/bicep/connection"
2121
"go.mondoo.com/mql/v13/providers/bicep/resources"
22+
"go.mondoo.com/mql/v13/utils/urlx"
2223
)
2324

2425
const (
@@ -141,6 +142,21 @@ func (s *Service) detect(asset *inventory.Asset, conn *connection.BicepConnectio
141142
TechnologyUrlSegments: []string{"iac", "bicep", "template"},
142143
}
143144

145+
// When discovered from a git repository (e.g. by the GitHub provider) prefer
146+
// the repo (org/repo) for the name and platform ID. The local path is a
147+
// temporary clone directory whose hash would change on every scan.
148+
if url, ok := asset.Connections[0].Options["ssh-url"]; ok {
149+
domain, org, repo, err := urlx.ParseGitSshUrl(url)
150+
if err == nil {
151+
platformID := "//platformid.api.mondoo.app/runtime/bicep/domain/" + domain + "/org/" + org + "/repo/" + repo
152+
asset.Id = platformID
153+
asset.Connections[0].PlatformId = platformID
154+
asset.PlatformIds = []string{platformID}
155+
asset.Name = "Bicep file " + org + "/" + repo
156+
return nil
157+
}
158+
}
159+
144160
projectPath, ok := asset.Connections[0].Options["path"]
145161
if ok {
146162
absPath, _ := filepath.Abs(projectPath)
@@ -150,7 +166,7 @@ func (s *Service) detect(asset *inventory.Asset, conn *connection.BicepConnectio
150166
platformID := "//platformid.api.mondoo.app/runtime/bicep/hash/" + hash
151167
asset.Connections[0].PlatformId = platformID
152168
asset.PlatformIds = []string{platformID}
153-
asset.Name = "Bicep Static Analysis " + parseNameFromPath(projectPath)
169+
asset.Name = "Bicep file " + parseNameFromPath(projectPath)
154170
return nil
155171
}
156172

@@ -182,7 +198,7 @@ func parseNameFromPath(file string) string {
182198

183199
// When the user passed a bare "." (current directory) the loop above
184200
// hands back "." via path.Base, which makes the asset name read
185-
// "Bicep Static Analysis .". Resolve to the absolute path so the
201+
// "Bicep file .". Resolve to the absolute path so the
186202
// recursion picks up the actual directory basename instead.
187203
if name == "." {
188204
abspath, err := filepath.Abs(file)

providers/bicep/provider/provider_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414

1515
// parseNameFromPath used to call filepath.Abs(name) — i.e. on the
1616
// "." sentinel instead of the original path argument — so the asset
17-
// name read "Bicep Static Analysis .". The fix passes `file` to Abs
18-
// so the directory basename is recovered.
17+
// name read "Bicep file .". The fix passes `file` to Abs so the
18+
// directory basename is recovered.
1919
func TestParseNameFromPath_DotResolvesToBasename(t *testing.T) {
2020
dir := t.TempDir()
2121
cwd, err := os.Getwd()

providers/cloudformation/connection/connection.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ package connection
66
import (
77
"errors"
88
"os"
9+
"path/filepath"
910

1011
"github.com/aws-cloudformation/rain/cft"
1112
"github.com/aws-cloudformation/rain/cft/parse"
1213
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
1314
"go.mondoo.com/mql/v13/providers-sdk/v1/plugin"
1415
)
1516

16-
var _ plugin.Connection = (*CloudformationConnection)(nil)
17+
var (
18+
_ plugin.Connection = (*CloudformationConnection)(nil)
19+
_ plugin.Closer = (*CloudformationConnection)(nil)
20+
)
1721

1822
type CloudformationConnection struct {
1923
plugin.Connection
@@ -22,6 +26,7 @@ type CloudformationConnection struct {
2226
// Add custom connection fields here
2327
path string
2428
cftTemplate cft.Template
29+
closer func()
2530
}
2631

2732
func NewCloudformationConnection(id uint32, asset *inventory.Asset, conf *inventory.Config) (*CloudformationConnection, error) {
@@ -30,9 +35,33 @@ func NewCloudformationConnection(id uint32, asset *inventory.Asset, conf *invent
3035
Conf: conf,
3136
asset: asset,
3237
}
33-
// initialize your connection here
38+
39+
// If a git clone is performed below, clean up the temporary directory on any
40+
// error path. Close() is a no-op when nothing was cloned, and the guard is
41+
// disarmed once the connection is returned and takes ownership of cleanup.
42+
cleanupClone := true
43+
defer func() {
44+
if cleanupClone {
45+
conn.Close()
46+
}
47+
}()
48+
3449
cc := asset.Connections[0]
3550
path := cc.Options["path"]
51+
// When discovered from a git repository (e.g. by the GitHub provider) the
52+
// asset carries the repo URL plus a repo-relative path to the template.
53+
// Clone the repo and resolve the template within the checkout. We keep the
54+
// repo-relative path in the options so the detector can build a stable,
55+
// human-friendly asset name and platform ID from the repo rather than the
56+
// temporary clone directory.
57+
if _, ok := cc.Options["http-url"]; ok {
58+
clonePath, closer, err := plugin.NewGitClone(asset)
59+
if err != nil {
60+
return nil, err
61+
}
62+
conn.closer = closer
63+
path = filepath.Join(clonePath, path)
64+
}
3665
conn.path = path
3766

3867
f, err := os.Open(path)
@@ -50,9 +79,17 @@ func NewCloudformationConnection(id uint32, asset *inventory.Asset, conf *invent
5079
}
5180
conn.cftTemplate = *cftTemplate
5281

82+
cleanupClone = false
5383
return conn, nil
5484
}
5585

86+
// Close cleans up any temporary directory created by a git clone.
87+
func (c *CloudformationConnection) Close() {
88+
if c.closer != nil {
89+
c.closer()
90+
}
91+
}
92+
5693
func (c *CloudformationConnection) Name() string {
5794
return "cloudformation"
5895
}

providers/cloudformation/provider/provider.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"go.mondoo.com/mql/v13/providers-sdk/v1/upstream"
2020
"go.mondoo.com/mql/v13/providers/cloudformation/connection"
2121
"go.mondoo.com/mql/v13/providers/cloudformation/resources"
22+
"go.mondoo.com/mql/v13/utils/urlx"
2223
)
2324

2425
const (
@@ -139,6 +140,29 @@ func (s *Service) detect(asset *inventory.Asset, conn *connection.Cloudformation
139140
TechnologyUrlSegments: []string{"iac", "cloudformation", "template"},
140141
}
141142

143+
// When discovered from a git repository (e.g. by the GitHub provider) prefer
144+
// the repo (org/repo) plus the repo-relative template path for the name and
145+
// platform ID. The local path is a temporary clone directory whose hash
146+
// would change on every scan.
147+
if url, ok := asset.Connections[0].Options["ssh-url"]; ok {
148+
domain, org, repo, err := urlx.ParseGitSshUrl(url)
149+
if err == nil {
150+
name := org + "/" + repo
151+
platformID := "//platformid.api.mondoo.app/runtime/cloudformation/domain/" + domain + "/org/" + org + "/repo/" + repo
152+
// A repository can hold multiple templates; qualify by the
153+
// repo-relative path so each one is a distinct asset.
154+
if relPath := asset.Connections[0].Options["path"]; relPath != "" {
155+
platformID += "/path/" + relPath
156+
name += "/" + relPath
157+
}
158+
asset.Id = platformID
159+
asset.Connections[0].PlatformId = platformID
160+
asset.PlatformIds = []string{platformID}
161+
asset.Name = "CloudFormation template " + name
162+
return nil
163+
}
164+
}
165+
142166
projectPath, ok := asset.Connections[0].Options["path"]
143167
if ok {
144168
absPath, _ := filepath.Abs(projectPath)
@@ -148,7 +172,7 @@ func (s *Service) detect(asset *inventory.Asset, conn *connection.Cloudformation
148172
platformID := "//platformid.api.mondoo.app/runtime/cloudformation/hash/" + hash
149173
asset.Connections[0].PlatformId = platformID
150174
asset.PlatformIds = []string{platformID}
151-
asset.Name = "CloudFormation Static Analysis " + parseNameFromPath(projectPath)
175+
asset.Name = "CloudFormation template " + parseNameFromPath(projectPath)
152176
return nil
153177
}
154178

providers/github/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ Notes:
4747
connection.DiscoveryOrganization,
4848
connection.DiscoveryTerraform,
4949
connection.DiscoveryK8sManifests,
50+
connection.DiscoveryCloudformation,
51+
connection.DiscoveryDockerfiles,
52+
connection.DiscoveryBicep,
53+
connection.DiscoveryHelm,
54+
connection.DiscoveryKustomize,
5055
},
5156
Flags: []plugin.Flag{
5257
{

providers/github/connection/platform.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ import (
99
)
1010

1111
const (
12-
DiscoveryAll = "all"
13-
DiscoveryAuto = "auto"
14-
DiscoveryRepos = "repos"
15-
DiscoveryUsers = "users"
16-
DiscoveryOrganization = "organization"
17-
DiscoveryTerraform = "terraform"
18-
DiscoveryK8sManifests = "k8s-manifests"
12+
DiscoveryAll = "all"
13+
DiscoveryAuto = "auto"
14+
DiscoveryRepos = "repos"
15+
DiscoveryUsers = "users"
16+
DiscoveryOrganization = "organization"
17+
DiscoveryTerraform = "terraform"
18+
DiscoveryK8sManifests = "k8s-manifests"
19+
DiscoveryCloudformation = "cloudformation"
20+
DiscoveryDockerfiles = "dockerfiles"
21+
DiscoveryBicep = "bicep"
22+
DiscoveryHelm = "helm"
23+
DiscoveryKustomize = "kustomize"
1924
)
2025

2126
var (

0 commit comments

Comments
 (0)