Skip to content

Commit 5bab816

Browse files
committed
feat(scan): expand manifest coverage, add Kubernetes/Helm, OCI enrichment, and .gitignore respect
Detection & parsing: - Kubernetes manifests (Pod/Deployment/ReplicaSet/StatefulSet/DaemonSet/Job/ CronJob incl. init & ephemeral containers) and Helm charts (Chart.yaml deps + values.yaml images) -> pkg:oci / pkg:helm components - New ecosystems: Conda (environment.yml), Clojure (project.clj/deps.edn/bb.edn), Python setuptools (setup.py/setup.cfg), C# *.fsproj/*.vbproj/packages.config, npm-shrinkwrap.json, mill build.sc - Registry-config detection (.npmrc/.yarnrc/settings.gradle) surfaces private/custom registry endpoints as a supply-chain signal (never tokens) OCI enrichment: - Every container image carries registryType (dockerhub/gcr/ecr/acr/ghcr/ gitlab/quay/local/private) and IsPrivateRegistry, emitted as CycloneDX component properties .gitignore respect: - New internal/ignore matcher (nested, negation, anchoring, dir-only, ** globs) - Default-on for sast/secrets/containers/iac/cbom/aibom; sca and malscan are exempt (they target commonly-gitignored install dirs) - Flags: --include-ignored (scan) and per-mode --<mode>-include-ignored Full integration across detector, parsers, purl/registry attribution, package- manager binary capability, malscan install dirs, and CLI docs.
1 parent 9de5391 commit 5bab816

30 files changed

Lines changed: 1451 additions & 40 deletions

cmd/aibom.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func init() {
6868
aibomCmd.Flags().Bool("no-commits", false, "Skip the git commit-history detection pass")
6969
aibomCmd.Flags().Int("commit-scan-max", 2000, "Maximum number of commits (from HEAD) the commit-history pass inspects")
7070
aibomCmd.Flags().Bool("no-upload", false, "Do not submit the AIBOM to Vulnetix (it is submitted automatically when authenticated)")
71+
aibomCmd.Flags().Bool("aibom-include-ignored", false, "Include files matched by .gitignore (default: gitignored paths are skipped)")
7172
rootCmd.AddCommand(aibomCmd)
7273
}
7374

