@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -6,16 +6,21 @@ import { join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  import { DEBUG_MODE } from "../../core/activity.js";
9
- import { getTimestamp, safeExistsSync } from "../../core/fs.js";
10
- import { buildAnnotationText } from "../../helpers/annotationFormatter.js";
9
+ import { safeExistsSync } from "../../core/fs.js";
11
10
  import { collectAiOversight } from "../../inventory/aiOversightCollector.js";
12
11
  import { collectAiProvenance } from "../../inventory/aiProvenanceCollector.js";
13
12
  import {
14
13
  expandBomAuditCategories,
14
+ OPT_IN_BOM_AUDIT_CATEGORIES,
15
15
  validateBomAuditCategories,
16
16
  } from "../../inventory/auditCategories.js";
17
17
  import { isHbomLikeBom as isHbomLikeBomDocument } from "../../inventory/hbomAnalysis.js";
18
18
  import { table } from "../../inventory/table.js";
19
+ import {
20
+ buildDocumentAnnotation,
21
+ evidenceProperties,
22
+ warnMissingAnnotator,
23
+ } from "./annotationBuilder.js";
19
24
  import { evaluateRules, loadRules } from "./ruleEngine.js";
20
25
 
21
26
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
@@ -53,6 +58,12 @@ async function loadConfiguredBomAuditRules(options = {}) {
53
58
  );
54
59
  }
55
60
  }
61
+ } else {
62
+ // Without explicit categories every loaded rule would activate, so rules
63
+ // belonging to opt-in-only categories are removed here.
64
+ activeRules = rules.filter(
65
+ (r) => !OPT_IN_BOM_AUDIT_CATEGORIES.includes(r.category),
66
+ );
56
67
  }
