@cdxgen/cdxgen 13.0.0 → 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.
Files changed (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. 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
+ }