@@ -89,6 +90,7 @@ func runAIBOM(cmd *cobra.Command, args []string) error {
8990
noCommits, _ := cmd.Flags().GetBool("no-commits")
9091
commitMax, _ := cmd.Flags().GetInt("commit-scan-max")
9192
noUpload, _ := cmd.Flags().GetBool("no-upload")
93+
includeIgnored, _ := cmd.Flags().GetBool("aibom-include-ignored")
9294

9395
switch outputFmt {
9496
case "pretty", "table", "json", "cyclonedx-json":
@@ -111,15 +113,16 @@ func runAIBOM(cmd *cobra.Command, args []string) error {
111113
}
112114

113115
det, err := aibom.Detect(aibom.Options{
114-
Root: rootPath,
115-
MaxDepth: depth,
116-
Ignore: ignore,
117-
ScanEnv: !noEnv,
118-
IncludeHome: includeHome,
119-
ScanSource: !noSource,
120-
ScanCommits: !noCommits,
121-
CommitMax: commitMax,
122-
Catalog: compiled,
116+
Root: rootPath,
117+
MaxDepth: depth,
118+
Ignore: ignore,
119+
ScanEnv: !noEnv,
120+
IncludeHome: includeHome,
121+
ScanSource: !noSource,
122+
ScanCommits: !noCommits,
123+
CommitMax: commitMax,
124+
Catalog: compiled,
125+
RespectGitignore: !includeIgnored,
123126
})
124127
if err != nil {
125128
return err

cmd/cbom.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func init() {
7575
cbomCmd.Flags().Bool("no-deps", false, "Skip the crypto-library detection pass")
7676
cbomCmd.Flags().String("fail-on", "none", "Exit non-zero when crypto of these PQC statuses is found: none, quantum-vulnerable, deprecated (comma-separated)")
7777
cbomCmd.Flags().Bool("no-upload", false, "Do not submit the CBOM to Vulnetix (it is submitted automatically when authenticated)")
78+
cbomCmd.Flags().Bool("cbom-include-ignored", false, "Include files matched by .gitignore (default: gitignored paths are skipped)")
7879
rootCmd.AddCommand(cbomCmd)
7980
}
8081

@@ -96,6 +97,7 @@ func runCBOM(cmd *cobra.Command, args []string) error {
9697
noDeps, _ := cmd.Flags().GetBool("no-deps")
9798
failOnRaw, _ := cmd.Flags().GetString("fail-on")
9899
noUpload, _ := cmd.Flags().GetBool("no-upload")
100+
includeIgnored, _ := cmd.Flags().GetBool("cbom-include-ignored")
99101

100102
switch outputFmt {
101103
case "pretty", "table", "json", "cyclonedx-json":
@@ -122,14 +124,15 @@ func runCBOM(cmd *cobra.Command, args []string) error {
122124
}
123125

124126
det, err := cbom.Detect(cbom.Options{
125-
Root: rootPath,
126-
MaxDepth: depth,
127-
Ignore: ignore,
128-
ScanSource: !noSource,
129-
ScanConfig: !noConfig,
130-
ScanCerts: !noCerts,
131-
ScanDeps: !noDeps,
132-
Catalog: compiled,
127+
Root: rootPath,
128+
MaxDepth: depth,
129+
Ignore: ignore,
130+
ScanSource: !noSource,
131+
ScanConfig: !noConfig,
132+
ScanCerts: !noCerts,
133+
ScanDeps: !noDeps,
134+
Catalog: compiled,
135+
RespectGitignore: !includeIgnored,
133136
})
134137
if err != nil {
135138
return err

cmd/cli_sca.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,12 @@ func registryForEcosystem(eco string) string {
13941394
return "https://packagist.org"
13951395
case "nuget", ".net":
13961396
return "https://www.nuget.org"
1397+
case "conda":
1398+
return "https://repo.anaconda.com/pkgs/main"
1399+
case "clojars", "clojure":
1400+
return "https://repo.clojars.org"
1401+
case "helm":
1402+
return "https://artifacthub.io"
13971403
}
13981404
return ""
13991405
}
@@ -1417,6 +1423,12 @@ func providerForEcosystem(eco string) string {
14171423
return "Packagist"
14181424
case "nuget", ".net":
14191425
return "NuGet"
1426+
case "conda":
1427+
return "Anaconda"
1428+
case "clojars", "clojure":
1429+
return "Clojars"
1430+
case "helm":
1431+
return "Artifact Hub"
14201432
}
14211433
return ""
14221434
}

cmd/scan.go

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,16 @@ func runScanWithFeatures(ctx context.Context, cmd *cobra.Command, noSAST, noSCA,
414414
yes, _ := cmd.Flags().GetBool("yes")
415415
pathExplicit := cmd.Flags().Changed("path")
416416

417+
// gitignore respect: the SAST-family walks (sast/secrets/containers/iac,
418+
// and the generic scan's SAST engine) honour .gitignore by default. sca is
419+
// exempt — dependency manifests routinely live in gitignored install dirs —
420+
// and so the shared manifest walk only prunes .gitignore for the standalone
421+
// containers/iac commands (where SCA is not consuming its output). Users opt
422+
// back in with --include-ignored (generic) or --<mode>-include-ignored.
423+
includeIgnored := includeIgnoredForScan(cmd)
424+
respectGitignoreSAST := !includeIgnored
425+
respectGitignoreManifest := !includeIgnored && (cmd.Name() == "containers" || cmd.Name() == "iac")
426+
417427
// Org quality-gate enforcement override. Applied after all nine control
418428
// flags are read but BEFORE any is consumed (sca-autofix strategy parsing
419429
// below, runLocalScan, and the gate options). For an authenticated org with
@@ -528,16 +538,38 @@ func runScanWithFeatures(ctx context.Context, cmd *cobra.Command, noSAST, noSCA,
528538

529539
// ── 2. Discover files ──────────────────────────────────────────────
530540
files, err := scan.WalkForScanFiles(scan.WalkOptions{
531-
RootPath: scanPath,
532-
MaxDepth: depth,
533-
Excludes: excludes,
541+
RootPath: scanPath,
542+
MaxDepth: depth,
543+
Excludes: excludes,
544+
RespectGitignore: respectGitignoreManifest,
534545
})
535546
if err != nil {
536547
return fmt.Errorf("failed to scan directory: %w", err)
537548
}
538549

539550
analytics.TrackScan("sbom", len(files))
540551

552+
// Surface private/custom package registries declared in .npmrc / .yarnrc /
553+
// settings.gradle as a supply-chain signal (informational; never gates).
554+
if endpoints := scan.SummarizeRegistryConfigs(files); len(endpoints) > 0 {
555+
var privates []scan.RegistryEndpoint
556+
for _, e := range endpoints {
557+
if e.Private {
558+
privates = append(privates, e)
559+
}
560+
}
561+
if len(privates) > 0 && !resultsOnly {
562+
fmt.Fprintf(os.Stderr, "Registry config: %d private/custom registry endpoint(s) declared:\n", len(privates))
563+
for _, e := range privates {
564+
scopeNote := ""
565+
if e.Scope != "" {
566+
scopeNote = " (" + e.Scope + ")"
567+
}
568+
fmt.Fprintf(os.Stderr, " %s%s → %s [%s]\n", e.Ecosystem, scopeNote, e.URL, e.Source)
569+
}
570+
}
571+
}
572+
541573
// ── Local malware scan (malscan-engine, in-process) ─────────────────
542574
// Runs as a pass on `scan`; on `sca` only when --block-malware / org
543575
// blockMalware is in effect (see shouldRunMalscanPass). It scans the
@@ -694,6 +726,7 @@ func runScanWithFeatures(ctx context.Context, cmd *cobra.Command, noSAST, noSCA,
694726
gitHistory,
695727
gitHistoryMaxCommits,
696728
gitHistoryMaxFiles,
729+
respectGitignoreSAST,
697730
)
698731

699732
return mergeMalscanBreach(scanErr, malscanBreach)
@@ -798,6 +831,7 @@ func runLocalScan(
798831
gitHistory bool,
799832
gitHistoryMaxCommits int,
800833
gitHistoryMaxFiles int,
834+
respectGitignore bool,
801835
) (retErr error) {
802836
dctx := display.NewWithProgress(display.ModeText, silent, noProgress)
803837
scanProgress := dctx.Progress("Scan", 7)
@@ -1215,6 +1249,7 @@ func runLocalScan(
12151249
gitHistory,
12161250
gitHistoryMaxCommits,
12171251
gitHistoryMaxFiles,
1252+
respectGitignore,
12181253
)
12191254
}
12201255
}
@@ -1423,6 +1458,7 @@ func runLocalScan(
14231458
GitHistory: enableGitHistory && gitHistory,
14241459
GitHistoryMaxCommits: gitHistoryMaxCommits,
14251460
GitHistoryMaxFiles: gitHistoryMaxFiles,
1461+
RespectGitignore: respectGitignore,
14261462
})
14271463
// Hint the caller that the synthetic-content behaviour was engaged.
14281464
if enableBinaryInspection && !ignoreBinaries {
@@ -4326,6 +4362,8 @@ func addScanFlags(cmd *cobra.Command) {
43264362
"Cap the number of commits walked during the git-history secrets stage (0 = no cap)")
43274363
cmd.Flags().Int("git-history-max-files", 5000,
43284364
"Cap the number of file versions extracted from git history (0 = no cap)")
4365+
cmd.Flags().Bool("include-ignored", false,
4366+
"Include files matched by .gitignore. By default the SAST, secrets, containers and IaC passes skip gitignored paths; sca and malscan always scan them (dependency install dirs are commonly gitignored).")
43294367
_ = cmd.Flags().MarkDeprecated("format", "use --output instead")
43304368
_ = cmd.RegisterFlagCompletionFunc("sca-autofix-strategy", cobra.FixedCompletions(
43314369
[]string{"stable", "safest", "latest"}, cobra.ShellCompDirectiveNoFileComp))
@@ -4338,6 +4376,21 @@ func addScanFlags(cmd *cobra.Command) {
43384376
_ = cmd.MarkFlagDirname("path")
43394377
}
43404378

4379+
// includeIgnoredForScan reports whether the user asked to include .gitignored
4380+
// paths, honouring both the generic --include-ignored flag and the per-mode
4381+
// alias (e.g. --sast-include-ignored) registered on specialized subcommands.
4382+
func includeIgnoredForScan(cmd *cobra.Command) bool {
4383+
for _, name := range []string{"include-ignored", cmd.Name() + "-include-ignored"} {
4384+
if cmd.Flags().Lookup(name) == nil {
4385+
continue
4386+
}
4387+
if v, _ := cmd.Flags().GetBool(name); v {
4388+
return true
4389+
}
4390+
}
4391+
return false
4392+
}
4393+
43414394
// addSASTFlags registers SAST-specific flags on cmd.
43424395
func addSASTFlags(cmd *cobra.Command) {
43434396
cmd.Flags().Bool("disable-default-rules", false, "Skip built-in default SAST rules")
@@ -4371,7 +4424,7 @@ func filterFilesByFeature(files []scan.DetectedFile, noSCA, noContainers, noIAC
43714424
continue
43724425
}
43734426
lang := f.ManifestInfo.Language
4374-
isContainer := lang == "docker"
4427+
isContainer := lang == "docker" || lang == "kubernetes" || lang == "helm"
43754428
isIAC := lang == "hcl" || lang == "nix"
43764429
isSCA := !isContainer && !isIAC
43774430
if isContainer && noContainers {

cmd/specialized_scans.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ func init() {
182182
for _, cmd := range []*cobra.Command{secretsCmd, containersCmd, iacCmd, sastCmd} {
183183
addScanFlags(cmd)
184184
addSASTFlags(cmd)
185+
// Per-mode .gitignore override alias. The generic --include-ignored
186+
// registered by addScanFlags also works, but the named flag reads more
187+
// naturally on a specialized command and is what the docs advertise.
188+
cmd.Flags().Bool(cmd.Name()+"-include-ignored", false,
189+
"Include files matched by .gitignore in this "+cmd.Name()+" scan (default: gitignored paths are skipped)")
185190
rootCmd.AddCommand(cmd)
186191
}
187192
}

internal/aibom/aibomgen/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ vulnetix aibom [path] [flags]
331331
| ` + "`--no-source`" + ` | bool | ` + "`false`" + ` | Skip the source-code SDK / model detection pass |
332332
| ` + "`--no-commits`" + ` | bool | ` + "`false`" + ` | Skip the git commit-history detection pass |
333333
| ` + "`--commit-scan-max`" + ` | int | ` + "`2000`" + ` | Max commits (from HEAD) the commit-history pass inspects |
334+
| ` + "`--aibom-include-ignored`" + ` | bool | ` + "`false`" + ` | Include files matched by ` + "`.gitignore`" + ` (default: gitignored paths are skipped) |
334335
335336
## Output
336337

internal/aibom/detect.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type Options struct {
4343
Catalog *CompiledCatalog
4444
// Environ is injectable for tests; defaults to os.Environ() when nil.
4545
Environ []string
46+
// RespectGitignore prunes .gitignored paths (default off; the aibom command
47+
// sets it true unless --aibom-include-ignored is passed).
48+
RespectGitignore bool
4649
}
4750

4851
// Detect runs the enabled passes and returns CycloneDX-ready detections.
@@ -72,9 +75,10 @@ func Detect(opts Options) (cdx.AIDetections, error) {
7275
depth = defaultMaxDepth
7376
}
7477
input, err := sast.BuildScanInputWithOptions(abs, sast.BuildOptions{
75-
MaxDepth: depth,
76-
IgnoreGlobs: opts.Ignore,
77-
IgnoreGit: true,
78+
MaxDepth: depth,
79+
IgnoreGlobs: opts.Ignore,
80+
IgnoreGit: true,
81+
RespectGitignore: opts.RespectGitignore,
7882
})
7983
if err != nil {
8084
return cdx.AIDetections{}, err

internal/cbom/cbomgen/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ vulnetix cbom [path] [flags]
319319
| ` + "`--no-deps`" + ` | bool | ` + "`false`" + ` | Skip the crypto-library detection pass |
320320
| ` + "`--fail-on`" + ` | string | ` + "`none`" + ` | Exit non-zero when crypto of these PQC statuses is found (e.g. ` + "`quantum-vulnerable`, `deprecated`" + `) |
321321
| ` + "`--no-upload`" + ` | bool | ` + "`false`" + ` | Do not submit the CBOM to Vulnetix (submitted automatically when authenticated) |
322+
| ` + "`--cbom-include-ignored`" + ` | bool | ` + "`false`" + ` | Include files matched by ` + "`.gitignore`" + ` (default: gitignored paths are skipped) |
322323
323324
## Output
324325

internal/cbom/detect.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type Options struct {
3434
ScanDeps bool
3535
IncludeHome bool
3636
Catalog *CompiledCatalog
37+
// RespectGitignore prunes .gitignored paths (default off; the cbom command
38+
// sets it true unless --cbom-include-ignored is passed).
39+
RespectGitignore bool
3740
}
3841

3942
// Detect runs the enabled passes and returns CycloneDX-ready crypto detections.
@@ -55,9 +58,10 @@ func Detect(opts Options) (cdx.CryptoDetections, error) {
5558
depth = defaultMaxDepth
5659
}
5760
input, err := sast.BuildScanInputWithOptions(abs, sast.BuildOptions{
58-
MaxDepth: depth,
59-
IgnoreGlobs: opts.Ignore,
60-
IgnoreGit: true,
61+
MaxDepth: depth,
62+
IgnoreGlobs: opts.Ignore,
63+
IgnoreGit: true,
64+
RespectGitignore: opts.RespectGitignore,
6165
})
6266
if err != nil {
6367
return cdx.CryptoDetections{}, err

internal/cdx/local.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"net/url"
88
"sort"
9+
"strconv"
910
"strings"
1011
"time"
1112

@@ -155,6 +156,13 @@ func BuildFromLocalScan(results []LocalScanResult, specVersion string, scanCtx *
155156
Value: pkg.SourceFile,
156157
})
157158
}
159+
// Container-image registry provenance (oci ecosystem only).
160+
if compType == "container" && pkg.RegistryType != "" {
161+
comp.Properties = append(comp.Properties,
162+
Property{Name: "vulnetix:oci:registryType", Value: pkg.RegistryType},
163+
Property{Name: "vulnetix:oci:private", Value: strconv.FormatBool(pkg.IsPrivateRegistry)},
164+
)
165+
}
158166
for _, cs := range pkg.Checksums {
159167
switch cs.Alg {
160168
case "SHA-256", "SHA-1":
@@ -476,6 +484,13 @@ func localEcosystemToPurlType(ecosystem string) string {
476484
return "hex"
477485
case "swift":
478486
return "swift"
487+
case "helm":
488+
return "helm"
489+
case "conda":
490+
return "conda"
491+
case "clojars", "clojure":
492+
// Clojure coordinates are Maven group:artifact pairs.
493+
return "maven"
479494
default:
480495
return ecosystem
481496
}

0 commit comments

Comments
 (0)