@cdxgen/cdxgen 13.0.1 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -13
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +125 -4
- package/bin/evinse.js +1 -1
- package/data/component-tags.json +8 -1
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1144 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/cli/bomAssembly.js +20 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +76 -48
- package/lib/cli/jsBom.js +32 -3
- package/lib/cli/jvmBom.js +1494 -1315
- package/lib/cli/managedBom.js +97 -28
- package/lib/cli/nativeBom.js +189 -10
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +69 -1
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/ecosystems/ecosystems.js +9 -0
- package/lib/ecosystems/gradleutils.js +355 -15
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-misc.js +16 -2
- package/lib/ecosystems/parsers-python.js +171 -35
- package/lib/ecosystems/parsers-rust.js +12 -2
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/evinser/evinser.js +30 -12
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/auditCategories.js +9 -0
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +431 -22
- package/lib/inventory/envcontext.js +206 -18
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +227 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +115 -2
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +14 -14
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +33 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +20 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +5 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +8 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +20 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +9 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +2 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# Build Fidelity Rules
|
|
2
|
+
# Category: build-fidelity (opt-in only)
|
|
3
|
+
# Detects when the generated BOM does not faithfully represent the project's
|
|
4
|
+
# real dependency set: an unresolved manifest passed off as a graph, a resolver
|
|
5
|
+
# that silently failed, or components missing the coordinates a healthy scan
|
|
6
|
+
# would produce.
|
|
7
|
+
#
|
|
8
|
+
# This category is activated by the `--introspect` reflection step, never by a
|
|
9
|
+
# default `--bom-audit` run. lib/inventory/auditCategories.js lists it in
|
|
10
|
+
# OPT_IN_BOM_AUDIT_CATEGORIES, which keeps auditBom() from activating it unless
|
|
11
|
+
# it is explicitly requested.
|
|
12
|
+
#
|
|
13
|
+
# Thresholds were calibrated against measured BOM pairs, not chosen a priori.
|
|
14
|
+
# The measured pairs (project: components/dep nodes/edges) are:
|
|
15
|
+
# healthy and must stay silent:
|
|
16
|
+
# ipsw (go) 203/204/387, binwalk (rust) 228/210/426, poetry (python)
|
|
17
|
+
# 149/102/125, eShop (dotnet) 75/25/125, java-sec-code (java, maven
|
|
18
|
+
# available) 204/205/204
|
|
19
|
+
# degraded and must fire:
|
|
20
|
+
# spotube (dart) 344/1/103, helm-charts (helm) 52/0/0, babashka (clj)
|
|
21
|
+
# 23/0/0, java-sec-code with maven hidden (java) 58/0/0
|
|
22
|
+
# A 30% coverage-loss threshold would fire on the healthy poetry BOM (32.2% of
|
|
23
|
+
# its components legitimately sit outside the dependency graph), so the
|
|
24
|
+
# coverage rules use >50%. Every threshold below cites the pair that fixed it.
|
|
25
|
+
#
|
|
26
|
+
# Conditions reference only BOM structure, so every rule supports dry runs in
|
|
27
|
+
# full. `tier-signal` names the fidelity tier the finding demotes the ecosystem
|
|
28
|
+
# to (resolved > lockfile > manifest > heuristic > absent); `applies-to` names
|
|
29
|
+
# the purl types the rule may fire on. A rule without `applies-to` is a rule
|
|
30
|
+
# about BOM structure itself (lifecycle claims, empty component sets) and is
|
|
31
|
+
# gated by its condition instead. Rules that assume a resolver or lockfile
|
|
32
|
+
# ecosystem MUST declare `applies-to`; an unscoped coverage rule would fire on
|
|
33
|
+
# every helm or clojure project forever. Ecosystems with no lockfile concept
|
|
34
|
+
# (r, perl, elm, crystal, nim) and ecosystems cdxgen parses without ever
|
|
35
|
+
# building a graph (mix.lock, Package.resolved) are kept out of `applies-to`
|
|
36
|
+
# so the rules stay silent where no remediation can improve the BOM.
|
|
37
|
+
#
|
|
38
|
+
# NOTE: the strongest single signal - an ecosystem's marker file exists on disk
|
|
39
|
+
# but the BOM has no components for it - is intentionally NOT expressed here.
|
|
40
|
+
# The rule engine sees no filesystem. That check belongs to the reflection
|
|
41
|
+
# step (markersOnDisk vs ecosystemsInBom). Do not add a BOM-only rule for it.
|
|
42
|
+
|
|
43
|
+
- id: BF-GEN-001
|
|
44
|
+
name: "Dependency graph covers less than half of the components"
|
|
45
|
+
description: >-
|
|
46
|
+
More than 50% of the components with a bom-ref appear nowhere in
|
|
47
|
+
dependencies[] - neither as a graph node nor as a dependsOn target - which
|
|
48
|
+
means the resolver or lockfile that would have connected them did not run.
|
|
49
|
+
severity: high
|
|
50
|
+
category: build-fidelity
|
|
51
|
+
dry-run-support: full
|
|
52
|
+
tier-signal: manifest
|
|
53
|
+
applies-to: [maven, npm, pypi, golang, cargo, gem, nuget, composer, pub, helm, clojars]
|
|
54
|
+
condition: |
|
|
55
|
+
(
|
|
56
|
+
$refs := $exists(dependencies) ? dependencies.ref : [];
|
|
57
|
+
$targets := $exists(dependencies) ? dependencies.dependsOn : [];
|
|
58
|
+
$comps := $exists(components) ? components[$exists($."bom-ref")] : [];
|
|
59
|
+
$total := $count($comps);
|
|
60
|
+
$covered := $count($comps[$listContains($refs, $."bom-ref") or $listContains($targets, $."bom-ref")]);
|
|
61
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
62
|
+
$notContainer and $count(components[purl][$arrayContains(['maven', 'npm', 'pypi', 'golang', 'cargo', 'gem', 'nuget', 'composer', 'pub', 'helm', 'clojars'], $substringBefore($substringAfter(purl, 'pkg:'), '/'))]) > 0 and $total > 0 and ($total - $covered) / $total > 0.5
|
|
63
|
+
? { "total": $total, "covered": $covered, "uncovered": $total - $covered, "uncoveredPercent": $round(100 * ($total - $covered) / $total, 1) }
|
|
64
|
+
: undefined
|
|
65
|
+
)
|
|
66
|
+
location: |
|
|
67
|
+
{ "componentCount": $count(components), "dependencyCount": $count(dependencies) }
|
|
68
|
+
message: "Only {{ covered }} of {{ total }} components appear in the dependency graph ({{ uncoveredPercent }}% uncovered); the BOM reads like a flat list of declarations"
|
|
69
|
+
mitigation: "Run the ecosystem resolver before generating the BOM (or let cdxgen --install-deps drive it) so transitive edges are captured, then re-scan"
|
|
70
|
+
evidence: |
|
|
71
|
+
{
|
|
72
|
+
"total": total,
|
|
73
|
+
"covered": covered,
|
|
74
|
+
"uncovered": uncovered,
|
|
75
|
+
"uncoveredPercent": uncoveredPercent
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
- id: BF-GEN-002
|
|
79
|
+
name: "More than 20% of components carry no version"
|
|
80
|
+
description: >-
|
|
81
|
+
Components without a version cannot be pinned, compared, or audited; they
|
|
82
|
+
usually come from filename or artifact inference rather than a manifest
|
|
83
|
+
that pins versions.
|
|
84
|
+
severity: medium
|
|
85
|
+
category: build-fidelity
|
|
86
|
+
dry-run-support: full
|
|
87
|
+
tier-signal: heuristic
|
|
88
|
+
applies-to: [maven, npm, pypi, golang, cargo, gem, nuget, composer, pub, helm, clojars]
|
|
89
|
+
condition: |
|
|
90
|
+
(
|
|
91
|
+
$comps := $exists(components) ? components : [];
|
|
92
|
+
$total := $count($comps);
|
|
93
|
+
$missing := $count($comps[$not($exists(version)) or $string(version) = '']);
|
|
94
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
95
|
+
$notContainer and $count($comps[purl][$arrayContains(['maven', 'npm', 'pypi', 'golang', 'cargo', 'gem', 'nuget', 'composer', 'pub', 'helm', 'clojars'], $substringBefore($substringAfter(purl, 'pkg:'), '/'))]) > 0 and $total > 0 and $missing / $total > 0.2
|
|
96
|
+
? { "total": $total, "missingVersionCount": $missing, "missingVersionPercent": $round(100 * $missing / $total, 1) }
|
|
97
|
+
: undefined
|
|
98
|
+
)
|
|
99
|
+
location: |
|
|
100
|
+
{ "componentCount": total }
|
|
101
|
+
message: "{{ missingVersionCount }} of {{ total }} components ({{ missingVersionPercent }}%) have no version, so the BOM cannot represent a reproducible dependency set"
|
|
102
|
+
mitigation: "Generate the BOM from a lockfile or a completed install so every component carries a pinned version"
|
|
103
|
+
evidence: |
|
|
104
|
+
{
|
|
105
|
+
"total": total,
|
|
106
|
+
"missingVersionCount": missingVersionCount,
|
|
107
|
+
"missingVersionPercent": missingVersionPercent
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
- id: BF-GEN-003
|
|
111
|
+
name: "More than 10% of components carry no purl"
|
|
112
|
+
description: >-
|
|
113
|
+
Components of a package type that always has a purl are emitted without
|
|
114
|
+
one, which indicates the scanner guessed at them from the filesystem
|
|
115
|
+
instead of reading a manifest.
|
|
116
|
+
severity: medium
|
|
117
|
+
category: build-fidelity
|
|
118
|
+
dry-run-support: full
|
|
119
|
+
tier-signal: heuristic
|
|
120
|
+
applies-to: [maven, npm, pypi, golang, cargo, gem, nuget, composer, pub, helm, clojars]
|
|
121
|
+
condition: |
|
|
122
|
+
(
|
|
123
|
+
$comps := $exists(components) ? components : [];
|
|
124
|
+
$total := $count($comps);
|
|
125
|
+
$missing := $count($comps[$not($exists(purl)) and $not($arrayContains(['cryptographic-asset', 'file', 'data'], type))]);
|
|
126
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
127
|
+
$notContainer and $count($comps[purl][$arrayContains(['maven', 'npm', 'pypi', 'golang', 'cargo', 'gem', 'nuget', 'composer', 'pub', 'helm', 'clojars'], $substringBefore($substringAfter(purl, 'pkg:'), '/'))]) > 0 and $total > 0 and $missing / $total > 0.1
|
|
128
|
+
? { "total": $total, "missingPurlCount": $missing, "missingPurlPercent": $round(100 * $missing / $total, 1) }
|
|
129
|
+
: undefined
|
|
130
|
+
)
|
|
131
|
+
location: |
|
|
132
|
+
{ "componentCount": total }
|
|
133
|
+
message: "{{ missingPurlCount }} of {{ total }} components ({{ missingPurlPercent }}%) have no package URL"
|
|
134
|
+
mitigation: "Regenerate the BOM from the project manifest or lockfile so components carry resolvable purls"
|
|
135
|
+
evidence: |
|
|
136
|
+
{
|
|
137
|
+
"total": total,
|
|
138
|
+
"missingPurlCount": missingPurlCount,
|
|
139
|
+
"missingPurlPercent": missingPurlPercent
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
- id: BF-GEN-004
|
|
143
|
+
name: "No component carries a hash or identity evidence in a hash-bearing ecosystem"
|
|
144
|
+
description: >-
|
|
145
|
+
npm lockfiles record integrity strings and Cargo.lock records checksums,
|
|
146
|
+
so a BOM from those ecosystems where not a single component carries a hash
|
|
147
|
+
or identity evidence was produced without reading the lockfile's
|
|
148
|
+
integrity data. Other ecosystems (maven, pypi, gem, composer) legitimately
|
|
149
|
+
produce hashless BOMs even from a fully resolved graph, so they are out of
|
|
150
|
+
scope for this rule.
|
|
151
|
+
severity: low
|
|
152
|
+
category: build-fidelity
|
|
153
|
+
dry-run-support: full
|
|
154
|
+
tier-signal: heuristic
|
|
155
|
+
applies-to: [npm, cargo]
|
|
156
|
+
condition: |
|
|
157
|
+
(
|
|
158
|
+
$types := $distinct(components[purl].$substringBefore($substringAfter(purl, 'pkg:'), '/'));
|
|
159
|
+
$hashEco := $count($types[$arrayContains(['npm', 'cargo'], $)]) > 0;
|
|
160
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
161
|
+
$notContainer and $hashEco and $count(components) > 0 and $count(components[hashes]) = 0 and $count(components[evidence.identity]) = 0
|
|
162
|
+
? { "componentCount": $count(components) }
|
|
163
|
+
: undefined
|
|
164
|
+
)
|
|
165
|
+
location: |
|
|
166
|
+
{ "componentCount": componentCount }
|
|
167
|
+
message: "None of the {{ componentCount }} components carries a hashes[] entry or identity evidence, so package integrity was not captured"
|
|
168
|
+
mitigation: "Scan the project with its lockfile present (package-lock.json, Cargo.lock) so integrity hashes flow into the BOM"
|
|
169
|
+
evidence: |
|
|
170
|
+
{
|
|
171
|
+
"componentCount": componentCount
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
- id: BF-GEN-005
|
|
175
|
+
name: "Post-build lifecycle claimed without any component evidence"
|
|
176
|
+
description: >-
|
|
177
|
+
The BOM claims a post-build lifecycle phase, but no component carries
|
|
178
|
+
evidence, so the claim is not backed by the observed artifacts a
|
|
179
|
+
post-build scan would have collected.
|
|
180
|
+
severity: low
|
|
181
|
+
category: build-fidelity
|
|
182
|
+
dry-run-support: full
|
|
183
|
+
tier-signal: manifest
|
|
184
|
+
condition: |
|
|
185
|
+
(
|
|
186
|
+
$postBuild := $count(metadata.lifecycles[phase = 'post-build']) > 0;
|
|
187
|
+
$postBuild and $count(components) > 0 and $count(components[evidence]) = 0
|
|
188
|
+
? { "componentCount": $count(components) }
|
|
189
|
+
: undefined
|
|
190
|
+
)
|
|
191
|
+
location: |
|
|
192
|
+
{ "componentCount": componentCount }
|
|
193
|
+
message: "The BOM declares a post-build lifecycle for {{ componentCount }} components but none carries evidence"
|
|
194
|
+
mitigation: "Re-run cdxgen after the project build so post-build components carry identity and occurrence evidence"
|
|
195
|
+
evidence: |
|
|
196
|
+
{
|
|
197
|
+
"componentCount": componentCount
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
- id: BF-GEN-006
|
|
201
|
+
name: "Supported ecosystem produced no components at all"
|
|
202
|
+
description: >-
|
|
203
|
+
The BOM's parent component is a known package ecosystem yet the
|
|
204
|
+
components array is empty. A parent purl is required so that BOMs from
|
|
205
|
+
ecosystems cdxgen cannot parse (zero components and an untyped parent)
|
|
206
|
+
stay silent; those are coverage gaps, not fidelity findings.
|
|
207
|
+
severity: high
|
|
208
|
+
category: build-fidelity
|
|
209
|
+
dry-run-support: full
|
|
210
|
+
tier-signal: absent
|
|
211
|
+
condition: |
|
|
212
|
+
(
|
|
213
|
+
$parentType := $substringBefore($substringAfter(metadata.component.purl, 'pkg:'), '/');
|
|
214
|
+
$count(components) = 0 and $arrayContains(['maven', 'npm', 'pypi', 'golang', 'cargo', 'gem', 'nuget', 'composer', 'pub', 'helm', 'clojars', 'cocoapods', 'swift'], $parentType)
|
|
215
|
+
? { "parentPurlType": $parentType }
|
|
216
|
+
: undefined
|
|
217
|
+
)
|
|
218
|
+
location: |
|
|
219
|
+
{ "parentPurlType": parentPurlType }
|
|
220
|
+
message: "The scan identified a {{ parentPurlType }} project but produced no components"
|
|
221
|
+
mitigation: "Install the ecosystem's build tool or use the cdxgen container image for that toolchain, then re-scan"
|
|
222
|
+
evidence: |
|
|
223
|
+
{
|
|
224
|
+
"parentPurlType": parentPurlType
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
- id: BF-GEN-007
|
|
228
|
+
name: "Dependency graph has no depth"
|
|
229
|
+
description: >-
|
|
230
|
+
With ten or more dependency nodes, at most one node carries dependsOn
|
|
231
|
+
entries, so the graph is a flat list of direct references with the
|
|
232
|
+
transitive structure lost. Small graphs (fewer than ten nodes) are
|
|
233
|
+
legitimately shallow and stay silent.
|
|
234
|
+
severity: medium
|
|
235
|
+
category: build-fidelity
|
|
236
|
+
dry-run-support: full
|
|
237
|
+
tier-signal: manifest
|
|
238
|
+
applies-to: [maven, npm, pypi, golang, cargo, gem, nuget, composer, pub, helm, clojars]
|
|
239
|
+
condition: |
|
|
240
|
+
(
|
|
241
|
+
$deps := $exists(dependencies) ? dependencies : [];
|
|
242
|
+
$branching := $count($deps[$exists(dependsOn) and $count(dependsOn) > 0]);
|
|
243
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
244
|
+
$notContainer and $count(components[purl][$arrayContains(['maven', 'npm', 'pypi', 'golang', 'cargo', 'gem', 'nuget', 'composer', 'pub', 'helm', 'clojars'], $substringBefore($substringAfter(purl, 'pkg:'), '/'))]) > 0 and $count($deps) >= 10 and $branching <= 1
|
|
245
|
+
? { "dependencyCount": $count($deps), "nodesWithEdges": $branching }
|
|
246
|
+
: undefined
|
|
247
|
+
)
|
|
248
|
+
location: |
|
|
249
|
+
{ "dependencyCount": dependencyCount }
|
|
250
|
+
message: "{{ dependencyCount }} dependency nodes exist but {{ nodesWithEdges }} carry dependsOn entries, so the transitive graph is missing"
|
|
251
|
+
mitigation: "Run the ecosystem resolver (mvn dependency:tree, go mod graph, cargo metadata and similar) before the scan so transitive edges are recorded"
|
|
252
|
+
evidence: |
|
|
253
|
+
{
|
|
254
|
+
"dependencyCount": dependencyCount,
|
|
255
|
+
"nodesWithEdges": nodesWithEdges
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
- id: BF-JVM-001
|
|
259
|
+
name: "Maven components with no dependency entries"
|
|
260
|
+
description: >-
|
|
261
|
+
pkg:maven components exist but no dependency graph node references any of
|
|
262
|
+
them, which is the pom.xml fallback shape: only declared dependencies were
|
|
263
|
+
captured and the resolver never ran.
|
|
264
|
+
severity: high
|
|
265
|
+
category: build-fidelity
|
|
266
|
+
dry-run-support: full
|
|
267
|
+
tier-signal: manifest
|
|
268
|
+
applies-to: [maven]
|
|
269
|
+
condition: |
|
|
270
|
+
(
|
|
271
|
+
$mvnRefs := components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'maven'].$."bom-ref";
|
|
272
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
273
|
+
$notContainer and $count($mvnRefs[$exists($)]) > 0 and $count(dependencies) = 0
|
|
274
|
+
? { "mavenComponentCount": $count($mvnRefs[$exists($)]) }
|
|
275
|
+
: undefined
|
|
276
|
+
)
|
|
277
|
+
location: |
|
|
278
|
+
{ "mavenComponentCount": mavenComponentCount }
|
|
279
|
+
message: "{{ mavenComponentCount }} pkg:maven components were captured with no dependency graph, so only declared dependencies are present"
|
|
280
|
+
mitigation: "Build the project with `mvn package -Dmaven.test.skip=true` (or `gradle build`) using the correct JDK before invoking cdxgen so the dependency tree is resolved"
|
|
281
|
+
evidence: |
|
|
282
|
+
{
|
|
283
|
+
"mavenComponentCount": mavenComponentCount
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
- id: BF-JVM-002
|
|
287
|
+
name: "Maven dependency graph is thinner than the component set"
|
|
288
|
+
description: >-
|
|
289
|
+
A dependency graph exists for pkg:maven components but carries fewer than
|
|
290
|
+
half as many edges as there are maven components, which indicates the
|
|
291
|
+
tree stopped early or fell back partway. Fully resolved trees measured on
|
|
292
|
+
healthy projects have edges >= maven components (java-sec-code: 204/204);
|
|
293
|
+
the half-margin keeps small deep trees silent.
|
|
294
|
+
severity: medium
|
|
295
|
+
category: build-fidelity
|
|
296
|
+
dry-run-support: full
|
|
297
|
+
tier-signal: manifest
|
|
298
|
+
applies-to: [maven]
|
|
299
|
+
condition: |
|
|
300
|
+
(
|
|
301
|
+
$mvnCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'maven']);
|
|
302
|
+
$edges := $sum($append(0, dependencies[$exists(dependsOn)].$count(dependsOn)));
|
|
303
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
304
|
+
$notContainer and $mvnCount > 0 and $count(dependencies) > 0 and $edges * 2 < $mvnCount
|
|
305
|
+
? { "mavenComponentCount": $mvnCount, "edgeCount": $edges }
|
|
306
|
+
: undefined
|
|
307
|
+
)
|
|
308
|
+
location: |
|
|
309
|
+
{ "mavenComponentCount": mavenComponentCount, "edgeCount": edgeCount }
|
|
310
|
+
message: "The dependency graph holds {{ edgeCount }} edges for {{ mavenComponentCount }} pkg:maven components, so transitive resolution stopped early"
|
|
311
|
+
mitigation: "Re-run the scan after a successful `mvn dependency:tree` or `gradle dependencies` on the same project"
|
|
312
|
+
evidence: |
|
|
313
|
+
{
|
|
314
|
+
"mavenComponentCount": mavenComponentCount,
|
|
315
|
+
"edgeCount": edgeCount
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
- id: BF-JVM-003
|
|
319
|
+
name: "Maven BOM built from jar scanning only"
|
|
320
|
+
description: >-
|
|
321
|
+
Almost every component is a pkg:maven library, yet nothing carries
|
|
322
|
+
identity evidence or hashes and no dependency graph exists - the shape of
|
|
323
|
+
a BOM assembled from jars found on disk rather than from any manifest or
|
|
324
|
+
resolver output.
|
|
325
|
+
severity: medium
|
|
326
|
+
category: build-fidelity
|
|
327
|
+
dry-run-support: full
|
|
328
|
+
tier-signal: heuristic
|
|
329
|
+
applies-to: [maven]
|
|
330
|
+
condition: |
|
|
331
|
+
(
|
|
332
|
+
$mvnCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'maven']);
|
|
333
|
+
$total := $count(components);
|
|
334
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
335
|
+
$notContainer and $total > 0 and $mvnCount / $total >= 0.9 and $count(dependencies) = 0 and $count(components[evidence.identity]) = 0 and $count(components[hashes]) = 0
|
|
336
|
+
? { "mavenComponentCount": $mvnCount, "total": $total }
|
|
337
|
+
: undefined
|
|
338
|
+
)
|
|
339
|
+
location: |
|
|
340
|
+
{ "mavenComponentCount": mavenComponentCount, "total": total }
|
|
341
|
+
message: "{{ mavenComponentCount }} of {{ total }} components are pkg:maven libraries with no evidence, hashes, or dependency graph, which matches jar scanning rather than a build"
|
|
342
|
+
mitigation: "Run the build inside the project (mvn package, gradle build) and re-scan so components come from the resolver with evidence attached"
|
|
343
|
+
evidence: |
|
|
344
|
+
{
|
|
345
|
+
"mavenComponentCount": mavenComponentCount,
|
|
346
|
+
"total": total
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
- id: BF-JVM-004
|
|
350
|
+
name: "Gradle project produced no Maven components"
|
|
351
|
+
description: >-
|
|
352
|
+
A component carries the cdx:gradle:GradleRootPath marker, which proves a
|
|
353
|
+
Gradle project was detected, yet the BOM holds no pkg:maven components at
|
|
354
|
+
all - Gradle was seen but never produced its dependency set. The
|
|
355
|
+
filesystem-level counterpart (build.gradle present, zero components
|
|
356
|
+
emitted) needs disk access and belongs to the reflection step.
|
|
357
|
+
severity: medium
|
|
358
|
+
category: build-fidelity
|
|
359
|
+
dry-run-support: full
|
|
360
|
+
tier-signal: absent
|
|
361
|
+
applies-to: [maven]
|
|
362
|
+
condition: |
|
|
363
|
+
(
|
|
364
|
+
$gradleMarked := $count(components[$hasProp($, 'cdx:gradle:GradleRootPath')]) > 0 or $hasProp(metadata.component, 'cdx:gradle:GradleRootPath');
|
|
365
|
+
$gradleMarked and $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'maven']) = 0
|
|
366
|
+
? { "componentCount": $count(components) }
|
|
367
|
+
: undefined
|
|
368
|
+
)
|
|
369
|
+
location: |
|
|
370
|
+
{ "componentCount": componentCount }
|
|
371
|
+
message: "A Gradle project was detected but the BOM carries no pkg:maven components"
|
|
372
|
+
mitigation: "Run `gradle build` or let cdxgen provision the Gradle toolchain, then re-scan so the resolved dependency set is captured"
|
|
373
|
+
evidence: |
|
|
374
|
+
{
|
|
375
|
+
"componentCount": componentCount
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
- id: BF-JS-001
|
|
379
|
+
name: "npm components without integrity hashes"
|
|
380
|
+
description: >-
|
|
381
|
+
pkg:npm components exist but none carries a hashes[] entry, which means
|
|
382
|
+
the lockfile integrity data (package-lock.json, pnpm-lock.yaml, yarn.lock)
|
|
383
|
+
was not read - a package.json-only or node_modules-less scan shape.
|
|
384
|
+
severity: medium
|
|
385
|
+
category: build-fidelity
|
|
386
|
+
dry-run-support: full
|
|
387
|
+
tier-signal: manifest
|
|
388
|
+
applies-to: [npm]
|
|
389
|
+
condition: |
|
|
390
|
+
(
|
|
391
|
+
$npm := components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'npm'];
|
|
392
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
393
|
+
$notContainer and $count($npm) > 0 and $count($npm[hashes]) = 0
|
|
394
|
+
? { "npmComponentCount": $count($npm) }
|
|
395
|
+
: undefined
|
|
396
|
+
)
|
|
397
|
+
location: |
|
|
398
|
+
{ "npmComponentCount": npmComponentCount }
|
|
399
|
+
message: "{{ npmComponentCount }} pkg:npm components carry no integrity hash, so the lockfile was not used"
|
|
400
|
+
mitigation: "Commit and generate from a lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock) so integrity data flows into hashes[]"
|
|
401
|
+
evidence: |
|
|
402
|
+
{
|
|
403
|
+
"npmComponentCount": npmComponentCount
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
- id: BF-JS-002
|
|
407
|
+
name: "npm versions are ranges, not pins"
|
|
408
|
+
description: >-
|
|
409
|
+
More than half of the pkg:npm components carry a range-looking version
|
|
410
|
+
(^, ~, >, or *), which is the manifest shape rather than the lockfile
|
|
411
|
+
shape; the exact installed versions are unknown.
|
|
412
|
+
severity: high
|
|
413
|
+
category: build-fidelity
|
|
414
|
+
dry-run-support: full
|
|
415
|
+
tier-signal: manifest
|
|
416
|
+
applies-to: [npm]
|
|
417
|
+
condition: |
|
|
418
|
+
(
|
|
419
|
+
$npm := components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'npm'];
|
|
420
|
+
$ranges := $count($npm[$startsWith($string(version), '^') or $startsWith($string(version), '~') or $startsWith($string(version), '>') or $string(version) = '*']);
|
|
421
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
422
|
+
$notContainer and $count($npm) > 0 and $ranges / $count($npm) > 0.5
|
|
423
|
+
? { "npmComponentCount": $count($npm), "rangeVersionCount": $ranges, "rangeVersionPercent": $round(100 * $ranges / $count($npm), 1) }
|
|
424
|
+
: undefined
|
|
425
|
+
)
|
|
426
|
+
location: |
|
|
427
|
+
{ "npmComponentCount": npmComponentCount }
|
|
428
|
+
message: "{{ rangeVersionCount }} of {{ npmComponentCount }} pkg:npm components ({{ rangeVersionPercent }}%) carry range versions instead of pinned ones"
|
|
429
|
+
mitigation: "Run the package manager install to produce a lockfile, then re-scan so components carry the resolved versions"
|
|
430
|
+
evidence: |
|
|
431
|
+
{
|
|
432
|
+
"npmComponentCount": npmComponentCount,
|
|
433
|
+
"rangeVersionCount": rangeVersionCount,
|
|
434
|
+
"rangeVersionPercent": rangeVersionPercent
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
- id: BF-JS-003
|
|
438
|
+
name: "Workspace marker with a single package's graph"
|
|
439
|
+
description: >-
|
|
440
|
+
The BOM carries the cdx:npm:isWorkspace marker proving a monorepo was
|
|
441
|
+
detected, yet the dependency graph holds at most one node, so only one
|
|
442
|
+
package's dependencies made it into the BOM.
|
|
443
|
+
severity: medium
|
|
444
|
+
category: build-fidelity
|
|
445
|
+
dry-run-support: full
|
|
446
|
+
tier-signal: manifest
|
|
447
|
+
applies-to: [npm]
|
|
448
|
+
condition: |
|
|
449
|
+
(
|
|
450
|
+
$workspace := $prop(metadata.component, 'cdx:npm:isWorkspace') = 'true' or $count(components[$prop($, 'cdx:npm:isWorkspace') = 'true']) > 0;
|
|
451
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
452
|
+
$notContainer and $workspace and $count(components) > 0 and $count(dependencies) <= 1
|
|
453
|
+
? { "dependencyCount": $count(dependencies) }
|
|
454
|
+
: undefined
|
|
455
|
+
)
|
|
456
|
+
location: |
|
|
457
|
+
{ "dependencyCount": dependencyCount }
|
|
458
|
+
message: "A workspace was detected but only {{ dependencyCount }} dependency node is present, so most workspace packages are missing their graphs"
|
|
459
|
+
mitigation: "Run the workspace install (npm/pnpm/yarn install at the repo root) before scanning so every workspace package resolves"
|
|
460
|
+
evidence: |
|
|
461
|
+
{
|
|
462
|
+
"dependencyCount": dependencyCount
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
- id: BF-PY-001
|
|
466
|
+
name: "PyPI components with no dependency entries"
|
|
467
|
+
description: >-
|
|
468
|
+
pkg:pypi components exist but no dependency graph node references any of
|
|
469
|
+
them, which is the requirements.txt shape: declared dependencies without
|
|
470
|
+
resolution (pipenv's Pipfile.lock parse produces the same shape).
|
|
471
|
+
severity: high
|
|
472
|
+
category: build-fidelity
|
|
473
|
+
dry-run-support: full
|
|
474
|
+
tier-signal: manifest
|
|
475
|
+
applies-to: [pypi]
|
|
476
|
+
condition: |
|
|
477
|
+
(
|
|
478
|
+
$py := components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'pypi'].$."bom-ref";
|
|
479
|
+
$refs := $exists(dependencies) ? dependencies.ref : [];
|
|
480
|
+
$targets := $exists(dependencies) ? dependencies.dependsOn : [];
|
|
481
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
482
|
+
$notContainer and $count($py[$exists($)]) > 0 and $count($py[$listContains($refs, $) or $listContains($targets, $)]) = 0
|
|
483
|
+
? { "pypiComponentCount": $count($py[$exists($)]) }
|
|
484
|
+
: undefined
|
|
485
|
+
)
|
|
486
|
+
location: |
|
|
487
|
+
{ "pypiComponentCount": pypiComponentCount }
|
|
488
|
+
message: "{{ pypiComponentCount }} pkg:pypi components were captured with no dependency graph, so the requirements were read without resolution"
|
|
489
|
+
mitigation: "Generate the BOM from a resolved environment or a poetry/uv lockfile so transitive dependencies are captured"
|
|
490
|
+
evidence: |
|
|
491
|
+
{
|
|
492
|
+
"pypiComponentCount": pypiComponentCount
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
- id: BF-PY-002
|
|
496
|
+
name: "Bare PyPI components with no provenance"
|
|
497
|
+
description: >-
|
|
498
|
+
pkg:pypi components exist with no hashes, no dependency graph, and no
|
|
499
|
+
cdx:pypi:* provenance property at all - the shape of a parse that found
|
|
500
|
+
package names but recorded nothing else about them. Poetry lockfile parses
|
|
501
|
+
legitimately carry no hashes but do build a graph, and requirements parses
|
|
502
|
+
record cdx:pypi:versionSpecifiers, so both stay silent here.
|
|
503
|
+
severity: medium
|
|
504
|
+
category: build-fidelity
|
|
505
|
+
dry-run-support: full
|
|
506
|
+
tier-signal: manifest
|
|
507
|
+
applies-to: [pypi]
|
|
508
|
+
condition: |
|
|
509
|
+
(
|
|
510
|
+
$py := components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'pypi'];
|
|
511
|
+
$withProvenance := $count($py[$exists(properties[$startsWith(name, 'cdx:pypi:')])]);
|
|
512
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
513
|
+
$notContainer and $count($py) > 0 and $withProvenance = 0 and $count(dependencies) = 0 and $count(components[hashes]) = 0
|
|
514
|
+
? { "pypiComponentCount": $count($py) }
|
|
515
|
+
: undefined
|
|
516
|
+
)
|
|
517
|
+
location: |
|
|
518
|
+
{ "pypiComponentCount": pypiComponentCount }
|
|
519
|
+
message: "{{ pypiComponentCount }} pkg:pypi components carry no hashes, no graph position, and no provenance properties"
|
|
520
|
+
mitigation: "Regenerate the BOM from a resolved environment (pip freeze, poetry lock, uv lock) so provenance and graph position are captured"
|
|
521
|
+
evidence: |
|
|
522
|
+
{
|
|
523
|
+
"pypiComponentCount": pypiComponentCount
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
- id: BF-PY-003
|
|
527
|
+
name: "PyPI versions are ranges or absent"
|
|
528
|
+
description: >-
|
|
529
|
+
More than 20% of pkg:pypi components carry a range-looking version
|
|
530
|
+
(^, ~, >, <, *) or none, so the BOM does not pin what was installed.
|
|
531
|
+
severity: medium
|
|
532
|
+
category: build-fidelity
|
|
533
|
+
dry-run-support: full
|
|
534
|
+
tier-signal: heuristic
|
|
535
|
+
applies-to: [pypi]
|
|
536
|
+
condition: |
|
|
537
|
+
(
|
|
538
|
+
$py := components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'pypi'];
|
|
539
|
+
$unpinned := $count($py[$not($exists(version)) or $startsWith($string(version), '^') or $startsWith($string(version), '~') or $startsWith($string(version), '>') or $startsWith($string(version), '<') or $string(version) = '*']);
|
|
540
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
541
|
+
$notContainer and $count($py) > 0 and $unpinned / $count($py) > 0.2
|
|
542
|
+
? { "pypiComponentCount": $count($py), "unpinnedCount": $unpinned, "unpinnedPercent": $round(100 * $unpinned / $count($py), 1) }
|
|
543
|
+
: undefined
|
|
544
|
+
)
|
|
545
|
+
location: |
|
|
546
|
+
{ "pypiComponentCount": pypiComponentCount }
|
|
547
|
+
message: "{{ unpinnedCount }} of {{ pypiComponentCount }} pkg:pypi components ({{ unpinnedPercent }}%) have unpinned versions"
|
|
548
|
+
mitigation: "Generate the BOM from a lockfile or a frozen environment so versions are exact"
|
|
549
|
+
evidence: |
|
|
550
|
+
{
|
|
551
|
+
"pypiComponentCount": pypiComponentCount,
|
|
552
|
+
"unpinnedCount": unpinnedCount,
|
|
553
|
+
"unpinnedPercent": unpinnedPercent
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
- id: BF-GO-001
|
|
557
|
+
name: "Go components in a flat graph"
|
|
558
|
+
description: >-
|
|
559
|
+
pkg:golang components exist with dependency nodes but none carries
|
|
560
|
+
dependsOn entries, which is the go.mod shape: declared module requires
|
|
561
|
+
without the resolved module graph.
|
|
562
|
+
severity: medium
|
|
563
|
+
category: build-fidelity
|
|
564
|
+
dry-run-support: full
|
|
565
|
+
tier-signal: manifest
|
|
566
|
+
applies-to: [golang]
|
|
567
|
+
condition: |
|
|
568
|
+
(
|
|
569
|
+
$goCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'golang']);
|
|
570
|
+
$branching := $count(dependencies[$exists(dependsOn) and $count(dependsOn) > 0]);
|
|
571
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
572
|
+
$notContainer and $goCount > 0 and $count(dependencies) > 0 and $branching = 0
|
|
573
|
+
? { "goComponentCount": $goCount, "dependencyCount": $count(dependencies) }
|
|
574
|
+
: undefined
|
|
575
|
+
)
|
|
576
|
+
location: |
|
|
577
|
+
{ "goComponentCount": goComponentCount, "dependencyCount": dependencyCount }
|
|
578
|
+
message: "{{ goComponentCount }} pkg:golang components sit in {{ dependencyCount }} dependency nodes with no edges, so the module graph was not resolved"
|
|
579
|
+
mitigation: "Run `go mod download` (or build the project) before scanning so the resolved module graph is captured"
|
|
580
|
+
evidence: |
|
|
581
|
+
{
|
|
582
|
+
"goComponentCount": goComponentCount,
|
|
583
|
+
"dependencyCount": dependencyCount
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
- id: BF-RB-001
|
|
587
|
+
name: "Ruby gems in a flat graph"
|
|
588
|
+
description: >-
|
|
589
|
+
pkg:gem components exist with dependency nodes but none carries dependsOn
|
|
590
|
+
entries, which is a declared-gems list rather than the Bundler-resolved
|
|
591
|
+
definition.
|
|
592
|
+
severity: medium
|
|
593
|
+
category: build-fidelity
|
|
594
|
+
dry-run-support: full
|
|
595
|
+
tier-signal: manifest
|
|
596
|
+
applies-to: [gem]
|
|
597
|
+
condition: |
|
|
598
|
+
(
|
|
599
|
+
$gemCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'gem']);
|
|
600
|
+
$branching := $count(dependencies[$exists(dependsOn) and $count(dependsOn) > 0]);
|
|
601
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
602
|
+
$notContainer and $gemCount > 0 and $count(dependencies) > 0 and $branching = 0
|
|
603
|
+
? { "gemComponentCount": $gemCount, "dependencyCount": $count(dependencies) }
|
|
604
|
+
: undefined
|
|
605
|
+
)
|
|
606
|
+
location: |
|
|
607
|
+
{ "gemComponentCount": gemComponentCount, "dependencyCount": dependencyCount }
|
|
608
|
+
message: "{{ gemComponentCount }} pkg:gem components sit in {{ dependencyCount }} dependency nodes with no edges, so the bundle was not resolved"
|
|
609
|
+
mitigation: "Run `bundle install` (or `bundle lock`) before scanning so the resolved Gemfile.lock definition is captured"
|
|
610
|
+
evidence: |
|
|
611
|
+
{
|
|
612
|
+
"gemComponentCount": gemComponentCount,
|
|
613
|
+
"dependencyCount": dependencyCount
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
- id: BF-RS-001
|
|
617
|
+
name: "Cargo components in a flat graph"
|
|
618
|
+
description: >-
|
|
619
|
+
pkg:cargo components exist with dependency nodes but none carries
|
|
620
|
+
dependsOn entries, which is the Cargo.toml shape rather than the resolved
|
|
621
|
+
Cargo.lock graph.
|
|
622
|
+
severity: medium
|
|
623
|
+
category: build-fidelity
|
|
624
|
+
dry-run-support: full
|
|
625
|
+
tier-signal: manifest
|
|
626
|
+
applies-to: [cargo]
|
|
627
|
+
condition: |
|
|
628
|
+
(
|
|
629
|
+
$cargoCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'cargo']);
|
|
630
|
+
$branching := $count(dependencies[$exists(dependsOn) and $count(dependsOn) > 0]);
|
|
631
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
632
|
+
$notContainer and $cargoCount > 0 and $count(dependencies) > 0 and $branching = 0
|
|
633
|
+
? { "cargoComponentCount": $cargoCount, "dependencyCount": $count(dependencies) }
|
|
634
|
+
: undefined
|
|
635
|
+
)
|
|
636
|
+
location: |
|
|
637
|
+
{ "cargoComponentCount": cargoComponentCount, "dependencyCount": dependencyCount }
|
|
638
|
+
message: "{{ cargoComponentCount }} pkg:cargo components sit in {{ dependencyCount }} dependency nodes with no edges, so the crate graph was not resolved"
|
|
639
|
+
mitigation: "Run `cargo metadata` or `cargo build` before scanning so the resolved lockfile graph is captured"
|
|
640
|
+
evidence: |
|
|
641
|
+
{
|
|
642
|
+
"cargoComponentCount": cargoComponentCount,
|
|
643
|
+
"dependencyCount": dependencyCount
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
- id: BF-CS-001
|
|
647
|
+
name: "NuGet components in a flat graph"
|
|
648
|
+
description: >-
|
|
649
|
+
pkg:nuget components exist with dependency nodes but none carries
|
|
650
|
+
dependsOn entries, which is a project-file read rather than the resolved
|
|
651
|
+
dependency graph.
|
|
652
|
+
severity: medium
|
|
653
|
+
category: build-fidelity
|
|
654
|
+
dry-run-support: full
|
|
655
|
+
tier-signal: manifest
|
|
656
|
+
applies-to: [nuget]
|
|
657
|
+
condition: |
|
|
658
|
+
(
|
|
659
|
+
$nugetCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'nuget']);
|
|
660
|
+
$branching := $count(dependencies[$exists(dependsOn) and $count(dependsOn) > 0]);
|
|
661
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
662
|
+
$notContainer and $nugetCount > 0 and $count(dependencies) > 0 and $branching = 0
|
|
663
|
+
? { "nugetComponentCount": $nugetCount, "dependencyCount": $count(dependencies) }
|
|
664
|
+
: undefined
|
|
665
|
+
)
|
|
666
|
+
location: |
|
|
667
|
+
{ "nugetComponentCount": nugetComponentCount, "dependencyCount": dependencyCount }
|
|
668
|
+
message: "{{ nugetComponentCount }} pkg:nuget components sit in {{ dependencyCount }} dependency nodes with no edges, so the package graph was not restored"
|
|
669
|
+
mitigation: "Run `dotnet restore` (or build the solution) before scanning so the resolved package graph is captured"
|
|
670
|
+
evidence: |
|
|
671
|
+
{
|
|
672
|
+
"nugetComponentCount": nugetComponentCount,
|
|
673
|
+
"dependencyCount": dependencyCount
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
- id: BF-SWIFT-001
|
|
677
|
+
name: "Swift packages with no dependency graph"
|
|
678
|
+
description: >-
|
|
679
|
+
pkg:swift components exist but the BOM holds no dependency nodes at all.
|
|
680
|
+
cdxgen parses Package.resolved but never builds a Swift graph, so this
|
|
681
|
+
finding reports the ecosystem's ceiling; the reflection step classifies
|
|
682
|
+
it as at-ceiling rather than asking anyone to fix it.
|
|
683
|
+
severity: low
|
|
684
|
+
category: build-fidelity
|
|
685
|
+
dry-run-support: full
|
|
686
|
+
tier-signal: lockfile
|
|
687
|
+
applies-to: [swift]
|
|
688
|
+
condition: |
|
|
689
|
+
(
|
|
690
|
+
$swiftCount := $count(components[purl][$substringBefore($substringAfter(purl, 'pkg:'), '/') = 'swift']);
|
|
691
|
+
$notContainer := $not($arrayContains(['container', 'operating-system', 'device'], $string(metadata.component.type)));
|
|
692
|
+
$notContainer and $swiftCount > 0 and $count(dependencies) = 0
|
|
693
|
+
? { "swiftComponentCount": $swiftCount }
|
|
694
|
+
: undefined
|
|
695
|
+
)
|
|
696
|
+
location: |
|
|
697
|
+
{ "swiftComponentCount": swiftComponentCount }
|
|
698
|
+
message: "{{ swiftComponentCount }} pkg:swift components were captured from the package resolved list with no dependency graph"
|
|
699
|
+
mitigation: "No action resolves this today; cdxgen does not build a Swift dependency graph. Treat the BOM as a package list"
|
|
700
|
+
evidence: |
|
|
701
|
+
{
|
|
702
|
+
"swiftComponentCount": swiftComponentCount
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
# Formulation-derived rules. Both gate on $formulationOrigin() = 'foreign':
|
|
706
|
+
# on a same-run scan the formulation's platform components are the ledger's
|
|
707
|
+
# own probes reported twice and its commands were never observed to run, so
|
|
708
|
+
# they must not move a verdict that already has a ledger. On a foreign BOM
|
|
709
|
+
# no ledger observed the run, and the formulation is the only surviving
|
|
710
|
+
# trace of what its build attempted. Both attribute their finding through
|
|
711
|
+
# the parent component's purl type.
|
|
712
|
+
- id: BF-FORM-001
|
|
713
|
+
name: "A resolver command is declared with no dependency graph in the BOM"
|
|
714
|
+
description: >-
|
|
715
|
+
The BOM's formulation declares resolver or build commands in its CI
|
|
716
|
+
workflows, yet dependencies[] carries no edges at all: the resolution the
|
|
717
|
+
project expects produced nothing here. On a foreign BOM this separates
|
|
718
|
+
"no build attempted" from "build attempted and failed", which otherwise
|
|
719
|
+
look identical.
|
|
720
|
+
severity: medium
|
|
721
|
+
category: build-fidelity
|
|
722
|
+
dry-run-support: full
|
|
723
|
+
tier-signal: manifest
|
|
724
|
+
reason-source: formulation
|
|
725
|
+
condition: |
|
|
726
|
+
(
|
|
727
|
+
$commands := $exists(formulation) ? [formulation.workflows.tasks.steps.commands.executed] : [];
|
|
728
|
+
$resolverTokens := ['mvn','mvnw','gradle','gradlew','ant','sbt','npm','pnpm','yarn','bun','deno','npx','pip','pip3','poetry','uv','pdm','pipenv','conda','python','python3','cargo','go','bundle','gem','rake','dotnet','nuget','composer','dart','flutter','helm','lein','clj','swift','pod','mix','cmake','make','meson','cabal','stack'];
|
|
729
|
+
$isResolver := function($cmd) { $lowercase($split($substringBefore($trim($string($cmd)), ' '), '/')[$count($split($substringBefore($trim($string($cmd)), ' '), '/')) - 1]) in $resolverTokens };
|
|
730
|
+
$resolvers := $count($commands[$isResolver($)]);
|
|
731
|
+
$edges := $exists(dependencies) ? $count(dependencies.dependsOn) : 0;
|
|
732
|
+
$formulationOrigin() = 'foreign' and $resolvers > 0 and $edges = 0
|
|
733
|
+
? {
|
|
734
|
+
"resolverCommands": $resolvers,
|
|
735
|
+
"parentPurlType": $exists(metadata.component.purl) ? $substringBefore($substringAfter(metadata.component.purl, 'pkg:'), '/') : undefined
|
|
736
|
+
}
|
|
737
|
+
: undefined
|
|
738
|
+
)
|
|
739
|
+
location: |
|
|
740
|
+
{ "resolverCommands": resolverCommands }
|
|
741
|
+
message: "{{ resolverCommands }} resolver command(s) declared in the BOM's CI workflows, yet the BOM carries no dependency edges — the resolution was attempted and produced nothing here"
|
|
742
|
+
mitigation: "Run the ecosystem resolver this BOM's CI declares so the dependency graph is captured, then re-scan; the declared command is a hypothesis to check, not something this run observed"
|
|
743
|
+
evidence: |
|
|
744
|
+
{
|
|
745
|
+
"resolverCommands": resolverCommands
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
- id: BF-FORM-002
|
|
749
|
+
name: "The BOM's toolchain record does not describe this environment"
|
|
750
|
+
description: >-
|
|
751
|
+
The formulation section records the toolchain the generating run probed,
|
|
752
|
+
and its entry for this runtime's own family carries a version different
|
|
753
|
+
from the runtime re-scanning the BOM. The environment that produced this
|
|
754
|
+
BOM is not the one re-scanning it, so the record's claims cannot be
|
|
755
|
+
verified here without regenerating the BOM locally.
|
|
756
|
+
severity: medium
|
|
757
|
+
category: build-fidelity
|
|
758
|
+
dry-run-support: full
|
|
759
|
+
tier-signal: manifest
|
|
760
|
+
reason-source: formulation
|
|
761
|
+
condition: |
|
|
762
|
+
(
|
|
763
|
+
$mismatch := $formulationOrigin() = 'foreign' ? $formulationRuntimeMismatch() : undefined;
|
|
764
|
+
$exists($mismatch)
|
|
765
|
+
? {
|
|
766
|
+
"recordedTool": $string($mismatch.tool),
|
|
767
|
+
"recordedVersion": $exists($mismatch.recordedVersion) ? $string($mismatch.recordedVersion) : 'none recorded',
|
|
768
|
+
"runtimeVersion": $string($mismatch.runtimeVersion),
|
|
769
|
+
"parentPurlType": $exists(metadata.component.purl) ? $substringBefore($substringAfter(metadata.component.purl, 'pkg:'), '/') : undefined
|
|
770
|
+
}
|
|
771
|
+
: undefined
|
|
772
|
+
)
|
|
773
|
+
location: |
|
|
774
|
+
{ "recordedTool": recordedTool }
|
|
775
|
+
message: "This BOM's formulation records {{ recordedTool }} {{ recordedVersion }}, but this environment runs {{ recordedTool }} {{ runtimeVersion }} — the record describes a different environment (another machine, or the same machine under a different runtime), so the BOM cannot be verified as-is here"
|
|
776
|
+
mitigation: "Re-generate the BOM on this machine so the verdict describes an observed run; install the recorded toolchain first if this environment lacks it"
|
|
777
|
+
evidence: |
|
|
778
|
+
{
|
|
779
|
+
"recordedTool": recordedTool,
|
|
780
|
+
"recordedVersion": recordedVersion,
|
|
781
|
+
"runtimeVersion": runtimeVersion
|
|
782
|
+
}
|