57
68
  return {
58
69
  activeRules,
@@ -369,89 +380,75 @@ export function formatAnnotations(findings, bomJson) {
369
380
  if (!findings?.length) {
370
381
  return [];
371
382
  }
372
- const cdxgenAnnotator =
373
- bomJson?.metadata?.tools?.components?.filter((c) => c.name === "cdxgen") ||
374
- [];
375
- if (!cdxgenAnnotator.length) {
376
- if (DEBUG_MODE) {
377
- console.warn(
378
- "Cannot create audit annotations: cdxgen tool component not found in metadata",
379
- );
380
- }
381
- return [];
382
- }
383
- return findings.map((f) => {
384
- const subjects = [bomJson.serialNumber];
385
- const properties = [
386
- { name: "cdx:audit:ruleId", value: f.ruleId },
387
- { name: "cdx:audit:severity", value: f.severity },
388
- { name: "cdx:audit:category", value: f.category },
389
- ];
390
- if (f.name) {
391
- properties.push({ name: "cdx:audit:name", value: f.name });
392
- }
393
- if (f.mitigation) {
394
- properties.push({ name: "cdx:audit:mitigation", value: f.mitigation });
395
- }
396
- if (f.attackTactics?.length) {
397
- properties.push({
398
- name: "cdx:audit:attack:tactics",
399
- value: f.attackTactics.join(","),
400
- });
401
- }
402
- if (f.attackTechniques?.length) {
403
- properties.push({
404
- name: "cdx:audit:attack:techniques",
405
- value: f.attackTechniques.join(","),
406
- });
407
- }
408
- if (f.standards && typeof f.standards === "object") {
409
- for (const [standardName, entries] of Object.entries(f.standards)) {
383
+ let warnedMissingAnnotator = false;
384
+ return findings
385
+ .map((f) => {
386
+ const properties = [
387
+ { name: "cdx:audit:ruleId", value: f.ruleId },
388
+ { name: "cdx:audit:severity", value: f.severity },
389
+ { name: "cdx:audit:category", value: f.category },
390
+ ];
391
+ if (f.name) {
392
+ properties.push({ name: "cdx:audit:name", value: f.name });
393
+ }
394
+ if (f.mitigation) {
395
+ properties.push({ name: "cdx:audit:mitigation", value: f.mitigation });
396
+ }
397
+ if (f.attackTactics?.length) {
410
398
  properties.push({
411
- name: `cdx:audit:standards:${standardName}`,
412
- value: Array.isArray(entries) ? entries.join(",") : String(entries),
399
+ name: "cdx:audit:attack:tactics",
400
+ value: f.attackTactics.join(","),
413
401
  });
414
402
  }
415
- }
416
- if (f?.location?.purl) {
417
- properties.push({
418
- name: "cdx:audit:location:purl",
419
- value: f.location.purl,
420
- });
421
- }
422
- if (f.location?.file) {
423
- properties.push({
424
- name: "cdx:audit:location:file",
425
- value: f.location.file,
426
- });
427
- }
428
- if (f.location?.bomRef) {
429
- properties.push({
430
- name: "cdx:audit:location:bomRef",
431
- value: f.location.bomRef,
432
- });
433
- }
434
- if (f.evidence && typeof f.evidence === "object") {
435
- for (const [key, value] of Object.entries(f.evidence)) {
436
- const propValue =
437
- typeof value === "object" ? JSON.stringify(value) : String(value);
403
+ if (f.attackTechniques?.length) {
438
404
  properties.push({
439
- name: `cdx:audit:evidence:${key}`,
440
- value: propValue,
405
+ name: "cdx:audit:attack:techniques",
406
+ value: f.attackTechniques.join(","),
441
407
  });
442
408
  }
443
- }
444
- return {
445
- subjects,
446
- annotator: {
447
- component: cdxgenAnnotator[0],
448
- },
449
- // An annotation describes the document it is attached to, so it carries
450
- // that document's timestamp rather than the moment the audit ran.
451
- timestamp: bomJson.metadata?.timestamp || getTimestamp(),
452
- text: buildAnnotationText(f.message, properties),
453
- };
454
- });
409
+ if (f.standards && typeof f.standards === "object") {
410
+ for (const [standardName, entries] of Object.entries(f.standards)) {
411
+ properties.push({
412
+ name: `cdx:audit:standards:${standardName}`,
413
+ value: Array.isArray(entries) ? entries.join(",") : String(entries),
414
+ });
415
+ }
416
+ }
417
+ if (f?.location?.purl) {
418
+ properties.push({
419
+ name: "cdx:audit:location:purl",
420
+ value: f.location.purl,
421
+ });
422
+ }
423
+ if (f.location?.file) {
424
+ properties.push({
425
+ name: "cdx:audit:location:file",
426
+ value: f.location.file,
427
+ });
428
+ }
429
+ if (f.location?.bomRef) {
430
+ properties.push({
431
+ name: "cdx:audit:location:bomRef",
432
+ value: f.location.bomRef,
433
+ });
434
+ }
435
+ properties.push(...evidenceProperties("cdx:audit", f.evidence));
436
+ return buildDocumentAnnotation({
437
+ bomJson,
438
+ message: f.message,
439
+ properties,
440
+ });
441
+ })
442
+ .filter((annotation) => {
443
+ if (!annotation) {
444
+ if (!warnedMissingAnnotator) {
445
+ warnMissingAnnotator("audit");
446
+ warnedMissingAnnotator = true;
447
+ }
448
+ return false;
449
+ }
450
+ return true;
451
+ });
455
452
  }
456
453
 
457
454
  /**
@@ -0,0 +1,320 @@
1
+ /**
2
+ * BOM mutations that carry the introspection verdict inside the document:
3
+ * eight metadata properties and a block of document-level annotations.
4
+ *
5
+ * Sidecar report files get lost; the BOM travels. A consumer who receives
6
+ * only the BOM still learns how much to trust it from the metadata properties,
7
+ * and the annotations carry one entry per remediation the loop should act on.
8
+ * No component or dependency is touched.
9
+ *
10
+ * Both mutations are replace-by-name: a document that was already introspected
11
+ * (a BOM enriched again after an evidence pass) loses its previous
12
+ * introspection state and gains a fresh one, so the verdict always describes
13
+ * the final document.
14
+ */
15
+
16
+ import {
17
+ buildDocumentAnnotation,
18
+ evidenceProperties,
19
+ } from "../annotationBuilder.js";
20
+ import { FORMULATION_RUN_ID_PROPERTY } from "./formulationEvidence.js";
21
+ import { overallAssessment } from "./report.js";
22
+
23
+ /**
24
+ * Version of the introspection BOM surface. Matches the JSON report's
25
+ * schemaVersion so the two stay comparable.
26
+ *
27
+ * @type {string}
28
+ */
29
+ export const INTROSPECTION_SCHEMA_VERSION = "1.1";
30
+
31
+ /**
32
+ * Namespace of every property and annotation entry this module emits.
33
+ *
34
+ * @type {string}
35
+ */
36
+ export const INTROSPECTION_PROPERTY_PREFIX = "cdx:introspection";
37
+
38
+ /**
39
+ * The metadata properties that summarize the verdict, in emission order. The
40
+ * overall tier and confidence are omitted when the run produced no scored row
41
+ * (nothing was graded, so no verdict is claimed); per-ecosystem rows cover the
42
+ * scored ecosystems only, since unsupported ones have neither a tier nor a
43
+ * score.
44
+ *
45
+ * @param {Object} reflection Reflection document from reflectOnRun.
46
+ * @param {Object} scored Scoring document from scoreReflection.
47
+ * @returns {{name: string, value: string}[]} Metadata property entries.
48
+ */
49
+ export function introspectionMetadataProperties(reflection, scored) {
50
+ const assessment = overallAssessment(scored);
51
+ const properties = [
52
+ {
53
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:schemaVersion`,
54
+ value: INTROSPECTION_SCHEMA_VERSION,
55
+ },
56
+ ];
57
+ if (assessment.tier) {
58
+ properties.push({
59
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:tier`,
60
+ value: assessment.tier,
61
+ });
62
+ }
63
+ properties.push({
64
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:score`,
65
+ value: String(scored?.overallScore ?? 100),
66
+ });
67
+ if (assessment.confidence) {
68
+ properties.push({
69
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:confidence`,
70
+ value: assessment.confidence,
71
+ });
72
+ }
73
+ properties.push({
74
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:ledgerComplete`,
75
+ value: String(reflection?.ledgerComplete !== false),
76
+ });
77
+ for (const row of sortedScoredRows(scored)) {
78
+ properties.push({
79
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:ecosystem:${row.ecosystem}:tier`,
80
+ value: `${row.tier}`,
81
+ });
82
+ properties.push({
83
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:ecosystem:${row.ecosystem}:score`,
84
+ value: String(row.score),
85
+ });
86
+ }
87
+ properties.push({
88
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:remediationCount`,
89
+ value: String(actionableRemediations(scored).length),
90
+ });
91
+ return properties;
92
+ }
93
+
94
+ /**
95
+ * The annotation describing the run as a whole. Its structured facts are the
96
+ * metadata properties, so the summary is readable without the rest of the
97
+ * document.
98
+ *
99
+ * @param {Object} bomJson CycloneDX BOM the annotation describes.
100
+ * @param {Object} reflection Reflection document from reflectOnRun.
101
+ * @param {Object} scored Scoring document from scoreReflection.
102
+ * @returns {Object|undefined} The summary annotation, or undefined without a cdxgen annotator.
103
+ */
104
+ function summaryAnnotation(bomJson, reflection, scored) {
105
+ const assessment = overallAssessment(scored);
106
+ const remediationCount = (
107
+ Array.isArray(scored?.remediations) ? scored.remediations : []
108
+ ).length;
109
+ // Annotation text is escaped as markdown before it reaches the BOM, so the
110
+ // summary is phrased without brackets or pipes that would survive as
111
+ // backslashes in the rendered document.
112
+ const message =
113
+ `Build introspection: overall tier ${assessment.tier ?? "ungraded"}, ` +
114
+ `score ${scored?.overallScore ?? 100} of 100, ` +
115
+ `confidence ${assessment.confidence ?? "not available"}. ` +
116
+ `${remediationCount} remediation${remediationCount === 1 ? "" : "s"} ranked.`;
117
+ return buildDocumentAnnotation({
118
+ bomJson,
119
+ message,
120
+ properties: introspectionMetadataProperties(reflection, scored),
121
+ });
122
+ }
123
+
124
+ /**
125
+ * One annotation per non-blocked remediation, in ranked order. Blocked
126
+ * entries are constraints of the environment rather than work the loop can
127
+ * do, so they stay in the report files and out of the document. A remediation
128
+ * that subsumes other rules names them, so nothing the run detected goes
129
+ * unreported.
130
+ *
131
+ * @param {Object} bomJson CycloneDX BOM the annotations describe.
132
+ * @param {Object} scored Scoring document from scoreReflection.
133
+ * @returns {Object[]} Remediation annotations.
134
+ */
135
+ function remediationAnnotations(bomJson, scored) {
136
+ const tierByEcosystem = new Map(
137
+ sortedScoredRows(scored).map((row) => [row.ecosystem, row.tier]),
138
+ );
139
+ const annotations = [];
140
+ for (const entry of actionableRemediations(scored)) {
141
+ const properties = [
142
+ {
143
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:remediationId`,
144
+ value: `${entry.remediationId}`,
145
+ },
146
+ {
147
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:ecosystem`,
148
+ value: `${entry.ecosystem}`,
149
+ },
150
+ {
151
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:currentTier`,
152
+ value: `${tierByEcosystem.get(entry.ecosystem) || entry.targetTier}`,
153
+ },
154
+ {
155
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:targetTier`,
156
+ value: `${entry.targetTier}`,
157
+ },
158
+ {
159
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:expectedGain`,
160
+ value: Number(entry.expectedGain ?? 0).toFixed(2),
161
+ },
162
+ {
163
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:confidence`,
164
+ value: `${entry.confidence}`,
165
+ },
166
+ ];
167
+ const rules = Array.isArray(entry.verify?.rules) ? entry.verify.rules : [];
168
+ if (rules.length) {
169
+ properties.push({
170
+ name: `${INTROSPECTION_PROPERTY_PREFIX}:verify:rules`,
171
+ value: rules.join(","),
172
+ });
173
+ }
174
+ const evidence = { source: entry.source };
175
+ if (entry.impact) {
176
+ evidence.impact = entry.impact;
177
+ }
178
+ if (entry.severity) {
179
+ evidence.severity = entry.severity;
180
+ }
181
+ if (Number.isFinite(entry.evidenceCount)) {
182
+ evidence.events = entry.evidenceCount;
183
+ }
184
+ if (Array.isArray(entry.subsumes) && entry.subsumes.length) {
185
+ evidence.subsumes = entry.subsumes;
186
+ }
187
+ if (
188
+ Array.isArray(entry.verify?.eventsCleared) &&
189
+ entry.verify.eventsCleared.length
190
+ ) {
191
+ evidence.eventsCleared = entry.verify.eventsCleared;
192
+ }
193
+ properties.push(
194
+ ...evidenceProperties(INTROSPECTION_PROPERTY_PREFIX, evidence),
195
+ );
196
+ const annotation = buildDocumentAnnotation({
197
+ bomJson,
198
+ message: `${entry.summary || entry.remediationId}`,
199
+ properties,
200
+ });
201
+ if (annotation) {
202
+ annotations.push(annotation);
203
+ }
204
+ }
205
+ return annotations;
206
+ }
207
+
208
+ /**
209
+ * Attach the introspection verdict to the document: metadata properties plus
210
+ * the summary and remediation annotations. Any introspection state left on the
211
+ * document by a previous pass is removed first, so enrichment flows that run
212
+ * post-processing twice leave exactly one verdict.
213
+ *
214
+ * @param {Object} bomJson CycloneDX BOM, mutated in place.
215
+ * @param {Object} reflection Reflection document from reflectOnRun.
216
+ * @param {Object} scored Scoring document from scoreReflection.
217
+ * @param {Object} [guards] Emission guards.
218
+ * @param {boolean} [guards.annotations] False skips the annotations, for documents at a spec version that predates them.
219
+ * @returns {Object} The mutated BOM.
220
+ */
221
+ export function applyIntrospectionToBom(
222
+ bomJson,
223
+ reflection,
224
+ scored,
225
+ guards = {},
226
+ ) {
227
+ if (!bomJson?.metadata) {
228
+ return bomJson;
229
+ }
230
+ const freshProperties = introspectionMetadataProperties(reflection, scored);
231
+ const retainedProperties = (
232
+ Array.isArray(bomJson.metadata.properties)
233
+ ? bomJson.metadata.properties
234
+ : []
235
+ ).filter(
236
+ (property) =>
237
+ !`${property?.name || ""}`.startsWith(INTROSPECTION_PROPERTY_PREFIX),
238
+ );
239
+ bomJson.metadata.properties = [...retainedProperties, ...freshProperties];
240
+ if (guards.annotations === false) {
241
+ return bomJson;
242
+ }
243
+ const retainedAnnotations = (
244
+ Array.isArray(bomJson.annotations) ? bomJson.annotations : []
245
+ ).filter(
246
+ (annotation) =>
247
+ !`${annotation?.text || ""}`.includes(INTROSPECTION_PROPERTY_PREFIX),
248
+ );
249
+ const freshAnnotations = [
250
+ summaryAnnotation(bomJson, reflection, scored),
251
+ ...remediationAnnotations(bomJson, scored),
252
+ ].filter(Boolean);
253
+ if (freshAnnotations.length) {
254
+ bomJson.annotations = [...retainedAnnotations, ...freshAnnotations];
255
+ }
256
+ return bomJson;
257
+ }
258
+
259
+ /** Scored rows in ecosystem order. */
260
+ function sortedScoredRows(scored) {
261
+ return [...(Array.isArray(scored?.ecosystems) ? scored.ecosystems : [])]
262
+ .filter((row) => row?.ecosystem)
263
+ .sort((a, b) => `${a.ecosystem}`.localeCompare(`${b.ecosystem}`));
264
+ }
265
+
266
+ /** Remediations the loop can act on, in ranked order. */
267
+ function actionableRemediations(scored) {
268
+ return (
269
+ Array.isArray(scored?.remediations) ? scored.remediations : []
270
+ ).filter((entry) => entry?.blocked !== true);
271
+ }
272
+
273
+ /**
274
+ * Stamp every formulation workflow with the id of the run that reflected over
275
+ * the document, so a later consumer of the BOM can tell a formulation record
276
+ * this run generated from one that arrived with a foreign BOM. The stamp is
277
+ * replace-by-name, matching how the rest of the verdict mutates the
278
+ * document: a BOM enriched twice keeps only the latest run's id.
279
+ *
280
+ * @param {Object} bomJson CycloneDX BOM, mutated in place.
281
+ * @param {string} runId Run id of the introspection pass.
282
+ * @returns {Object} The mutated BOM.
283
+ */
284
+ export function applyFormulationRunIdMarker(bomJson, runId) {
285
+ if (!bomJson || !runId) {
286
+ return bomJson;
287
+ }
288
+ for (const entry of Array.isArray(bomJson.formulation)
289
+ ? bomJson.formulation
290
+ : []) {
291
+ for (const workflow of Array.isArray(entry?.workflows)
292
+ ? entry.workflows
293
+ : []) {
294
+ const retained = (
295
+ Array.isArray(workflow.properties) ? workflow.properties : []
296
+ ).filter((property) => property?.name !== FORMULATION_RUN_ID_PROPERTY);
297
+ workflow.properties = [
298
+ ...retained,
299
+ { name: FORMULATION_RUN_ID_PROPERTY, value: runId },
300
+ ];
301
+ }
302
+ }
303
+ return bomJson;
304
+ }
305
+
306
+ /**
307
+ * Whether a BOM already carries an introspection verdict.
308
+ *
309
+ * Evidence collection re-processes a finished BOM through a fresh wrapper, so
310
+ * the document itself is the only marker that survives between passes.
311
+ *
312
+ * @param {Object} bomJson CycloneDX BOM.
313
+ * @returns {boolean} True when the BOM was already introspected.
314
+ */
315
+ export function hasIntrospectionVerdict(bomJson) {
316
+ return (bomJson?.metadata?.properties || []).some(
317
+ (property) =>
318
+ property?.name === `${INTROSPECTION_PROPERTY_PREFIX}:schemaVersion`,
319
+ );
320
+ }