@cdxgen/cdxgen 0.0.1 → 13.0.1

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 (722) hide show
  1. package/README.md +921 -87
  2. package/bin/audit.js +239 -0
  3. package/bin/cdxgen.js +2335 -0
  4. package/bin/convert.js +220 -0
  5. package/bin/evinse.js +353 -0
  6. package/bin/hbom.js +504 -0
  7. package/bin/repl.js +2224 -0
  8. package/bin/sign.js +195 -0
  9. package/bin/tracebom.js +303 -0
  10. package/bin/validate.js +268 -0
  11. package/bin/verify.js +189 -0
  12. package/data/README.md +307 -0
  13. package/data/bom-1.4.schema.json +1605 -0
  14. package/data/bom-1.5.schema.json +3660 -0
  15. package/data/bom-1.6.schema.json +5511 -0
  16. package/data/bom-1.7.schema.json +6700 -0
  17. package/data/cbomosdb-queries.json +68 -0
  18. package/data/component-tags.json +339 -0
  19. package/data/container-knowledge-index.json +125 -0
  20. package/data/cosdb-queries.json +17 -0
  21. package/data/crypto-oid.json +1998 -0
  22. package/data/cryptography-defs.json +3793 -0
  23. package/data/cryptography-defs.schema.json +576 -0
  24. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  25. package/data/frameworks-list.json +219 -0
  26. package/data/glibc-stdlib.json +117 -0
  27. package/data/gtfobins-index.json +6296 -0
  28. package/data/helpers/init.gradle +71 -0
  29. package/data/jsf-0.82.schema.json +211 -0
  30. package/data/known-licenses.json +82 -0
  31. package/data/lic-mapping.json +338 -0
  32. package/data/license-aliases.json +6624 -0
  33. package/data/license-db.json +39059 -0
  34. package/data/license-deprecations.json +302 -0
  35. package/data/lolbas-index.json +150 -0
  36. package/data/predictive-audit-allowlist.json +11 -0
  37. package/data/pypi-pkg-aliases.json +1199 -0
  38. package/data/python-stdlib.json +308 -0
  39. package/data/queries-darwin.json +193 -0
  40. package/data/queries-win.json +257 -0
  41. package/data/queries.json +268 -0
  42. package/data/ruby-known-modules.json +281 -0
  43. package/data/rules/ai-agent-governance.yaml +323 -0
  44. package/data/rules/ai-bom.yaml +268 -0
  45. package/data/rules/ai-oversight.yaml +186 -0
  46. package/data/rules/ai-provenance.yaml +92 -0
  47. package/data/rules/asar-archives.yaml +150 -0
  48. package/data/rules/cbom-security.yaml +165 -0
  49. package/data/rules/chrome-extensions.yaml +248 -0
  50. package/data/rules/ci-permissions.yaml +802 -0
  51. package/data/rules/container-risk.yaml +353 -0
  52. package/data/rules/dependency-sources.yaml +236 -0
  53. package/data/rules/golem-go.yaml +229 -0
  54. package/data/rules/hbom-compliance.yaml +325 -0
  55. package/data/rules/hbom-performance.yaml +307 -0
  56. package/data/rules/hbom-security.yaml +248 -0
  57. package/data/rules/host-topology.yaml +165 -0
  58. package/data/rules/mcp-servers.yaml +388 -0
  59. package/data/rules/obom-runtime.yaml +1776 -0
  60. package/data/rules/package-integrity.yaml +364 -0
  61. package/data/rules/rootfs-hardening.yaml +179 -0
  62. package/data/rules/vscode-extensions.yaml +237 -0
  63. package/data/spdx-export.schema.json +6794 -0
  64. package/data/spdx-license-list.json +7449 -0
  65. package/data/spdx-licenses.json +759 -0
  66. package/data/spdx.schema.json +819 -0
  67. package/data/templates/README.md +3 -0
  68. package/data/templates/asvs-4.0.3.cdx.json +2943 -0
  69. package/data/templates/asvs-5.0.cdx.json +3046 -0
  70. package/data/templates/bsimm-v13.cdx.json +1008 -0
  71. package/data/templates/masvs-2.0.0.cdx.json +225 -0
  72. package/data/templates/nist-ssdf-1.1.cdx.json +828 -0
  73. package/data/templates/pcissc-secure-slc-1.1.cdx.json +1027 -0
  74. package/data/templates/scvs-1.0.0.cdx.json +813 -0
  75. package/data/templates/ssaf-DRAFT-2023-11.cdx.json +127 -0
  76. package/data/vendor-alias.json +41967 -0
  77. package/data/wrapdb-releases.json +4227 -0
  78. package/index.cjs +15 -0
  79. package/lib/audit/index.js +2800 -0
  80. package/lib/audit/progress.js +114 -0
  81. package/lib/audit/reporters.js +937 -0
  82. package/lib/audit/scoring.js +560 -0
  83. package/lib/audit/targets.js +798 -0
  84. package/lib/cli/bomAssembly.js +1882 -0
  85. package/lib/cli/cliOptions.js +543 -0
  86. package/lib/cli/index.js +2759 -0
  87. package/lib/cli/jsBom.js +2087 -0
  88. package/lib/cli/jvmBom.js +1858 -0
  89. package/lib/cli/managedBom.js +2083 -0
  90. package/lib/cli/nativeBom.js +2125 -0
  91. package/lib/core/activity.js +1087 -0
  92. package/lib/core/env.js +719 -0
  93. package/lib/core/fs.js +1058 -0
  94. package/lib/core/httpClient.js +730 -0
  95. package/lib/core/logger.js +280 -0
  96. package/lib/core/parallel.js +323 -0
  97. package/lib/core/paths.js +70 -0
  98. package/lib/core/projectConfig.js +122 -0
  99. package/lib/core/propertySanitizer.js +174 -0
  100. package/lib/core/spdx.js +5 -0
  101. package/lib/core/state.js +59 -0
  102. package/lib/core/text.js +207 -0
  103. package/lib/core/ui.js +920 -0
  104. package/lib/core/urls.js +84 -0
  105. package/lib/ecosystems/asarutils.js +1636 -0
  106. package/lib/ecosystems/bunutils.js +402 -0
  107. package/lib/ecosystems/caxa.js +77 -0
  108. package/lib/ecosystems/chromextutils.js +1174 -0
  109. package/lib/ecosystems/cmakeResolver.js +587 -0
  110. package/lib/ecosystems/cppEvidence.js +305 -0
  111. package/lib/ecosystems/denoutils.js +1094 -0
  112. package/lib/ecosystems/dotnetutils.js +132 -0
  113. package/lib/ecosystems/ecosystems.js +2772 -0
  114. package/lib/ecosystems/golem.js +1807 -0
  115. package/lib/ecosystems/gradleutils.js +1048 -0
  116. package/lib/ecosystems/jsEvidence.js +317 -0
  117. package/lib/ecosystems/npmutils.js +586 -0
  118. package/lib/ecosystems/parsers-bazel.js +875 -0
  119. package/lib/ecosystems/parsers-dotnet.js +1430 -0
  120. package/lib/ecosystems/parsers-gleam.js +302 -0
  121. package/lib/ecosystems/parsers-go.js +818 -0
  122. package/lib/ecosystems/parsers-js.js +4222 -0
  123. package/lib/ecosystems/parsers-jvm.js +948 -0
  124. package/lib/ecosystems/parsers-misc.js +3191 -0
  125. package/lib/ecosystems/parsers-python.js +2070 -0
  126. package/lib/ecosystems/parsers-rust.js +1294 -0
  127. package/lib/ecosystems/parsers-zig.js +861 -0
  128. package/lib/ecosystems/pep770.js +341 -0
  129. package/lib/ecosystems/pylockutils.js +427 -0
  130. package/lib/ecosystems/pythonutils.js +1281 -0
  131. package/lib/ecosystems/registryProvenance.js +1003 -0
  132. package/lib/ecosystems/remote/huggingface.js +1343 -0
  133. package/lib/ecosystems/rubyutils.js +1629 -0
  134. package/lib/ecosystems/rusi.js +820 -0
  135. package/lib/ecosystems/sbtutils.js +972 -0
  136. package/lib/ecosystems/tea.js +607 -0
  137. package/lib/ecosystems/utils.js +583 -0
  138. package/lib/ecosystems/zigResolver.js +410 -0
  139. package/lib/evinser/db.js +145 -0
  140. package/lib/evinser/evinser.js +2362 -0
  141. package/lib/evinser/scalasem.js +65 -0
  142. package/lib/evinser/swiftsem.js +767 -0
  143. package/lib/helpers/annotationFormatter.js +84 -0
  144. package/lib/helpers/bomSigner.js +312 -0
  145. package/lib/helpers/exportUtils.js +160 -0
  146. package/lib/helpers/remote/dependency-track.js +117 -0
  147. package/lib/helpers/versutils.js +202 -0
  148. package/lib/helpers/vsixutils.js +1069 -0
  149. package/lib/inventory/agentFormulationParser.js +322 -0
  150. package/lib/inventory/aiCollector.js +2140 -0
  151. package/lib/inventory/aiInventory.js +414 -0
  152. package/lib/inventory/aiModelVariants.js +121 -0
  153. package/lib/inventory/aiOversightCollector.js +661 -0
  154. package/lib/inventory/aiProvenanceCollector.js +822 -0
  155. package/lib/inventory/analyzer.js +6089 -0
  156. package/lib/inventory/analyzerScope.js +807 -0
  157. package/lib/inventory/astWorker.js +20 -0
  158. package/lib/inventory/atomUtils.js +1092 -0
  159. package/lib/inventory/auditCategories.js +192 -0
  160. package/lib/inventory/bomUtils.js +369 -0
  161. package/lib/inventory/cacheCommands.js +182 -0
  162. package/lib/inventory/cacheDir.js +104 -0
  163. package/lib/inventory/cbomutils.js +590 -0
  164. package/lib/inventory/cdxrs.js +434 -0
  165. package/lib/inventory/ciParsers/azurePipelines.js +295 -0
  166. package/lib/inventory/ciParsers/circleCi.js +286 -0
  167. package/lib/inventory/ciParsers/common.js +24 -0
  168. package/lib/inventory/ciParsers/githubActions.js +2434 -0
  169. package/lib/inventory/ciParsers/gitlabCi.js +213 -0
  170. package/lib/inventory/ciParsers/jenkins.js +180 -0
  171. package/lib/inventory/citations.js +234 -0
  172. package/lib/inventory/communityAiConfigParser.js +756 -0
  173. package/lib/inventory/containerRisk.js +204 -0
  174. package/lib/inventory/cryptoAlgorithmFamily.js +258 -0
  175. package/lib/inventory/deps.js +807 -0
  176. package/lib/inventory/depsUtils.js +588 -0
  177. package/lib/inventory/display.js +2128 -0
  178. package/lib/inventory/dosai.js +601 -0
  179. package/lib/inventory/dosaiParsers.js +154 -0
  180. package/lib/inventory/dynamic.js +145 -0
  181. package/lib/inventory/envcontext.js +1793 -0
  182. package/lib/inventory/evidenceUtils.js +774 -0
  183. package/lib/inventory/fetchBatch.js +779 -0
  184. package/lib/inventory/fetchRate.js +255 -0
  185. package/lib/inventory/forgeEnricher.js +300 -0
  186. package/lib/inventory/formulationParsers.js +422 -0
  187. package/lib/inventory/gtfobins.js +339 -0
  188. package/lib/inventory/hbom.js +719 -0
  189. package/lib/inventory/hbomAnalysis.js +310 -0
  190. package/lib/inventory/hbomLoader.js +35 -0
  191. package/lib/inventory/hostTopology.js +835 -0
  192. package/lib/inventory/inventoryStats.js +100 -0
  193. package/lib/inventory/jsonLike.js +121 -0
  194. package/lib/inventory/licenseEnhancer.js +918 -0
  195. package/lib/inventory/lolbas.js +285 -0
  196. package/lib/inventory/mcp.js +253 -0
  197. package/lib/inventory/mcpConfigParser.js +683 -0
  198. package/lib/inventory/mcpDiscovery.js +112 -0
  199. package/lib/inventory/mcpPinning.js +209 -0
  200. package/lib/inventory/osPackageResolver.js +493 -0
  201. package/lib/inventory/osinfo.js +383 -0
  202. package/lib/inventory/osqueryTransform.js +274 -0
  203. package/lib/inventory/plugins.js +466 -0
  204. package/lib/inventory/protobom.js +231 -0
  205. package/lib/inventory/protobomLoader.js +43 -0
  206. package/lib/inventory/provenanceUtils.js +224 -0
  207. package/lib/inventory/purl.js +749 -0
  208. package/lib/inventory/rustFormulationParser.js +339 -0
  209. package/lib/inventory/sbomDocument.js +340 -0
  210. package/lib/inventory/source.js +1488 -0
  211. package/lib/inventory/spdx.js +361 -0
  212. package/lib/inventory/spdxExpression.js +512 -0
  213. package/lib/inventory/spdxUtils.js +107 -0
  214. package/lib/inventory/table.js +267 -0
  215. package/lib/inventory/traceRunner.js +439 -0
  216. package/lib/inventory/unicodeScan.js +147 -0
  217. package/lib/managers/binary.js +3459 -0
  218. package/lib/managers/containerutils.js +125 -0
  219. package/lib/managers/docker.js +2145 -0
  220. package/lib/managers/dockerConnection.js +162 -0
  221. package/lib/managers/oci.js +704 -0
  222. package/lib/managers/piptree.js +267 -0
  223. package/lib/parsers/cmakeCache.js +199 -0
  224. package/lib/parsers/gguf.js +492 -0
  225. package/lib/parsers/gitAiNotes.js +233 -0
  226. package/lib/parsers/gitmodules.js +239 -0
  227. package/lib/parsers/htmlExtract.js +610 -0
  228. package/lib/parsers/huggingfaceManifest.js +551 -0
  229. package/lib/parsers/huggingfaceUtils.js +506 -0
  230. package/lib/parsers/iri.js +618 -0
  231. package/lib/parsers/npmrc.js +99 -0
  232. package/lib/parsers/ollama.js +101 -0
  233. package/lib/parsers/xml.js +841 -0
  234. package/lib/server/openapi.yaml +433 -0
  235. package/lib/server/server.js +616 -0
  236. package/lib/stages/postgen/annotator.js +977 -0
  237. package/lib/stages/postgen/auditBom.js +468 -0
  238. package/lib/stages/postgen/postgen.js +1239 -0
  239. package/lib/stages/postgen/ruleEngine.js +731 -0
  240. package/lib/stages/postgen/sortBom.js +195 -0
  241. package/lib/stages/postgen/spdxConverter.js +796 -0
  242. package/lib/stages/postgen/specVersionCompat.js +703 -0
  243. package/lib/stages/pregen/envAudit.js +388 -0
  244. package/lib/stages/pregen/pregen.js +637 -0
  245. package/lib/third-party/README.md +9 -0
  246. package/lib/third-party/arborist/CHANGELOG.md +1109 -0
  247. package/lib/third-party/arborist/LICENSE.md +20 -0
  248. package/lib/third-party/arborist/README.md +315 -0
  249. package/lib/third-party/arborist/lib/arborist/index.js +258 -0
  250. package/lib/third-party/arborist/lib/arborist/load-actual.js +605 -0
  251. package/lib/third-party/arborist/lib/arborist/load-virtual.js +300 -0
  252. package/lib/third-party/arborist/lib/bin-links.js +120 -0
  253. package/lib/third-party/arborist/lib/calc-dep-flags.js +130 -0
  254. package/lib/third-party/arborist/lib/case-insensitive-map.js +51 -0
  255. package/lib/third-party/arborist/lib/consistent-resolve.js +42 -0
  256. package/lib/third-party/arborist/lib/debug.js +50 -0
  257. package/lib/third-party/arborist/lib/dep-valid.js +190 -0
  258. package/lib/third-party/arborist/lib/edge.js +417 -0
  259. package/lib/third-party/arborist/lib/from-path.js +36 -0
  260. package/lib/third-party/arborist/lib/gather-dep-set.js +43 -0
  261. package/lib/third-party/arborist/lib/index.js +22 -0
  262. package/lib/third-party/arborist/lib/inventory.js +138 -0
  263. package/lib/third-party/arborist/lib/link.js +169 -0
  264. package/lib/third-party/arborist/lib/node.js +1673 -0
  265. package/lib/third-party/arborist/lib/npm-extension.js +45 -0
  266. package/lib/third-party/arborist/lib/override-resolves.js +11 -0
  267. package/lib/third-party/arborist/lib/override-set.js +319 -0
  268. package/lib/third-party/arborist/lib/package-extensions.js +289 -0
  269. package/lib/third-party/arborist/lib/printable.js +210 -0
  270. package/lib/third-party/arborist/lib/proc-log.js +30 -0
  271. package/lib/third-party/arborist/lib/realpath.js +100 -0
  272. package/lib/third-party/arborist/lib/relpath.js +4 -0
  273. package/lib/third-party/arborist/lib/shrinkwrap.js +1291 -0
  274. package/lib/third-party/arborist/lib/spec-from-lock.js +34 -0
  275. package/lib/third-party/arborist/lib/tree-check.js +165 -0
  276. package/lib/third-party/arborist/lib/validate-npm-package-name.js +107 -0
  277. package/lib/third-party/arborist/lib/version-from-tgz.js +51 -0
  278. package/lib/third-party/arborist/lib/yarn-lock.js +397 -0
  279. package/lib/validator/bomValidator.js +985 -0
  280. package/lib/validator/complianceEngine.js +241 -0
  281. package/lib/validator/complianceRules.js +1680 -0
  282. package/lib/validator/generated/runtime.mjs +17 -0
  283. package/lib/validator/generated/validate-1.6.mjs +21258 -0
  284. package/lib/validator/generated/validate-1.7.mjs +24803 -0
  285. package/lib/validator/generated/validate-2.0.mjs +22931 -0
  286. package/lib/validator/index.js +246 -0
  287. package/lib/validator/reporters/annotations.js +142 -0
  288. package/lib/validator/reporters/console.js +160 -0
  289. package/lib/validator/reporters/index.js +41 -0
  290. package/lib/validator/reporters/json.js +37 -0
  291. package/lib/validator/reporters/sarif.js +184 -0
  292. package/package.json +209 -26
  293. package/types/bin/audit.d.ts +3 -0
  294. package/types/bin/audit.d.ts.map +1 -0
  295. package/types/bin/cdxgen.d.ts +3 -0
  296. package/types/bin/cdxgen.d.ts.map +1 -0
  297. package/types/bin/convert.d.ts +3 -0
  298. package/types/bin/convert.d.ts.map +1 -0
  299. package/types/bin/evinse.d.ts +3 -0
  300. package/types/bin/evinse.d.ts.map +1 -0
  301. package/types/bin/hbom.d.ts +3 -0
  302. package/types/bin/hbom.d.ts.map +1 -0
  303. package/types/bin/repl.d.ts +3 -0
  304. package/types/bin/repl.d.ts.map +1 -0
  305. package/types/bin/sign.d.ts +3 -0
  306. package/types/bin/sign.d.ts.map +1 -0
  307. package/types/bin/tracebom.d.ts +3 -0
  308. package/types/bin/tracebom.d.ts.map +1 -0
  309. package/types/bin/validate.d.ts +14 -0
  310. package/types/bin/validate.d.ts.map +1 -0
  311. package/types/bin/verify.d.ts +3 -0
  312. package/types/bin/verify.d.ts.map +1 -0
  313. package/types/lib/audit/index.d.ts +153 -0
  314. package/types/lib/audit/index.d.ts.map +1 -0
  315. package/types/lib/audit/progress.d.ts +31 -0
  316. package/types/lib/audit/progress.d.ts.map +1 -0
  317. package/types/lib/audit/reporters.d.ts +62 -0
  318. package/types/lib/audit/reporters.d.ts.map +1 -0
  319. package/types/lib/audit/scoring.d.ts +36 -0
  320. package/types/lib/audit/scoring.d.ts.map +1 -0
  321. package/types/lib/audit/targets.d.ts +77 -0
  322. package/types/lib/audit/targets.d.ts.map +1 -0
  323. package/types/lib/cli/bomAssembly.d.ts +329 -0
  324. package/types/lib/cli/bomAssembly.d.ts.map +1 -0
  325. package/types/lib/cli/cliOptions.d.ts +156 -0
  326. package/types/lib/cli/cliOptions.d.ts.map +1 -0
  327. package/types/lib/cli/index.d.ts +107 -0
  328. package/types/lib/cli/index.d.ts.map +1 -0
  329. package/types/lib/cli/jsBom.d.ts +94 -0
  330. package/types/lib/cli/jsBom.d.ts.map +1 -0
  331. package/types/lib/cli/jvmBom.d.ts +54 -0
  332. package/types/lib/cli/jvmBom.d.ts.map +1 -0
  333. package/types/lib/cli/managedBom.d.ts +105 -0
  334. package/types/lib/cli/managedBom.d.ts.map +1 -0
  335. package/types/lib/cli/nativeBom.d.ts +143 -0
  336. package/types/lib/cli/nativeBom.d.ts.map +1 -0
  337. package/types/lib/core/activity.d.ts +178 -0
  338. package/types/lib/core/activity.d.ts.map +1 -0
  339. package/types/lib/core/env.d.ts +192 -0
  340. package/types/lib/core/env.d.ts.map +1 -0
  341. package/types/lib/core/fs.d.ts +158 -0
  342. package/types/lib/core/fs.d.ts.map +1 -0
  343. package/types/lib/core/httpClient.d.ts +149 -0
  344. package/types/lib/core/httpClient.d.ts.map +1 -0
  345. package/types/lib/core/logger.d.ts +50 -0
  346. package/types/lib/core/logger.d.ts.map +1 -0
  347. package/types/lib/core/parallel.d.ts +95 -0
  348. package/types/lib/core/parallel.d.ts.map +1 -0
  349. package/types/lib/core/paths.d.ts +36 -0
  350. package/types/lib/core/paths.d.ts.map +1 -0
  351. package/types/lib/core/projectConfig.d.ts +23 -0
  352. package/types/lib/core/projectConfig.d.ts.map +1 -0
  353. package/types/lib/core/propertySanitizer.d.ts +23 -0
  354. package/types/lib/core/propertySanitizer.d.ts.map +1 -0
  355. package/types/lib/core/spdx.d.ts +5 -0
  356. package/types/lib/core/spdx.d.ts.map +1 -0
  357. package/types/lib/core/state.d.ts +20 -0
  358. package/types/lib/core/state.d.ts.map +1 -0
  359. package/types/lib/core/text.d.ts +78 -0
  360. package/types/lib/core/text.d.ts.map +1 -0
  361. package/types/lib/core/ui.d.ts +87 -0
  362. package/types/lib/core/ui.d.ts.map +1 -0
  363. package/types/lib/core/urls.d.ts +43 -0
  364. package/types/lib/core/urls.d.ts.map +1 -0
  365. package/types/lib/ecosystems/asarutils.d.ts +71 -0
  366. package/types/lib/ecosystems/asarutils.d.ts.map +1 -0
  367. package/types/lib/ecosystems/bunutils.d.ts +38 -0
  368. package/types/lib/ecosystems/bunutils.d.ts.map +1 -0
  369. package/types/lib/ecosystems/caxa.d.ts +8 -0
  370. package/types/lib/ecosystems/caxa.d.ts.map +1 -0
  371. package/types/lib/ecosystems/chromextutils.d.ts +97 -0
  372. package/types/lib/ecosystems/chromextutils.d.ts.map +1 -0
  373. package/types/lib/ecosystems/cmakeResolver.d.ts +176 -0
  374. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -0
  375. package/types/lib/ecosystems/cppEvidence.d.ts +35 -0
  376. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -0
  377. package/types/lib/ecosystems/denoutils.d.ts +76 -0
  378. package/types/lib/ecosystems/denoutils.d.ts.map +1 -0
  379. package/types/lib/ecosystems/dotnetutils.d.ts +45 -0
  380. package/types/lib/ecosystems/dotnetutils.d.ts.map +1 -0
  381. package/types/lib/ecosystems/ecosystems.d.ts +279 -0
  382. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -0
  383. package/types/lib/ecosystems/golem.d.ts +60 -0
  384. package/types/lib/ecosystems/golem.d.ts.map +1 -0
  385. package/types/lib/ecosystems/gradleutils.d.ts +139 -0
  386. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -0
  387. package/types/lib/ecosystems/jsEvidence.d.ts +17 -0
  388. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -0
  389. package/types/lib/ecosystems/npmutils.d.ts +183 -0
  390. package/types/lib/ecosystems/npmutils.d.ts.map +1 -0
  391. package/types/lib/ecosystems/parsers-bazel.d.ts +36 -0
  392. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -0
  393. package/types/lib/ecosystems/parsers-dotnet.d.ts +116 -0
  394. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -0
  395. package/types/lib/ecosystems/parsers-gleam.d.ts +33 -0
  396. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -0
  397. package/types/lib/ecosystems/parsers-go.d.ts +92 -0
  398. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -0
  399. package/types/lib/ecosystems/parsers-js.d.ts +242 -0
  400. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -0
  401. package/types/lib/ecosystems/parsers-jvm.d.ts +100 -0
  402. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -0
  403. package/types/lib/ecosystems/parsers-misc.d.ts +450 -0
  404. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -0
  405. package/types/lib/ecosystems/parsers-python.d.ts +145 -0
  406. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -0
  407. package/types/lib/ecosystems/parsers-rust.d.ts +56 -0
  408. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -0
  409. package/types/lib/ecosystems/parsers-zig.d.ts +83 -0
  410. package/types/lib/ecosystems/parsers-zig.d.ts.map +1 -0
  411. package/types/lib/ecosystems/pep770.d.ts +38 -0
  412. package/types/lib/ecosystems/pep770.d.ts.map +1 -0
  413. package/types/lib/ecosystems/pylockutils.d.ts +68 -0
  414. package/types/lib/ecosystems/pylockutils.d.ts.map +1 -0
  415. package/types/lib/ecosystems/pythonutils.d.ts +70 -0
  416. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -0
  417. package/types/lib/ecosystems/registryProvenance.d.ts +26 -0
  418. package/types/lib/ecosystems/registryProvenance.d.ts.map +1 -0
  419. package/types/lib/ecosystems/remote/huggingface.d.ts +31 -0
  420. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -0
  421. package/types/lib/ecosystems/rubyutils.d.ts +182 -0
  422. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -0
  423. package/types/lib/ecosystems/rusi.d.ts +40 -0
  424. package/types/lib/ecosystems/rusi.d.ts.map +1 -0
  425. package/types/lib/ecosystems/sbtutils.d.ts +179 -0
  426. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -0
  427. package/types/lib/ecosystems/tea.d.ts +180 -0
  428. package/types/lib/ecosystems/tea.d.ts.map +1 -0
  429. package/types/lib/ecosystems/utils.d.ts +41 -0
  430. package/types/lib/ecosystems/utils.d.ts.map +1 -0
  431. package/types/lib/ecosystems/zigResolver.d.ts +70 -0
  432. package/types/lib/ecosystems/zigResolver.d.ts.map +1 -0
  433. package/types/lib/evinser/db.d.ts +17 -0
  434. package/types/lib/evinser/db.d.ts.map +1 -0
  435. package/types/lib/evinser/evinser.d.ts +354 -0
  436. package/types/lib/evinser/evinser.d.ts.map +1 -0
  437. package/types/lib/evinser/scalasem.d.ts +15 -0
  438. package/types/lib/evinser/scalasem.d.ts.map +1 -0
  439. package/types/lib/evinser/swiftsem.d.ts +103 -0
  440. package/types/lib/evinser/swiftsem.d.ts.map +1 -0
  441. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  442. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  443. package/types/lib/helpers/bomSigner.d.ts +28 -0
  444. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  445. package/types/lib/helpers/exportUtils.d.ts +52 -0
  446. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  447. package/types/lib/helpers/remote/dependency-track.d.ts +23 -0
  448. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  449. package/types/lib/helpers/versutils.d.ts +8 -0
  450. package/types/lib/helpers/versutils.d.ts.map +1 -0
  451. package/types/lib/helpers/vsixutils.d.ts +130 -0
  452. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  453. package/types/lib/inventory/agentFormulationParser.d.ts +21 -0
  454. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -0
  455. package/types/lib/inventory/aiCollector.d.ts +61 -0
  456. package/types/lib/inventory/aiCollector.d.ts.map +1 -0
  457. package/types/lib/inventory/aiInventory.d.ts +103 -0
  458. package/types/lib/inventory/aiInventory.d.ts.map +1 -0
  459. package/types/lib/inventory/aiModelVariants.d.ts +31 -0
  460. package/types/lib/inventory/aiModelVariants.d.ts.map +1 -0
  461. package/types/lib/inventory/aiOversightCollector.d.ts +28 -0
  462. package/types/lib/inventory/aiOversightCollector.d.ts.map +1 -0
  463. package/types/lib/inventory/aiProvenanceCollector.d.ts +13 -0
  464. package/types/lib/inventory/aiProvenanceCollector.d.ts.map +1 -0
  465. package/types/lib/inventory/analyzer.d.ts +157 -0
  466. package/types/lib/inventory/analyzer.d.ts.map +1 -0
  467. package/types/lib/inventory/analyzerScope.d.ts +106 -0
  468. package/types/lib/inventory/analyzerScope.d.ts.map +1 -0
  469. package/types/lib/inventory/astWorker.d.ts +9 -0
  470. package/types/lib/inventory/astWorker.d.ts.map +1 -0
  471. package/types/lib/inventory/atomUtils.d.ts +159 -0
  472. package/types/lib/inventory/atomUtils.d.ts.map +1 -0
  473. package/types/lib/inventory/auditCategories.d.ts +80 -0
  474. package/types/lib/inventory/auditCategories.d.ts.map +1 -0
  475. package/types/lib/inventory/bomUtils.d.ts +145 -0
  476. package/types/lib/inventory/bomUtils.d.ts.map +1 -0
  477. package/types/lib/inventory/cacheCommands.d.ts +28 -0
  478. package/types/lib/inventory/cacheCommands.d.ts.map +1 -0
  479. package/types/lib/inventory/cacheDir.d.ts +55 -0
  480. package/types/lib/inventory/cacheDir.d.ts.map +1 -0
  481. package/types/lib/inventory/cbomutils.d.ts +37 -0
  482. package/types/lib/inventory/cbomutils.d.ts.map +1 -0
  483. package/types/lib/inventory/cdxrs.d.ts +90 -0
  484. package/types/lib/inventory/cdxrs.d.ts.map +1 -0
  485. package/types/lib/inventory/ciParsers/azurePipelines.d.ts +27 -0
  486. package/types/lib/inventory/ciParsers/azurePipelines.d.ts.map +1 -0
  487. package/types/lib/inventory/ciParsers/circleCi.d.ts +27 -0
  488. package/types/lib/inventory/ciParsers/circleCi.d.ts.map +1 -0
  489. package/types/lib/inventory/ciParsers/common.d.ts +11 -0
  490. package/types/lib/inventory/ciParsers/common.d.ts.map +1 -0
  491. package/types/lib/inventory/ciParsers/githubActions.d.ts +38 -0
  492. package/types/lib/inventory/ciParsers/githubActions.d.ts.map +1 -0
  493. package/types/lib/inventory/ciParsers/gitlabCi.d.ts +26 -0
  494. package/types/lib/inventory/ciParsers/gitlabCi.d.ts.map +1 -0
  495. package/types/lib/inventory/ciParsers/jenkins.d.ts +26 -0
  496. package/types/lib/inventory/ciParsers/jenkins.d.ts.map +1 -0
  497. package/types/lib/inventory/citations.d.ts +90 -0
  498. package/types/lib/inventory/citations.d.ts.map +1 -0
  499. package/types/lib/inventory/communityAiConfigParser.d.ts +14 -0
  500. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -0
  501. package/types/lib/inventory/containerRisk.d.ts +20 -0
  502. package/types/lib/inventory/containerRisk.d.ts.map +1 -0
  503. package/types/lib/inventory/cryptoAlgorithmFamily.d.ts +37 -0
  504. package/types/lib/inventory/cryptoAlgorithmFamily.d.ts.map +1 -0
  505. package/types/lib/inventory/deps.d.ts +140 -0
  506. package/types/lib/inventory/deps.d.ts.map +1 -0
  507. package/types/lib/inventory/depsUtils.d.ts +74 -0
  508. package/types/lib/inventory/depsUtils.d.ts.map +1 -0
  509. package/types/lib/inventory/display.d.ts +227 -0
  510. package/types/lib/inventory/display.d.ts.map +1 -0
  511. package/types/lib/inventory/dosai.d.ts +130 -0
  512. package/types/lib/inventory/dosai.d.ts.map +1 -0
  513. package/types/lib/inventory/dosaiParsers.d.ts +59 -0
  514. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -0
  515. package/types/lib/inventory/dynamic.d.ts +25 -0
  516. package/types/lib/inventory/dynamic.d.ts.map +1 -0
  517. package/types/lib/inventory/envcontext.d.ts +377 -0
  518. package/types/lib/inventory/envcontext.d.ts.map +1 -0
  519. package/types/lib/inventory/evidenceUtils.d.ts +74 -0
  520. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -0
  521. package/types/lib/inventory/fetchBatch.d.ts +127 -0
  522. package/types/lib/inventory/fetchBatch.d.ts.map +1 -0
  523. package/types/lib/inventory/fetchRate.d.ts +118 -0
  524. package/types/lib/inventory/fetchRate.d.ts.map +1 -0
  525. package/types/lib/inventory/forgeEnricher.d.ts +31 -0
  526. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -0
  527. package/types/lib/inventory/formulationParsers.d.ts +50 -0
  528. package/types/lib/inventory/formulationParsers.d.ts.map +1 -0
  529. package/types/lib/inventory/gtfobins.d.ts +27 -0
  530. package/types/lib/inventory/gtfobins.d.ts.map +1 -0
  531. package/types/lib/inventory/hbom.d.ts +58 -0
  532. package/types/lib/inventory/hbom.d.ts.map +1 -0
  533. package/types/lib/inventory/hbomAnalysis.d.ts +56 -0
  534. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -0
  535. package/types/lib/inventory/hbomLoader.d.ts +7 -0
  536. package/types/lib/inventory/hbomLoader.d.ts.map +1 -0
  537. package/types/lib/inventory/hostTopology.d.ts +47 -0
  538. package/types/lib/inventory/hostTopology.d.ts.map +1 -0
  539. package/types/lib/inventory/inventoryStats.d.ts +42 -0
  540. package/types/lib/inventory/inventoryStats.d.ts.map +1 -0
  541. package/types/lib/inventory/jsonLike.d.ts +23 -0
  542. package/types/lib/inventory/jsonLike.d.ts.map +1 -0
  543. package/types/lib/inventory/licenseEnhancer.d.ts +70 -0
  544. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -0
  545. package/types/lib/inventory/lolbas.d.ts +16 -0
  546. package/types/lib/inventory/lolbas.d.ts.map +1 -0
  547. package/types/lib/inventory/mcp.d.ts +29 -0
  548. package/types/lib/inventory/mcp.d.ts.map +1 -0
  549. package/types/lib/inventory/mcpConfigParser.d.ts +20 -0
  550. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -0
  551. package/types/lib/inventory/mcpDiscovery.d.ts +33 -0
  552. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -0
  553. package/types/lib/inventory/mcpPinning.d.ts +11 -0
  554. package/types/lib/inventory/mcpPinning.d.ts.map +1 -0
  555. package/types/lib/inventory/osPackageResolver.d.ts +49 -0
  556. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -0
  557. package/types/lib/inventory/osinfo.d.ts +201 -0
  558. package/types/lib/inventory/osinfo.d.ts.map +1 -0
  559. package/types/lib/inventory/osqueryTransform.d.ts +84 -0
  560. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -0
  561. package/types/lib/inventory/plugins.d.ts +58 -0
  562. package/types/lib/inventory/plugins.d.ts.map +1 -0
  563. package/types/lib/inventory/protobom.d.ts +71 -0
  564. package/types/lib/inventory/protobom.d.ts.map +1 -0
  565. package/types/lib/inventory/protobomLoader.d.ts +17 -0
  566. package/types/lib/inventory/protobomLoader.d.ts.map +1 -0
  567. package/types/lib/inventory/provenanceUtils.d.ts +98 -0
  568. package/types/lib/inventory/provenanceUtils.d.ts.map +1 -0
  569. package/types/lib/inventory/purl.d.ts +313 -0
  570. package/types/lib/inventory/purl.d.ts.map +1 -0
  571. package/types/lib/inventory/rustFormulationParser.d.ts +17 -0
  572. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -0
  573. package/types/lib/inventory/sbomDocument.d.ts +59 -0
  574. package/types/lib/inventory/sbomDocument.d.ts.map +1 -0
  575. package/types/lib/inventory/source.d.ts +163 -0
  576. package/types/lib/inventory/source.d.ts.map +1 -0
  577. package/types/lib/inventory/spdx.d.ts +66 -0
  578. package/types/lib/inventory/spdx.d.ts.map +1 -0
  579. package/types/lib/inventory/spdxExpression.d.ts +58 -0
  580. package/types/lib/inventory/spdxExpression.d.ts.map +1 -0
  581. package/types/lib/inventory/spdxUtils.d.ts +12 -0
  582. package/types/lib/inventory/spdxUtils.d.ts.map +1 -0
  583. package/types/lib/inventory/table.d.ts +22 -0
  584. package/types/lib/inventory/table.d.ts.map +1 -0
  585. package/types/lib/inventory/traceRunner.d.ts +80 -0
  586. package/types/lib/inventory/traceRunner.d.ts.map +1 -0
  587. package/types/lib/inventory/unicodeScan.d.ts +46 -0
  588. package/types/lib/inventory/unicodeScan.d.ts.map +1 -0
  589. package/types/lib/managers/binary.d.ts +147 -0
  590. package/types/lib/managers/binary.d.ts.map +1 -0
  591. package/types/lib/managers/containerutils.d.ts +30 -0
  592. package/types/lib/managers/containerutils.d.ts.map +1 -0
  593. package/types/lib/managers/docker.d.ts +157 -0
  594. package/types/lib/managers/docker.d.ts.map +1 -0
  595. package/types/lib/managers/dockerConnection.d.ts +42 -0
  596. package/types/lib/managers/dockerConnection.d.ts.map +1 -0
  597. package/types/lib/managers/oci.d.ts +21 -0
  598. package/types/lib/managers/oci.d.ts.map +1 -0
  599. package/types/lib/managers/piptree.d.ts +13 -0
  600. package/types/lib/managers/piptree.d.ts.map +1 -0
  601. package/types/lib/parsers/cmakeCache.d.ts +70 -0
  602. package/types/lib/parsers/cmakeCache.d.ts.map +1 -0
  603. package/types/lib/parsers/gguf.d.ts +39 -0
  604. package/types/lib/parsers/gguf.d.ts.map +1 -0
  605. package/types/lib/parsers/gitAiNotes.d.ts +15 -0
  606. package/types/lib/parsers/gitAiNotes.d.ts.map +1 -0
  607. package/types/lib/parsers/gitmodules.d.ts +88 -0
  608. package/types/lib/parsers/gitmodules.d.ts.map +1 -0
  609. package/types/lib/parsers/htmlExtract.d.ts +34 -0
  610. package/types/lib/parsers/htmlExtract.d.ts.map +1 -0
  611. package/types/lib/parsers/huggingfaceManifest.d.ts +153 -0
  612. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -0
  613. package/types/lib/parsers/huggingfaceUtils.d.ts +155 -0
  614. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -0
  615. package/types/lib/parsers/iri.d.ts +68 -0
  616. package/types/lib/parsers/iri.d.ts.map +1 -0
  617. package/types/lib/parsers/npmrc.d.ts +33 -0
  618. package/types/lib/parsers/npmrc.d.ts.map +1 -0
  619. package/types/lib/parsers/ollama.d.ts +22 -0
  620. package/types/lib/parsers/ollama.d.ts.map +1 -0
  621. package/types/lib/parsers/xml.d.ts +47 -0
  622. package/types/lib/parsers/xml.d.ts.map +1 -0
  623. package/types/lib/server/server.d.ts +51 -0
  624. package/types/lib/server/server.d.ts.map +1 -0
  625. package/types/lib/stages/postgen/annotator.d.ts +36 -0
  626. package/types/lib/stages/postgen/annotator.d.ts.map +1 -0
  627. package/types/lib/stages/postgen/auditBom.d.ts +92 -0
  628. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  629. package/types/lib/stages/postgen/postgen.d.ts +59 -0
  630. package/types/lib/stages/postgen/postgen.d.ts.map +1 -0
  631. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  632. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  633. package/types/lib/stages/postgen/sortBom.d.ts +34 -0
  634. package/types/lib/stages/postgen/sortBom.d.ts.map +1 -0
  635. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  636. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  637. package/types/lib/stages/postgen/specVersionCompat.d.ts +71 -0
  638. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -0
  639. package/types/lib/stages/pregen/envAudit.d.ts +14 -0
  640. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  641. package/types/lib/stages/pregen/pregen.d.ts +59 -0
  642. package/types/lib/stages/pregen/pregen.d.ts.map +1 -0
  643. package/types/lib/third-party/arborist/lib/arborist/index.d.ts +47 -0
  644. package/types/lib/third-party/arborist/lib/arborist/index.d.ts.map +1 -0
  645. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts +4 -0
  646. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts.map +1 -0
  647. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts +4 -0
  648. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts.map +1 -0
  649. package/types/lib/third-party/arborist/lib/bin-links.d.ts +18 -0
  650. package/types/lib/third-party/arborist/lib/bin-links.d.ts.map +1 -0
  651. package/types/lib/third-party/arborist/lib/calc-dep-flags.d.ts +3 -0
  652. package/types/lib/third-party/arborist/lib/calc-dep-flags.d.ts.map +1 -0
  653. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts +9 -0
  654. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts.map +1 -0
  655. package/types/lib/third-party/arborist/lib/consistent-resolve.d.ts +3 -0
  656. package/types/lib/third-party/arborist/lib/consistent-resolve.d.ts.map +1 -0
  657. package/types/lib/third-party/arborist/lib/debug.d.ts +3 -0
  658. package/types/lib/third-party/arborist/lib/debug.d.ts.map +1 -0
  659. package/types/lib/third-party/arborist/lib/dep-valid.d.ts +3 -0
  660. package/types/lib/third-party/arborist/lib/dep-valid.d.ts.map +1 -0
  661. package/types/lib/third-party/arborist/lib/edge.d.ts +46 -0
  662. package/types/lib/third-party/arborist/lib/edge.d.ts.map +1 -0
  663. package/types/lib/third-party/arborist/lib/from-path.d.ts +3 -0
  664. package/types/lib/third-party/arborist/lib/from-path.d.ts.map +1 -0
  665. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts +3 -0
  666. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts.map +1 -0
  667. package/types/lib/third-party/arborist/lib/index.d.ts +3 -0
  668. package/types/lib/third-party/arborist/lib/index.d.ts.map +1 -0
  669. package/types/lib/third-party/arborist/lib/inventory.d.ts +14 -0
  670. package/types/lib/third-party/arborist/lib/inventory.d.ts.map +1 -0
  671. package/types/lib/third-party/arborist/lib/link.d.ts +18 -0
  672. package/types/lib/third-party/arborist/lib/link.d.ts.map +1 -0
  673. package/types/lib/third-party/arborist/lib/node.d.ts +112 -0
  674. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -0
  675. package/types/lib/third-party/arborist/lib/npm-extension.d.ts +18 -0
  676. package/types/lib/third-party/arborist/lib/npm-extension.d.ts.map +1 -0
  677. package/types/lib/third-party/arborist/lib/override-resolves.d.ts +3 -0
  678. package/types/lib/third-party/arborist/lib/override-resolves.d.ts.map +1 -0
  679. package/types/lib/third-party/arborist/lib/override-set.d.ts +27 -0
  680. package/types/lib/third-party/arborist/lib/override-set.d.ts.map +1 -0
  681. package/types/lib/third-party/arborist/lib/package-extensions.d.ts +36 -0
  682. package/types/lib/third-party/arborist/lib/package-extensions.d.ts.map +1 -0
  683. package/types/lib/third-party/arborist/lib/printable.d.ts +3 -0
  684. package/types/lib/third-party/arborist/lib/printable.d.ts.map +1 -0
  685. package/types/lib/third-party/arborist/lib/proc-log.d.ts +20 -0
  686. package/types/lib/third-party/arborist/lib/proc-log.d.ts.map +1 -0
  687. package/types/lib/third-party/arborist/lib/realpath.d.ts +3 -0
  688. package/types/lib/third-party/arborist/lib/realpath.d.ts.map +1 -0
  689. package/types/lib/third-party/arborist/lib/relpath.d.ts +3 -0
  690. package/types/lib/third-party/arborist/lib/relpath.d.ts.map +1 -0
  691. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts +54 -0
  692. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts.map +1 -0
  693. package/types/lib/third-party/arborist/lib/spec-from-lock.d.ts +3 -0
  694. package/types/lib/third-party/arborist/lib/spec-from-lock.d.ts.map +1 -0
  695. package/types/lib/third-party/arborist/lib/tree-check.d.ts +3 -0
  696. package/types/lib/third-party/arborist/lib/tree-check.d.ts.map +1 -0
  697. package/types/lib/third-party/arborist/lib/validate-npm-package-name.d.ts +13 -0
  698. package/types/lib/third-party/arborist/lib/validate-npm-package-name.d.ts.map +1 -0
  699. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts +6 -0
  700. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts.map +1 -0
  701. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts +35 -0
  702. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts.map +1 -0
  703. package/types/lib/validator/bomValidator.d.ts +40 -0
  704. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  705. package/types/lib/validator/complianceEngine.d.ts +72 -0
  706. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  707. package/types/lib/validator/complianceRules.d.ts +61 -0
  708. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  709. package/types/lib/validator/index.d.ts +101 -0
  710. package/types/lib/validator/index.d.ts.map +1 -0
  711. package/types/lib/validator/reporters/annotations.d.ts +37 -0
  712. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  713. package/types/lib/validator/reporters/console.d.ts +34 -0
  714. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  715. package/types/lib/validator/reporters/index.d.ts +30 -0
  716. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  717. package/types/lib/validator/reporters/json.d.ts +11 -0
  718. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  719. package/types/lib/validator/reporters/sarif.d.ts +22 -0
  720. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  721. package/mod.d.ts +0 -7
  722. package/mod.js +0 -22
@@ -0,0 +1,2800 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import process from "node:process";
5
+
6
+ import { createBom } from "../cli/index.js";
7
+ import {
8
+ isDryRun,
9
+ readEnvironmentVariable,
10
+ recordActivity,
11
+ } from "../core/activity.js";
12
+ import {
13
+ getTmpDir,
14
+ safeExistsSync,
15
+ safeMkdirSync,
16
+ safeMkdtempSync,
17
+ safeRmSync,
18
+ safeWriteSync,
19
+ } from "../core/fs.js";
20
+ import { thoughtLog } from "../core/logger.js";
21
+ import { dirNameStr } from "../core/paths.js";
22
+ import { PROJECT_CONFIG_FILENAMES } from "../core/projectConfig.js";
23
+ import { fetchPomXmlAsJson } from "../ecosystems/ecosystems.js";
24
+ import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "../inventory/auditCategories.js";
25
+ import {
26
+ DEFAULT_CDX_SPEC_VERSION,
27
+ getCycloneDxFormat,
28
+ getNonCycloneDxErrorMessage,
29
+ isCycloneDxBom,
30
+ } from "../inventory/bomUtils.js";
31
+ import {
32
+ collectPolicyViolations,
33
+ loadPolicy,
34
+ } from "../inventory/licenseEnhancer.js";
35
+ import {
36
+ hasRegistryProvenanceEvidenceProperties,
37
+ hasTrustedPublishingProperties,
38
+ } from "../inventory/provenanceUtils.js";
39
+ import {
40
+ cleanupSourceDir,
41
+ findGitRefForPurlVersion,
42
+ hardenedGitCommand,
43
+ isSafeGitRefName,
44
+ prefetchGitUrlSources,
45
+ resolveGitUrlFromPurl,
46
+ resolvePurlSourceDirectory,
47
+ sanitizeRemoteUrlForLogs,
48
+ } from "../inventory/source.js";
49
+ import { executeOsQuery } from "../managers/binary.js";
50
+ import { getBomWithOras } from "../managers/oci.js";
51
+ import {
52
+ auditBom,
53
+ isHbomLikeBom,
54
+ isObomLikeBom,
55
+ } from "../stages/postgen/auditBom.js";
56
+ import { postProcess } from "../stages/postgen/postgen.js";
57
+ import { formatTargetLabel } from "./progress.js";
58
+ import { renderAuditReport } from "./reporters.js";
59
+ import {
60
+ SEVERITY_ORDER,
61
+ scoreTargetRisk,
62
+ severityMeetsThreshold,
63
+ } from "./scoring.js";
64
+ import {
65
+ collectAuditTargets,
66
+ enrichInputBomsWithRegistryMetadata,
67
+ normalizePackageName,
68
+ } from "./targets.js";
69
+
70
+ /** Default predictive audit category list applied when none are explicitly requested. */
71
+ export const DEFAULT_AUDIT_CATEGORIES = [
72
+ "ai-agent",
73
+ "ci-permission",
74
+ "dependency-source",
75
+ "package-integrity",
76
+ ];
77
+
78
+ const DIRECT_AUDIT_TOOL_NAME = "cdx-audit";
79
+
80
+ const AUDIT_CACHE_DIRNAME = ".cdx-audit";
81
+ const AUDIT_CACHE_BOM_FILE = "source-bom.json";
82
+ const AUDIT_CACHE_META_FILE = "source-bom.meta.json";
83
+ const CLONE_RETRY_DELAYS_MS = [750, 1500];
84
+ const LARGE_PREDICTIVE_AUDIT_THRESHOLD = 50;
85
+ const VERY_LARGE_PREDICTIVE_AUDIT_THRESHOLD = 100;
86
+
87
+ const PYTHON_METADATA_FILES = ["pyproject.toml", "setup.cfg", "setup.py"];
88
+ const PYTHON_HEURISTIC_FILENAMES = new Set(["setup.py", "__init__.py"]);
89
+ const PYTHON_HEURISTIC_FILE_LIMIT = 32;
90
+ const PYTHON_HEURISTIC_MAX_FILE_BYTES = 256 * 1024;
91
+ const PYTHON_SKIP_DIRS = new Set([
92
+ ".git",
93
+ ".hg",
94
+ ".tox",
95
+ ".venv",
96
+ "__pycache__",
97
+ "build",
98
+ "dist",
99
+ "node_modules",
100
+ "site-packages",
101
+ "venv",
102
+ ]);
103
+
104
+ const PYTHON_EXECUTION_PATTERN =
105
+ /\b(?:exec|eval|compile)\s*\(|\b(?:subprocess\.(?:Popen|run|call|check_output)|os\.(?:system|popen))\b/i;
106
+
107
+ const PYTHON_NETWORK_PATTERN =
108
+ /\b(?:requests\.(?:get|post|put|patch)|urllib(?:\.request)?\.urlopen|http\.client|socket\.socket)\b/i;
109
+
110
+ const PYTHON_OBFUSCATION_PATTERN =
111
+ /\b(?:base64\.(?:b64decode|urlsafe_b64decode)|binascii\.a2b_base64|marshal\.loads|zlib\.decompress|codecs\.decode\s*\([^)]*base64|bytes\.fromhex)\b/i;
112
+
113
+ const PYTHON_SETUP_CMDCLASS_PATTERN = /\bcmdclass\s*=/i;
114
+
115
+ /**
116
+ * Read and validate a CycloneDX BOM file.
117
+ *
118
+ * @param {string} bomPath BOM file path
119
+ * @returns {object} parsed CycloneDX BOM
120
+ */
121
+ export function loadBomFile(bomPath) {
122
+ const resolvedPath = resolve(bomPath);
123
+ let bomJson;
124
+ try {
125
+ bomJson = JSON.parse(readFileSync(resolvedPath, "utf8"));
126
+ } catch (error) {
127
+ throw new Error(`Failed to parse ${resolvedPath}: ${error.message}`);
128
+ }
129
+ if (!isCycloneDxBom(bomJson)) {
130
+ throw new Error(getNonCycloneDxErrorMessage(bomJson, "cdx-audit"));
131
+ }
132
+ return bomJson;
133
+ }
134
+
135
+ /**
136
+ * Recursively list JSON files under a BOM directory.
137
+ *
138
+ * @param {string} bomDir directory path
139
+ * @returns {string[]} discovered file paths
140
+ */
141
+ export function listBomFiles(bomDir) {
142
+ const foundFiles = [];
143
+ const queue = [resolve(bomDir)];
144
+ while (queue.length) {
145
+ const currentDir = queue.shift();
146
+ const entries = readdirSync(currentDir, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ const entryPath = join(currentDir, entry.name);
149
+ if (entry.isDirectory()) {
150
+ queue.push(entryPath);
151
+ continue;
152
+ }
153
+ if (entry.isFile() && entry.name.endsWith(".json")) {
154
+ foundFiles.push(entryPath);
155
+ }
156
+ }
157
+ }
158
+ return foundFiles.sort();
159
+ }
160
+
161
+ /**
162
+ * Load input BOM files from either a single file or a directory.
163
+ *
164
+ * @param {object} options CLI options
165
+ * @returns {{ source: string, bomJson: object }[]} loaded input BOMs
166
+ */
167
+ export function loadInputBoms(options) {
168
+ const inputBoms = [];
169
+ if (options.bom) {
170
+ inputBoms.push({
171
+ bomJson: loadBomFile(options.bom),
172
+ source: resolve(options.bom),
173
+ });
174
+ }
175
+ if (options.bomDir) {
176
+ const bomFiles = listBomFiles(options.bomDir);
177
+ for (const bomFile of bomFiles) {
178
+ try {
179
+ inputBoms.push({
180
+ bomJson: loadBomFile(bomFile),
181
+ source: bomFile,
182
+ });
183
+ } catch (error) {
184
+ console.warn(
185
+ `Skipping non-CycloneDX JSON file '${bomFile}': ${error.message}`,
186
+ );
187
+ }
188
+ }
189
+ }
190
+ return inputBoms;
191
+ }
192
+
193
+ function summarizeDirectAuditFindings(findings = []) {
194
+ const findingsBySeverity = {
195
+ critical: 0,
196
+ high: 0,
197
+ low: 0,
198
+ medium: 0,
199
+ };
200
+ let maxSeverity = "none";
201
+ for (const finding of findings) {
202
+ const severity = finding?.severity || "low";
203
+ if (findingsBySeverity[severity] !== undefined) {
204
+ findingsBySeverity[severity] += 1;
205
+ }
206
+ if (
207
+ (SEVERITY_ORDER[severity] ?? -1) > (SEVERITY_ORDER[maxSeverity] ?? -1)
208
+ ) {
209
+ maxSeverity = severity;
210
+ }
211
+ }
212
+ return {
213
+ findingsBySeverity,
214
+ findingsCount: findings.length,
215
+ maxSeverity,
216
+ };
217
+ }
218
+
219
+ function buildDirectAuditOptions(bomJson, options = {}) {
220
+ const explicitCategories = options.categories?.length
221
+ ? options.categories.join(",")
222
+ : undefined;
223
+ return {
224
+ aiProvenance: options.aiProvenance,
225
+ bomAuditCategories:
226
+ explicitCategories ||
227
+ (isHbomLikeBom(bomJson)
228
+ ? DEFAULT_HBOM_AUDIT_CATEGORIES
229
+ : isObomLikeBom(bomJson)
230
+ ? "obom-runtime"
231
+ : undefined),
232
+ bomAuditMinSeverity: options.minSeverity || "low",
233
+ bomAuditRulesDir: options.rulesDir,
234
+ workspaceDir: options.workspaceDir,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Run the direct (post-generation) BOM policy audit over loaded input BOMs.
240
+ *
241
+ * Audits each supplied BOM with {@link auditBom}, collects per-BOM findings and
242
+ * summaries, and aggregates them into a single report keyed by `auditMode: "direct"`.
243
+ *
244
+ * @param {{ source: string, bomJson: object }[]} inputBoms Loaded CycloneDX BOM objects to audit
245
+ * @param {object} [options={}] CLI options including optional category, severity, and rules configuration
246
+ * @returns {Promise<object>} Aggregate direct audit report with per-BOM results and a rollup summary
247
+ */
248
+ export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
249
+ if (!inputBoms.length) {
250
+ throw new Error("No CycloneDX BOM inputs were found.");
251
+ }
252
+ const results = [];
253
+ for (const inputBom of inputBoms) {
254
+ const directAuditOptions = buildDirectAuditOptions(
255
+ inputBom.bomJson,
256
+ options,
257
+ );
258
+ const findings = await auditBom(inputBom.bomJson, {
259
+ ...directAuditOptions,
260
+ getBomWithOras,
261
+ });
262
+ results.push({
263
+ auditOptions: directAuditOptions,
264
+ bomFormat: getCycloneDxFormat(inputBom.bomJson),
265
+ findings,
266
+ serialNumber: inputBom.bomJson?.serialNumber,
267
+ source: inputBom.source,
268
+ specVersion: inputBom.bomJson?.specVersion,
269
+ status: "audited",
270
+ summary: summarizeDirectAuditFindings(findings),
271
+ });
272
+ }
273
+ const summary = {
274
+ findingsBySeverity: {
275
+ critical: 0,
276
+ high: 0,
277
+ low: 0,
278
+ medium: 0,
279
+ },
280
+ inputBomCount: inputBoms.length,
281
+ maxSeverity: "none",
282
+ totalFindings: 0,
283
+ bomsWithFindings: 0,
284
+ };
285
+ for (const result of results) {
286
+ summary.totalFindings += result.summary.findingsCount;
287
+ if (result.summary.findingsCount > 0) {
288
+ summary.bomsWithFindings += 1;
289
+ }
290
+ for (const [severity, count] of Object.entries(
291
+ result.summary.findingsBySeverity,
292
+ )) {
293
+ summary.findingsBySeverity[severity] += count;
294
+ }
295
+ if (
296
+ (SEVERITY_ORDER[result.summary.maxSeverity] ?? -1) >
297
+ (SEVERITY_ORDER[summary.maxSeverity] ?? -1)
298
+ ) {
299
+ summary.maxSeverity = result.summary.maxSeverity;
300
+ }
301
+ }
302
+ return {
303
+ auditMode: "direct",
304
+ generatedAt: new Date().toISOString(),
305
+ inputs: inputBoms.map((inputBom) => inputBom.source),
306
+ results,
307
+ summary,
308
+ tool: {
309
+ name: DIRECT_AUDIT_TOOL_NAME,
310
+ version: readPackageVersion(),
311
+ },
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Read the package version from the local package.json file.
317
+ *
318
+ * @returns {string} package version
319
+ */
320
+ function readPackageVersion() {
321
+ const packageJson = JSON.parse(
322
+ readFileSync(join(dirNameStr, "package.json"), "utf8"),
323
+ );
324
+ return packageJson.version;
325
+ }
326
+
327
+ /**
328
+ * Build a deterministic directory-safe slug for report and workspace paths.
329
+ *
330
+ * @param {object} target audit target
331
+ * @returns {string} slug string
332
+ */
333
+ function targetSlug(target) {
334
+ const packageName = target.namespace
335
+ ? `${target.namespace}-${target.name}`
336
+ : target.name;
337
+ const normalized = normalizePackageName(packageName)
338
+ .replace(/[^a-z0-9-]/g, "-")
339
+ .replace(/-+/g, "-")
340
+ .replace(/^-|-$/g, "");
341
+ const version = normalizePackageName(target.version || "latest") || "latest";
342
+ const digest = createHash("sha256")
343
+ .update(target.purl)
344
+ .digest("hex")
345
+ .slice(0, 12);
346
+ return `${target.type}-${normalized || "package"}-${version}-${digest}`;
347
+ }
348
+
349
+ /**
350
+ * Ensure a parent directory exists before writing a file.
351
+ *
352
+ * @param {string} filePath file path to create
353
+ * @param {string} content file content
354
+ * @returns {void}
355
+ */
356
+ function writeTextFile(filePath, content) {
357
+ const parentDir = dirname(filePath);
358
+ if (!safeExistsSync(parentDir)) {
359
+ safeMkdirSync(parentDir, { recursive: true });
360
+ }
361
+ safeWriteSync(filePath, content);
362
+ }
363
+
364
+ /**
365
+ * Ensure a parent directory exists before writing JSON.
366
+ *
367
+ * @param {string} filePath file path to create
368
+ * @param {object} payload JSON payload
369
+ * @returns {void}
370
+ */
371
+ function writeJsonFile(filePath, payload) {
372
+ writeTextFile(filePath, `${JSON.stringify(payload, null, 2)}\n`);
373
+ }
374
+
375
+ function sleep(ms) {
376
+ return new Promise((resolvePromise) => {
377
+ setTimeout(resolvePromise, ms);
378
+ });
379
+ }
380
+
381
+ function isPathWithin(parentDir, childPath) {
382
+ const normalizePath = (candidatePath) => {
383
+ try {
384
+ return realpathSync.native
385
+ ? realpathSync.native(candidatePath)
386
+ : realpathSync(candidatePath);
387
+ } catch {
388
+ return resolve(candidatePath);
389
+ }
390
+ };
391
+ const normalizedChild = normalizePath(childPath);
392
+ const candidateParents = [parentDir];
393
+ if (process.platform !== "win32") {
394
+ candidateParents.push("/tmp");
395
+ candidateParents.push("/private/tmp");
396
+ }
397
+ return candidateParents.some((candidateParent) => {
398
+ const normalizedParent = normalizePath(candidateParent);
399
+ return (
400
+ normalizedChild === normalizedParent ||
401
+ normalizedChild.startsWith(`${normalizedParent}/`)
402
+ );
403
+ });
404
+ }
405
+
406
+ function isTemporaryWorkspaceDir(workspaceDir) {
407
+ return workspaceDir ? isPathWithin(getTmpDir(), workspaceDir) : false;
408
+ }
409
+
410
+ function prepareWorkspaceContext(options = {}) {
411
+ if (!options.workspaceDir) {
412
+ return {
413
+ cleanupOnFinish: false,
414
+ workspaceDir: undefined,
415
+ };
416
+ }
417
+ const workspaceDir = resolve(options.workspaceDir);
418
+ const existed = safeExistsSync(workspaceDir);
419
+ if (!existed) {
420
+ safeMkdirSync(workspaceDir, { recursive: true });
421
+ }
422
+ return {
423
+ cleanupOnFinish: !existed && isTemporaryWorkspaceDir(workspaceDir),
424
+ workspaceDir,
425
+ };
426
+ }
427
+
428
+ function getWorkspaceTargetDir(workspaceDir, target) {
429
+ return join(resolve(workspaceDir), targetSlug(target));
430
+ }
431
+
432
+ function getWorkspaceCachePaths(workspaceDir, target) {
433
+ const targetDir = getWorkspaceTargetDir(workspaceDir, target);
434
+ const cacheDir = join(targetDir, AUDIT_CACHE_DIRNAME);
435
+ return {
436
+ cacheDir,
437
+ metadataFile: join(cacheDir, AUDIT_CACHE_META_FILE),
438
+ sourceBomFile: join(cacheDir, AUDIT_CACHE_BOM_FILE),
439
+ targetDir,
440
+ };
441
+ }
442
+
443
+ function loadCachedChildBom(workspaceDir, target) {
444
+ if (!workspaceDir) {
445
+ return undefined;
446
+ }
447
+ const cachePaths = getWorkspaceCachePaths(workspaceDir, target);
448
+ if (!safeExistsSync(cachePaths.sourceBomFile)) {
449
+ return undefined;
450
+ }
451
+ try {
452
+ const bomJson = loadBomFile(cachePaths.sourceBomFile);
453
+ let metadata = {};
454
+ if (safeExistsSync(cachePaths.metadataFile)) {
455
+ metadata = JSON.parse(readFileSync(cachePaths.metadataFile, "utf8"));
456
+ }
457
+ const scanDir = metadata.scanDirRelative
458
+ ? resolve(cachePaths.targetDir, metadata.scanDirRelative)
459
+ : cachePaths.targetDir;
460
+ return {
461
+ bomJson,
462
+ cacheDir: cachePaths.cacheDir,
463
+ repoUrl: metadata.repoUrl,
464
+ resolution: metadata.resolution,
465
+ scanDir,
466
+ sourceDirectoryConfidence: metadata.sourceDirectoryConfidence || "medium",
467
+ versionMatched: metadata.versionMatched !== false,
468
+ };
469
+ } catch {
470
+ return undefined;
471
+ }
472
+ }
473
+
474
+ function writeCachedChildBom(workspaceDir, target, payload) {
475
+ if (!workspaceDir || !payload?.bomJson) {
476
+ return;
477
+ }
478
+ const cachePaths = getWorkspaceCachePaths(workspaceDir, target);
479
+ safeMkdirSync(cachePaths.cacheDir, { recursive: true });
480
+ writeJsonFile(cachePaths.sourceBomFile, payload.bomJson);
481
+ writeJsonFile(cachePaths.metadataFile, {
482
+ generatedAt: new Date().toISOString(),
483
+ repoUrl: payload.repoUrl,
484
+ resolution: payload.resolution,
485
+ scanDirRelative: payload.scanDir
486
+ ? relative(cachePaths.targetDir, resolve(payload.scanDir)) || "."
487
+ : ".",
488
+ sourceDirectoryConfidence: payload.sourceDirectoryConfidence,
489
+ versionMatched: payload.versionMatched,
490
+ });
491
+ }
492
+
493
+ function persistAuditArtifacts(result, options, sourceBomJson) {
494
+ if (!options.reportsDir) {
495
+ return result;
496
+ }
497
+ const resultDir = join(
498
+ resolve(options.reportsDir),
499
+ targetSlug(result.target),
500
+ );
501
+ safeMkdirSync(resultDir, { recursive: true });
502
+ result.reportDir = resultDir;
503
+ result.findingsFile = join(resultDir, "findings.json");
504
+ result.summaryFile = join(resultDir, "summary.json");
505
+ if (sourceBomJson) {
506
+ result.sourceBomFile = join(resultDir, "source-bom.json");
507
+ writeJsonFile(result.sourceBomFile, sourceBomJson);
508
+ }
509
+ writeJsonFile(result.findingsFile, result.findings || []);
510
+ writeJsonFile(result.summaryFile, {
511
+ assessment: result.assessment,
512
+ cacheHit: result.cacheHit || false,
513
+ error: result.error,
514
+ errorType: result.errorType,
515
+ findingsCount: result.findings?.length || 0,
516
+ repoUrl: result.repoUrl,
517
+ sourceDirectoryConfidence: result.sourceDirectoryConfidence,
518
+ status: result.status,
519
+ target: result.target,
520
+ });
521
+ return result;
522
+ }
523
+
524
+ /**
525
+ * Emit a progress event when a callback is configured.
526
+ *
527
+ * @param {object} options CLI options
528
+ * @param {object} event progress event payload
529
+ * @returns {void}
530
+ */
531
+ function emitProgress(options, event) {
532
+ if (typeof options?.onProgress === "function") {
533
+ options.onProgress(event);
534
+ }
535
+ }
536
+
537
+ function buildPredictiveAuditEstimate(selectedTargets) {
538
+ if (selectedTargets >= VERY_LARGE_PREDICTIVE_AUDIT_THRESHOLD) {
539
+ return "This may take 10+ minutes depending on repository lookups and child SBOM generation.";
540
+ }
541
+ if (selectedTargets >= LARGE_PREDICTIVE_AUDIT_THRESHOLD) {
542
+ return "This may take several minutes depending on repository lookups and child SBOM generation.";
543
+ }
544
+ return undefined;
545
+ }
546
+
547
+ function buildPredictiveAuditPreflightMessage(extractedTargets, options) {
548
+ const selectedTargets = extractedTargets?.targets?.length || 0;
549
+ const allowlistedTargetsExcluded =
550
+ extractedTargets?.stats?.allowlistedTargetsExcluded || 0;
551
+ const availableTargets = extractedTargets?.stats?.availableTargets || 0;
552
+ const requiredTargets = extractedTargets?.stats?.requiredTargets || 0;
553
+ const trustedTargetsExcluded =
554
+ extractedTargets?.stats?.trustedTargetsExcluded || 0;
555
+ const truncatedTargets = extractedTargets?.stats?.truncatedTargets || 0;
556
+ const estimate = buildPredictiveAuditEstimate(selectedTargets);
557
+ const trustedHint = options?.trustedSelectionHelp
558
+ ? ` ${options.trustedSelectionHelp}`
559
+ : "";
560
+ const trustedExclusionMessage = trustedTargetsExcluded
561
+ ? ` Skipping ${trustedTargetsExcluded} trusted-publishing-backed package(s) by default.${trustedHint}`
562
+ : "";
563
+ const customAllowlistSuffix = options?.allowlistFile
564
+ ? " and your custom allowlist"
565
+ : "";
566
+ const allowlistExclusionMessage = allowlistedTargetsExcluded
567
+ ? ` Skipping ${allowlistedTargetsExcluded} allowlisted package(s) using the built-in well-known purl prefix filter${customAllowlistSuffix}.`
568
+ : "";
569
+ if (!estimate && availableTargets < LARGE_PREDICTIVE_AUDIT_THRESHOLD) {
570
+ const passiveMessage =
571
+ `${trustedExclusionMessage}${allowlistExclusionMessage}`.trim();
572
+ return passiveMessage || undefined;
573
+ }
574
+ if (options?.scope === "required") {
575
+ return `Predictive audit will scan ${selectedTargets} required package(s). ${estimate || "Large required-only scans may still take a while depending on repository lookups and child SBOM generation."}${trustedExclusionMessage}${allowlistExclusionMessage}`;
576
+ }
577
+ if (truncatedTargets > 0) {
578
+ const additionalTargets = Math.max(0, selectedTargets - requiredTargets);
579
+ return `Predictive audit selected ${selectedTargets} of ${availableTargets} package(s) (${requiredTargets} required${additionalTargets ? ` + ${additionalTargets} additional` : ""}) using required-first prioritization. ${estimate || "This run was trimmed to keep audit time reasonable."}${trustedExclusionMessage}${allowlistExclusionMessage}`;
580
+ }
581
+ return `Predictive audit will scan ${selectedTargets} package(s). ${estimate || "Large predictive audits may still take a while depending on repository lookups and child SBOM generation."}${trustedExclusionMessage}${allowlistExclusionMessage}`;
582
+ }
583
+
584
+ /**
585
+ * Read a custom property from a target descriptor.
586
+ *
587
+ * @param {object} target audit target
588
+ * @param {string} propertyName property name
589
+ * @returns {string | undefined} property value
590
+ */
591
+ function getTargetProperty(target, propertyName) {
592
+ return target?.properties?.find((property) => property.name === propertyName)
593
+ ?.value;
594
+ }
595
+
596
+ function getTargetNumberProperty(target, propertyName) {
597
+ const value = getTargetProperty(target, propertyName);
598
+ if (!value) {
599
+ return undefined;
600
+ }
601
+ const numericValue = Number(value);
602
+ return Number.isFinite(numericValue) ? numericValue : undefined;
603
+ }
604
+
605
+ function getTargetTimestampProperty(target, propertyName) {
606
+ const value = getTargetProperty(target, propertyName);
607
+ if (!value) {
608
+ return undefined;
609
+ }
610
+ const timestamp = Date.parse(value);
611
+ return Number.isNaN(timestamp) ? undefined : timestamp;
612
+ }
613
+
614
+ function getTargetListProperty(target, propertyName) {
615
+ const value = getTargetProperty(target, propertyName);
616
+ if (!value) {
617
+ return [];
618
+ }
619
+ return [
620
+ ...new Set(
621
+ value
622
+ .split(",")
623
+ .map((entry) => entry.trim())
624
+ .filter(Boolean),
625
+ ),
626
+ ];
627
+ }
628
+
629
+ function isEstablishedPackage(target, propertyPrefix) {
630
+ const packageCreatedTime = getTargetTimestampProperty(
631
+ target,
632
+ `${propertyPrefix}:packageCreatedTime`,
633
+ );
634
+ const versionCount = getTargetNumberProperty(
635
+ target,
636
+ `${propertyPrefix}:versionCount`,
637
+ );
638
+ if (!packageCreatedTime || !versionCount) {
639
+ return false;
640
+ }
641
+ const packageAgeMs = Date.now() - packageCreatedTime;
642
+ return packageAgeMs >= 1000 * 60 * 60 * 24 * 30 && versionCount >= 3;
643
+ }
644
+
645
+ function isRecentRelease(target, propertyPrefix) {
646
+ const publishTime = getTargetTimestampProperty(
647
+ target,
648
+ `${propertyPrefix}:publishTime`,
649
+ );
650
+ if (!publishTime) {
651
+ return false;
652
+ }
653
+ const releaseAgeMs = Date.now() - publishTime;
654
+ return releaseAgeMs >= 0 && releaseAgeMs <= 1000 * 60 * 60 * 72;
655
+ }
656
+
657
+ function hasPublisherDrift(target, propertyPrefix) {
658
+ return (
659
+ getTargetProperty(target, `${propertyPrefix}:publisherDrift`) === "true"
660
+ );
661
+ }
662
+
663
+ function hasMaintainerSetDrift(target, propertyPrefix) {
664
+ return (
665
+ getTargetProperty(target, `${propertyPrefix}:maintainerSetDrift`) ===
666
+ "true" ||
667
+ getTargetProperty(target, `${propertyPrefix}:uploaderSetDrift`) === "true"
668
+ );
669
+ }
670
+
671
+ function hasPartialIdentitySetDrift(target, propertyPrefix) {
672
+ const explicitPropertyName =
673
+ propertyPrefix === "cdx:npm"
674
+ ? `${propertyPrefix}:maintainerSetPartialDrift`
675
+ : `${propertyPrefix}:uploaderSetPartialDrift`;
676
+ if (getTargetProperty(target, explicitPropertyName) === "true") {
677
+ return true;
678
+ }
679
+ const currentPropertyName =
680
+ propertyPrefix === "cdx:npm"
681
+ ? `${propertyPrefix}:maintainerSet`
682
+ : `${propertyPrefix}:uploaderSet`;
683
+ const priorPropertyName =
684
+ propertyPrefix === "cdx:npm"
685
+ ? `${propertyPrefix}:priorMaintainerSet`
686
+ : `${propertyPrefix}:priorUploaderSet`;
687
+ const currentSet = getTargetListProperty(target, currentPropertyName);
688
+ const priorSet = getTargetListProperty(target, priorPropertyName);
689
+ if (!currentSet.length || !priorSet.length) {
690
+ return false;
691
+ }
692
+ const priorValues = new Set(priorSet);
693
+ const overlapCount = currentSet.filter((value) =>
694
+ priorValues.has(value),
695
+ ).length;
696
+ if (overlapCount === 0) {
697
+ return false;
698
+ }
699
+ const unionCount = new Set([...currentSet, ...priorSet]).size;
700
+ return (
701
+ overlapCount < unionCount &&
702
+ (overlapCount < currentSet.length || overlapCount < priorSet.length)
703
+ );
704
+ }
705
+
706
+ function hasDormantReleaseGapAnomaly(target, propertyPrefix) {
707
+ const currentGapDays = getTargetNumberProperty(
708
+ target,
709
+ `${propertyPrefix}:releaseGapDays`,
710
+ );
711
+ const baselineGapDays = getTargetNumberProperty(
712
+ target,
713
+ `${propertyPrefix}:releaseGapBaselineDays`,
714
+ );
715
+ const sampleSize = getTargetNumberProperty(
716
+ target,
717
+ `${propertyPrefix}:releaseGapSampleSize`,
718
+ );
719
+ if (!currentGapDays || !baselineGapDays || !sampleSize || sampleSize < 3) {
720
+ return false;
721
+ }
722
+ return currentGapDays >= Math.max(90, baselineGapDays * 8);
723
+ }
724
+
725
+ function hasCompressedCadence(target, propertyPrefix) {
726
+ if (
727
+ getTargetProperty(target, `${propertyPrefix}:compressedCadence`) === "true"
728
+ ) {
729
+ return true;
730
+ }
731
+ const currentGapDays = getTargetNumberProperty(
732
+ target,
733
+ `${propertyPrefix}:releaseGapDays`,
734
+ );
735
+ const baselineGapDays = getTargetNumberProperty(
736
+ target,
737
+ `${propertyPrefix}:releaseGapBaselineDays`,
738
+ );
739
+ const sampleSize = getTargetNumberProperty(
740
+ target,
741
+ `${propertyPrefix}:releaseGapSampleSize`,
742
+ );
743
+ if (
744
+ currentGapDays === undefined ||
745
+ baselineGapDays === undefined ||
746
+ sampleSize === undefined ||
747
+ sampleSize < 3 ||
748
+ currentGapDays <= 0 ||
749
+ baselineGapDays <= 0 ||
750
+ baselineGapDays < 21
751
+ ) {
752
+ return false;
753
+ }
754
+ return currentGapDays <= 14 && currentGapDays / baselineGapDays <= 0.33;
755
+ }
756
+
757
+ /**
758
+ * Build low-noise provenance-aware contextual findings from the root BOM target.
759
+ *
760
+ * These are intentionally conservative and only fire when there is explicit risk
761
+ * posture already present in the target metadata.
762
+ *
763
+ * @param {object} target audit target
764
+ * @returns {object[]} contextual findings
765
+ */
766
+ export function buildTargetContextFindings(target) {
767
+ const findings = [];
768
+ const hasTrustedPublishing = hasTrustedPublishingProperties(
769
+ target?.properties,
770
+ );
771
+ const hasProvenanceEvidence = hasRegistryProvenanceEvidenceProperties(
772
+ target?.properties,
773
+ );
774
+ if (target.type === "npm") {
775
+ const hasInstallScript =
776
+ getTargetProperty(target, "cdx:npm:hasInstallScript") === "true";
777
+ const establishedPackage = isEstablishedPackage(target, "cdx:npm");
778
+ const recentRelease = isRecentRelease(target, "cdx:npm");
779
+ const publisherDrift = hasPublisherDrift(target, "cdx:npm");
780
+ const maintainerSetDrift = hasMaintainerSetDrift(target, "cdx:npm");
781
+ const partialMaintainerSetDrift = hasPartialIdentitySetDrift(
782
+ target,
783
+ "cdx:npm",
784
+ );
785
+ const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
786
+ target,
787
+ "cdx:npm",
788
+ );
789
+ const compressedCadence = hasCompressedCadence(target, "cdx:npm");
790
+ if (
791
+ target.version &&
792
+ hasInstallScript &&
793
+ !hasTrustedPublishing &&
794
+ !hasProvenanceEvidence
795
+ ) {
796
+ findings.push({
797
+ category: "package-integrity",
798
+ description:
799
+ "Install-time execution combined with missing registry-visible provenance raises future tampering risk.",
800
+ location: {
801
+ bomRef: target.bomRefs?.[0],
802
+ purl: target.purl,
803
+ },
804
+ message: `npm package '${target.name}@${target.version}' has install-time execution hooks but no registry-visible trusted publishing or provenance evidence.`,
805
+ mitigation:
806
+ "Prefer versions with registry-visible provenance evidence, review install scripts carefully, and pin/allowlist publishers for high-risk packages.",
807
+ ruleId: "PROV-001",
808
+ severity: "medium",
809
+ });
810
+ }
811
+ if (
812
+ target.version &&
813
+ establishedPackage &&
814
+ recentRelease &&
815
+ hasInstallScript &&
816
+ !hasTrustedPublishing &&
817
+ !hasProvenanceEvidence
818
+ ) {
819
+ findings.push({
820
+ category: "package-integrity",
821
+ description:
822
+ "A very recent release on a mature package, combined with install-time execution and missing provenance, deserves extra scrutiny before adoption.",
823
+ location: {
824
+ bomRef: target.bomRefs?.[0],
825
+ purl: target.purl,
826
+ },
827
+ message: `npm package '${target.name}@${target.version}' is a very recent release on an established package and still lacks registry-visible provenance.`,
828
+ mitigation:
829
+ "Delay adoption briefly, verify publisher identity, and prefer registry-visible provenance for high-risk packages with install hooks.",
830
+ ruleId: "PROV-003",
831
+ severity: "low",
832
+ });
833
+ }
834
+ if (
835
+ target.version &&
836
+ establishedPackage &&
837
+ publisherDrift &&
838
+ hasInstallScript &&
839
+ !hasTrustedPublishing &&
840
+ !hasProvenanceEvidence
841
+ ) {
842
+ findings.push({
843
+ category: "package-integrity",
844
+ description:
845
+ "Publisher drift on mature packages can be legitimate, but becomes more concerning when install-time execution is present and provenance is weak.",
846
+ location: {
847
+ bomRef: target.bomRefs?.[0],
848
+ purl: target.purl,
849
+ },
850
+ message: `npm package '${target.name}@${target.version}' was published by a different identity than the prior release and lacks registry-visible provenance.`,
851
+ mitigation:
852
+ "Review maintainer changes, compare the prior release publisher, and validate provenance before upgrading execution-capable packages.",
853
+ ruleId: "PROV-004",
854
+ severity: "medium",
855
+ });
856
+ }
857
+ if (
858
+ target.version &&
859
+ establishedPackage &&
860
+ maintainerSetDrift &&
861
+ hasInstallScript &&
862
+ !hasTrustedPublishing &&
863
+ !hasProvenanceEvidence
864
+ ) {
865
+ findings.push({
866
+ category: "package-integrity",
867
+ description:
868
+ "Maintainer-set drift on execution-capable packages is a triage signal when the resolved release also lacks registry-visible provenance.",
869
+ location: {
870
+ bomRef: target.bomRefs?.[0],
871
+ purl: target.purl,
872
+ },
873
+ message: `npm package '${target.name}@${target.version}' has a fully different maintainer identity set than the prior release and lacks registry-visible provenance.`,
874
+ mitigation:
875
+ "Compare the prior and current maintainer sets, verify maintainer transitions, and prefer releases with provenance before upgrading packages with install hooks.",
876
+ ruleId: "PROV-007",
877
+ severity: "medium",
878
+ });
879
+ }
880
+ if (
881
+ target.version &&
882
+ establishedPackage &&
883
+ partialMaintainerSetDrift &&
884
+ !maintainerSetDrift &&
885
+ hasInstallScript &&
886
+ !hasTrustedPublishing &&
887
+ !hasProvenanceEvidence
888
+ ) {
889
+ findings.push({
890
+ category: "package-integrity",
891
+ description:
892
+ "Partial maintainer-set drift is a low-severity triage signal when execution-capable releases retain some identities but also introduce maintainer churn without registry-visible provenance.",
893
+ location: {
894
+ bomRef: target.bomRefs?.[0],
895
+ purl: target.purl,
896
+ },
897
+ message: `npm package '${target.name}@${target.version}' retains only part of the prior maintainer identity set and lacks registry-visible provenance.`,
898
+ mitigation:
899
+ "Review which maintainer identities changed, compare against the prior release, and validate the transition before upgrading packages with install hooks.",
900
+ ruleId: "PROV-011",
901
+ severity: "low",
902
+ });
903
+ }
904
+ if (
905
+ target.version &&
906
+ establishedPackage &&
907
+ dormantReleaseGapAnomaly &&
908
+ hasInstallScript &&
909
+ !hasTrustedPublishing &&
910
+ !hasProvenanceEvidence
911
+ ) {
912
+ findings.push({
913
+ category: "package-integrity",
914
+ description:
915
+ "A long dormant gap followed by a new execution-capable release can warrant a short review window when provenance is missing.",
916
+ location: {
917
+ bomRef: target.bomRefs?.[0],
918
+ purl: target.purl,
919
+ },
920
+ message: `npm package '${target.name}@${target.version}' arrived after an unusually long release gap and lacks registry-visible provenance.`,
921
+ mitigation:
922
+ "Review the release diff, compare against the prior version, and validate maintainer continuity before adopting after long dormancy.",
923
+ ruleId: "PROV-008",
924
+ severity: "low",
925
+ });
926
+ }
927
+ if (
928
+ target.version &&
929
+ establishedPackage &&
930
+ compressedCadence &&
931
+ hasInstallScript &&
932
+ !hasTrustedPublishing &&
933
+ !hasProvenanceEvidence
934
+ ) {
935
+ findings.push({
936
+ category: "package-integrity",
937
+ description:
938
+ "A materially faster-than-usual release on a mature execution-capable package is a low-severity review signal when registry-visible provenance is absent.",
939
+ location: {
940
+ bomRef: target.bomRefs?.[0],
941
+ purl: target.purl,
942
+ },
943
+ message: `npm package '${target.name}@${target.version}' arrived materially faster than its prior release cadence and lacks registry-visible provenance.`,
944
+ mitigation:
945
+ "Review the release diff, compare the release timing against prior cadence, and validate the publisher transition before rapid upgrades of execution-capable packages.",
946
+ ruleId: "PROV-012",
947
+ severity: "low",
948
+ });
949
+ }
950
+ }
951
+ if (target.type === "pypi") {
952
+ const registry = getTargetProperty(target, "cdx:pypi:registry");
953
+ const isDefaultRegistry =
954
+ !registry ||
955
+ ["https://pypi.org", "https://pypi.org/simple"].includes(registry);
956
+ const uploaderVerified =
957
+ getTargetProperty(target, "cdx:pypi:uploaderVerified") === "true";
958
+ const establishedPackage = isEstablishedPackage(target, "cdx:pypi");
959
+ const recentRelease = isRecentRelease(target, "cdx:pypi");
960
+ const publisherDrift = hasPublisherDrift(target, "cdx:pypi");
961
+ const maintainerSetDrift = hasMaintainerSetDrift(target, "cdx:pypi");
962
+ const partialMaintainerSetDrift = hasPartialIdentitySetDrift(
963
+ target,
964
+ "cdx:pypi",
965
+ );
966
+ const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
967
+ target,
968
+ "cdx:pypi",
969
+ );
970
+ const compressedCadence = hasCompressedCadence(target, "cdx:pypi");
971
+ if (
972
+ target.version &&
973
+ isDefaultRegistry &&
974
+ !hasTrustedPublishing &&
975
+ !hasProvenanceEvidence &&
976
+ !uploaderVerified
977
+ ) {
978
+ findings.push({
979
+ category: "package-integrity",
980
+ description:
981
+ "Default-registry PyPI packages without provenance or verified uploader context are weaker candidates for publisher-trust decisions.",
982
+ location: {
983
+ bomRef: target.bomRefs?.[0],
984
+ purl: target.purl,
985
+ },
986
+ message: `PyPI package '${target.name}@${target.version}' lacks registry-visible provenance and uploader verification signals.`,
987
+ mitigation:
988
+ "Prefer releases with provenance evidence or verified uploader metadata, especially for sensitive or newly introduced dependencies.",
989
+ ruleId: "PROV-002",
990
+ severity: "low",
991
+ });
992
+ }
993
+ if (
994
+ target.version &&
995
+ isDefaultRegistry &&
996
+ establishedPackage &&
997
+ recentRelease &&
998
+ !hasTrustedPublishing &&
999
+ !hasProvenanceEvidence &&
1000
+ !uploaderVerified
1001
+ ) {
1002
+ findings.push({
1003
+ category: "package-integrity",
1004
+ description:
1005
+ "Very recent releases on mature packages can benefit from a short review window when provenance and uploader-verification signals are absent.",
1006
+ location: {
1007
+ bomRef: target.bomRefs?.[0],
1008
+ purl: target.purl,
1009
+ },
1010
+ message: `PyPI package '${target.name}@${target.version}' is a very recent release on an established package without provenance or uploader verification signals.`,
1011
+ mitigation:
1012
+ "Delay adoption briefly, compare the release to the previous known-good version, and prefer verified/provenance-backed uploads for sensitive dependencies.",
1013
+ ruleId: "PROV-005",
1014
+ severity: "low",
1015
+ });
1016
+ }
1017
+ if (
1018
+ target.version &&
1019
+ isDefaultRegistry &&
1020
+ establishedPackage &&
1021
+ publisherDrift &&
1022
+ !hasTrustedPublishing &&
1023
+ !hasProvenanceEvidence &&
1024
+ !uploaderVerified
1025
+ ) {
1026
+ findings.push({
1027
+ category: "package-integrity",
1028
+ description:
1029
+ "Uploader drift on established PyPI packages is usually a triage signal, but becomes more meaningful when provenance and verification are missing.",
1030
+ location: {
1031
+ bomRef: target.bomRefs?.[0],
1032
+ purl: target.purl,
1033
+ },
1034
+ message: `PyPI package '${target.name}@${target.version}' was uploaded by a different identity than the prior release and lacks provenance or uploader verification signals.`,
1035
+ mitigation:
1036
+ "Review the uploader change, compare the prior release uploader, and validate project ownership before upgrading critical dependencies.",
1037
+ ruleId: "PROV-006",
1038
+ severity: "low",
1039
+ });
1040
+ }
1041
+ if (
1042
+ target.version &&
1043
+ isDefaultRegistry &&
1044
+ establishedPackage &&
1045
+ maintainerSetDrift &&
1046
+ !hasTrustedPublishing &&
1047
+ !hasProvenanceEvidence &&
1048
+ !uploaderVerified
1049
+ ) {
1050
+ findings.push({
1051
+ category: "package-integrity",
1052
+ description:
1053
+ "Uploader-set drift on established PyPI packages is a triage signal when provenance and uploader verification are absent.",
1054
+ location: {
1055
+ bomRef: target.bomRefs?.[0],
1056
+ purl: target.purl,
1057
+ },
1058
+ message: `PyPI package '${target.name}@${target.version}' has a fully different uploader identity set than the prior release and lacks provenance or uploader verification signals.`,
1059
+ mitigation:
1060
+ "Review uploader transitions, compare the prior release uploader set, and validate project ownership before upgrading sensitive dependencies.",
1061
+ ruleId: "PROV-009",
1062
+ severity: "low",
1063
+ });
1064
+ }
1065
+ if (
1066
+ target.version &&
1067
+ isDefaultRegistry &&
1068
+ establishedPackage &&
1069
+ partialMaintainerSetDrift &&
1070
+ !maintainerSetDrift &&
1071
+ !hasTrustedPublishing &&
1072
+ !hasProvenanceEvidence &&
1073
+ !uploaderVerified
1074
+ ) {
1075
+ findings.push({
1076
+ category: "package-integrity",
1077
+ description:
1078
+ "Partial uploader-set drift is a low-severity review signal on established PyPI packages when provenance and uploader verification are absent.",
1079
+ location: {
1080
+ bomRef: target.bomRefs?.[0],
1081
+ purl: target.purl,
1082
+ },
1083
+ message: `PyPI package '${target.name}@${target.version}' retains only part of the prior uploader identity set and lacks provenance or uploader verification signals.`,
1084
+ mitigation:
1085
+ "Review which uploader identities changed, compare the release against the prior version, and validate project ownership before upgrading sensitive dependencies.",
1086
+ ruleId: "PROV-013",
1087
+ severity: "low",
1088
+ });
1089
+ }
1090
+ if (
1091
+ target.version &&
1092
+ isDefaultRegistry &&
1093
+ establishedPackage &&
1094
+ dormantReleaseGapAnomaly &&
1095
+ !hasTrustedPublishing &&
1096
+ !hasProvenanceEvidence &&
1097
+ !uploaderVerified
1098
+ ) {
1099
+ findings.push({
1100
+ category: "package-integrity",
1101
+ description:
1102
+ "Established packages resurfacing after a long dormant gap benefit from extra review when provenance is weak.",
1103
+ location: {
1104
+ bomRef: target.bomRefs?.[0],
1105
+ purl: target.purl,
1106
+ },
1107
+ message: `PyPI package '${target.name}@${target.version}' followed an unusually long release gap and lacks provenance or uploader verification signals.`,
1108
+ mitigation:
1109
+ "Compare the release to the prior known-good version and review maintainership continuity before adopting after long dormancy.",
1110
+ ruleId: "PROV-010",
1111
+ severity: "low",
1112
+ });
1113
+ }
1114
+ if (
1115
+ target.version &&
1116
+ isDefaultRegistry &&
1117
+ establishedPackage &&
1118
+ compressedCadence &&
1119
+ !hasTrustedPublishing &&
1120
+ !hasProvenanceEvidence &&
1121
+ !uploaderVerified
1122
+ ) {
1123
+ findings.push({
1124
+ category: "package-integrity",
1125
+ description:
1126
+ "Materially faster-than-usual release timing is a low-severity triage signal on mature PyPI packages when provenance and uploader verification remain weak.",
1127
+ location: {
1128
+ bomRef: target.bomRefs?.[0],
1129
+ purl: target.purl,
1130
+ },
1131
+ message: `PyPI package '${target.name}@${target.version}' arrived materially faster than its prior release cadence and lacks provenance or uploader verification signals.`,
1132
+ mitigation:
1133
+ "Compare the release timing and contents against prior versions, then validate uploader continuity before rapid upgrades of sensitive dependencies.",
1134
+ ruleId: "PROV-014",
1135
+ severity: "low",
1136
+ });
1137
+ }
1138
+ }
1139
+ if (target.type === "cargo") {
1140
+ const yanked = getTargetProperty(target, "cdx:cargo:yanked") === "true";
1141
+ const establishedPackage = isEstablishedPackage(target, "cdx:cargo");
1142
+ const recentRelease = isRecentRelease(target, "cdx:cargo");
1143
+ const publisherDrift = hasPublisherDrift(target, "cdx:cargo");
1144
+ const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
1145
+ target,
1146
+ "cdx:cargo",
1147
+ );
1148
+ const compressedCadence = hasCompressedCadence(target, "cdx:cargo");
1149
+ if (target.version && yanked) {
1150
+ findings.push({
1151
+ category: "package-integrity",
1152
+ description:
1153
+ "Yanked crates are removed from normal Cargo resolution and usually indicate a correctness, security, or publisher-action issue that deserves review before further adoption.",
1154
+ location: {
1155
+ bomRef: target.bomRefs?.[0],
1156
+ purl: target.purl,
1157
+ },
1158
+ message: `Cargo crate '${target.name}@${target.version}' has been yanked from crates.io.`,
1159
+ mitigation:
1160
+ "Prefer a non-yanked release and review the crate's publisher history and changelog before upgrading.",
1161
+ ruleId: "PROV-015",
1162
+ severity: "high",
1163
+ });
1164
+ }
1165
+ if (
1166
+ target.version &&
1167
+ establishedPackage &&
1168
+ recentRelease &&
1169
+ !hasTrustedPublishing &&
1170
+ !hasProvenanceEvidence
1171
+ ) {
1172
+ findings.push({
1173
+ category: "package-integrity",
1174
+ description:
1175
+ "Very recent releases on established crates benefit from a short review window when trusted publishing and provenance remain weak.",
1176
+ location: {
1177
+ bomRef: target.bomRefs?.[0],
1178
+ purl: target.purl,
1179
+ },
1180
+ message: `Cargo crate '${target.name}@${target.version}' is a very recent release on an established package without registry-visible provenance signals.`,
1181
+ mitigation:
1182
+ "Delay adoption briefly, compare the release to the prior version, and prefer trusted-publishing-backed releases for sensitive crates.",
1183
+ ruleId: "PROV-016",
1184
+ severity: "low",
1185
+ });
1186
+ }
1187
+ if (
1188
+ target.version &&
1189
+ establishedPackage &&
1190
+ publisherDrift &&
1191
+ !hasTrustedPublishing &&
1192
+ !hasProvenanceEvidence
1193
+ ) {
1194
+ findings.push({
1195
+ category: "package-integrity",
1196
+ description:
1197
+ "Publisher drift on established crates is often benign, but becomes more meaningful when provenance and trusted publishing are absent.",
1198
+ location: {
1199
+ bomRef: target.bomRefs?.[0],
1200
+ purl: target.purl,
1201
+ },
1202
+ message: `Cargo crate '${target.name}@${target.version}' was published by a different identity than the prior release and lacks registry-visible provenance signals.`,
1203
+ mitigation:
1204
+ "Review the publisher transition, compare the prior release metadata, and validate ownership before upgrading sensitive crates.",
1205
+ ruleId: "PROV-017",
1206
+ severity: "medium",
1207
+ });
1208
+ }
1209
+ if (
1210
+ target.version &&
1211
+ establishedPackage &&
1212
+ (dormantReleaseGapAnomaly || compressedCadence) &&
1213
+ !hasTrustedPublishing &&
1214
+ !hasProvenanceEvidence
1215
+ ) {
1216
+ findings.push({
1217
+ category: "package-integrity",
1218
+ description:
1219
+ "Release timing anomalies on established crates are low-noise triage signals when provenance remains weak.",
1220
+ location: {
1221
+ bomRef: target.bomRefs?.[0],
1222
+ purl: target.purl,
1223
+ },
1224
+ message: `Cargo crate '${target.name}@${target.version}' shows unusual release timing and lacks registry-visible provenance signals.`,
1225
+ mitigation:
1226
+ "Review the release diff and timing versus prior versions before rapidly adopting the new crate release.",
1227
+ ruleId: "PROV-018",
1228
+ severity: "low",
1229
+ });
1230
+ }
1231
+ }
1232
+ return findings;
1233
+ }
1234
+
1235
+ /**
1236
+ * Clone a repository into a deterministic workspace directory.
1237
+ *
1238
+ * @param {string} repoUrl repository URL
1239
+ * @param {string} cloneDir target clone directory
1240
+ * @param {string | undefined} gitRef git ref to checkout
1241
+ * @throws {Error} when gitRef is not a safe ref name
1242
+ * @returns {void}
1243
+ */
1244
+ function cloneRepositoryToDir(repoUrl, cloneDir, gitRef) {
1245
+ const gitArgs = [
1246
+ "-c",
1247
+ "alias.clone=",
1248
+ "-c",
1249
+ "core.fsmonitor=false",
1250
+ "-c",
1251
+ "safe.bareRepository=explicit",
1252
+ "-c",
1253
+ "core.hooksPath=/dev/null",
1254
+ "clone",
1255
+ "--template=",
1256
+ "--depth",
1257
+ "1",
1258
+ "--",
1259
+ repoUrl,
1260
+ cloneDir,
1261
+ ];
1262
+ if (gitRef) {
1263
+ if (!isSafeGitRefName(gitRef)) {
1264
+ const error = new Error("git ref contains unsupported characters");
1265
+ error.errorType = "clone";
1266
+ error.retryable = false;
1267
+ throw error;
1268
+ }
1269
+ const cloneIndex = gitArgs.indexOf("clone");
1270
+ gitArgs.splice(cloneIndex + 1, 0, "--branch", gitRef);
1271
+ }
1272
+ const result = hardenedGitCommand(gitArgs);
1273
+ if (result.status !== 0) {
1274
+ const stderr = result.stderr
1275
+ ? result.stderr.toString()
1276
+ : "unknown git clone error";
1277
+ const error = new Error(stderr.trim());
1278
+ error.retryable =
1279
+ /(timed out|unable to connect|could not resolve host|network is unreachable|connection reset|connection refused|temporary failure|remote end hung up unexpectedly|http 5\d\d|tls|econnreset|econnrefused|etimedout)/i.test(
1280
+ stderr,
1281
+ );
1282
+ error.errorType = error.retryable ? "network" : "clone";
1283
+ throw error;
1284
+ }
1285
+ }
1286
+
1287
+ async function cloneRepositoryToDirWithRetry(repoUrl, cloneDir, gitRef) {
1288
+ let lastError;
1289
+ for (let attempt = 0; attempt <= CLONE_RETRY_DELAYS_MS.length; attempt += 1) {
1290
+ try {
1291
+ cloneRepositoryToDir(repoUrl, cloneDir, gitRef);
1292
+ return;
1293
+ } catch (error) {
1294
+ lastError = error;
1295
+ safeRmSync(cloneDir, { force: true, recursive: true });
1296
+ if (!error?.retryable || attempt >= CLONE_RETRY_DELAYS_MS.length) {
1297
+ break;
1298
+ }
1299
+ await sleep(CLONE_RETRY_DELAYS_MS[attempt]);
1300
+ }
1301
+ }
1302
+ const sanitizedRepoUrl = sanitizeRemoteUrlForLogs(repoUrl);
1303
+ const message = lastError?.message || "unknown git clone error";
1304
+ const error = new Error(
1305
+ `Unable to clone '${sanitizedRepoUrl}' after ${CLONE_RETRY_DELAYS_MS.length + 1} attempt(s): ${message}`,
1306
+ );
1307
+ error.errorType = lastError?.errorType || "clone";
1308
+ error.retryable = Boolean(lastError?.retryable);
1309
+ throw error;
1310
+ }
1311
+
1312
+ /**
1313
+ * Reuse or create a checkout for a target repository.
1314
+ *
1315
+ * @param {object} target audit target
1316
+ * @param {object} resolution resolved repository metadata
1317
+ * @param {string | undefined} workspaceDir workspace directory
1318
+ * @param {string | undefined} gitRef git ref to checkout
1319
+ * @returns {{ cleanup: boolean, cloneDir: string, reused: boolean }} checkout info
1320
+ */
1321
+ async function ensureCheckout(target, resolution, workspaceDir, gitRef) {
1322
+ if (!workspaceDir) {
1323
+ const cloneDir = safeMkdtempSync(
1324
+ join(getTmpDir(), `${targetSlug(target)}-`),
1325
+ );
1326
+ await cloneRepositoryToDirWithRetry(resolution.repoUrl, cloneDir, gitRef);
1327
+ return {
1328
+ cleanup: true,
1329
+ cloneDir,
1330
+ reused: false,
1331
+ };
1332
+ }
1333
+ const resolvedWorkspaceDir = resolve(workspaceDir);
1334
+ if (!safeExistsSync(resolvedWorkspaceDir)) {
1335
+ safeMkdirSync(resolvedWorkspaceDir, { recursive: true });
1336
+ }
1337
+ const cloneDir = join(resolvedWorkspaceDir, targetSlug(target));
1338
+ if (safeExistsSync(join(cloneDir, ".git"))) {
1339
+ return {
1340
+ cleanup: false,
1341
+ cloneDir,
1342
+ reused: true,
1343
+ };
1344
+ }
1345
+ if (safeExistsSync(cloneDir)) {
1346
+ safeRmSync(cloneDir, { force: true, recursive: true });
1347
+ }
1348
+ await cloneRepositoryToDirWithRetry(resolution.repoUrl, cloneDir, gitRef);
1349
+ return {
1350
+ cleanup: false,
1351
+ cloneDir,
1352
+ reused: false,
1353
+ };
1354
+ }
1355
+
1356
+ /**
1357
+ * Extract an expected package name from Python packaging metadata.
1358
+ *
1359
+ * @param {string} filePath metadata file path
1360
+ * @returns {string | undefined} discovered package name
1361
+ */
1362
+ function readPythonPackageName(filePath) {
1363
+ let fileContent;
1364
+ try {
1365
+ fileContent = readFileSync(filePath, "utf8");
1366
+ } catch {
1367
+ return undefined;
1368
+ }
1369
+ const patterns = [
1370
+ /(^|\n)\s*name\s*=\s*["']([^"'\n]+)["']/m,
1371
+ /(^|\n)\s*name\s*=\s*([^\n#]+)/m,
1372
+ /setup\s*\([^)]*name\s*=\s*["']([^"']+)["']/ms,
1373
+ ];
1374
+ for (const pattern of patterns) {
1375
+ const match = fileContent.match(pattern);
1376
+ if (!match) {
1377
+ continue;
1378
+ }
1379
+ const packageName = (match[2] || match[1] || "").trim();
1380
+ if (packageName) {
1381
+ return packageName;
1382
+ }
1383
+ }
1384
+ return undefined;
1385
+ }
1386
+
1387
+ /**
1388
+ * Resolve the most specific Python package directory inside a cloned repo.
1389
+ *
1390
+ * @param {string} cloneDir cloned repository root
1391
+ * @param {object} target audit target
1392
+ * @returns {{ confidence: string, scanDir: string }} selected directory and confidence
1393
+ */
1394
+ export function resolvePythonSourceDirectory(cloneDir, target) {
1395
+ const normalizedTargetName = normalizePackageName(target.name);
1396
+ const queue = [cloneDir];
1397
+ const matches = [];
1398
+ while (queue.length) {
1399
+ const currentDir = queue.shift();
1400
+ let entries = [];
1401
+ try {
1402
+ entries = readdirSync(currentDir, { withFileTypes: true });
1403
+ } catch {
1404
+ continue;
1405
+ }
1406
+ for (const entry of entries) {
1407
+ const entryPath = join(currentDir, entry.name);
1408
+ if (entry.isDirectory()) {
1409
+ if (!PYTHON_SKIP_DIRS.has(entry.name)) {
1410
+ queue.push(entryPath);
1411
+ }
1412
+ continue;
1413
+ }
1414
+ if (!entry.isFile() || !PYTHON_METADATA_FILES.includes(entry.name)) {
1415
+ continue;
1416
+ }
1417
+ const packageName = readPythonPackageName(entryPath);
1418
+ if (normalizePackageName(packageName) === normalizedTargetName) {
1419
+ matches.push(currentDir);
1420
+ }
1421
+ }
1422
+ }
1423
+ if (!matches.length) {
1424
+ return {
1425
+ confidence: "low",
1426
+ scanDir: cloneDir,
1427
+ };
1428
+ }
1429
+ matches.sort((left, right) => left.length - right.length);
1430
+ return {
1431
+ confidence: matches[0] === cloneDir ? "medium" : "high",
1432
+ scanDir: matches[0],
1433
+ };
1434
+ }
1435
+
1436
+ /**
1437
+ * Remove cdxgen config files from a directory the audit is about to scan.
1438
+ *
1439
+ * Audit mode clones repositories cdxgen picked itself, from registry metadata,
1440
+ * so nobody approved the contents. A config file in such a checkout would let
1441
+ * the audited repository choose its own scan options, which is the audited
1442
+ * repository deciding how it gets audited.
1443
+ *
1444
+ * @param {string} dir directory to clear
1445
+ * @returns {void}
1446
+ */
1447
+ function rejectProjectConfigFiles(dir) {
1448
+ if (!dir) {
1449
+ return;
1450
+ }
1451
+ for (const configFileName of PROJECT_CONFIG_FILENAMES) {
1452
+ const configPath = join(dir, configFileName);
1453
+ if (!safeExistsSync(configPath)) {
1454
+ continue;
1455
+ }
1456
+ safeRmSync(configPath, { force: true });
1457
+ console.warn(
1458
+ `Ignoring the config file '${configFileName}' found in the cloned repository ${dir}.`,
1459
+ );
1460
+ recordActivity({
1461
+ kind: "inspect",
1462
+ reason:
1463
+ "Removed a cdxgen config file from an audit checkout. Cloned repositories do not configure their own audit.",
1464
+ status: "completed",
1465
+ target: configPath,
1466
+ });
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * Resolve the most appropriate scan directory for a cloned target repository.
1472
+ *
1473
+ * @param {string} cloneDir cloned repository root
1474
+ * @param {object} target audit target
1475
+ * @param {object} resolution repository resolution metadata
1476
+ * @returns {{ confidence: string, scanDir: string }} selected directory and confidence
1477
+ */
1478
+ export function resolveTargetSourceDirectory(cloneDir, target, resolution) {
1479
+ const selection = selectTargetSourceDirectory(cloneDir, target, resolution);
1480
+ rejectProjectConfigFiles(cloneDir);
1481
+ if (selection.scanDir !== cloneDir) {
1482
+ rejectProjectConfigFiles(selection.scanDir);
1483
+ }
1484
+ return selection;
1485
+ }
1486
+
1487
+ /**
1488
+ * Pick the scan directory for a cloned target repository.
1489
+ *
1490
+ * @param {string} cloneDir cloned repository root
1491
+ * @param {object} target audit target
1492
+ * @param {object} resolution repository resolution metadata
1493
+ * @returns {{ confidence: string, scanDir: string }} selected directory and confidence
1494
+ */
1495
+ function selectTargetSourceDirectory(cloneDir, target, resolution) {
1496
+ if (target.type === "npm") {
1497
+ const scanDir = resolvePurlSourceDirectory(cloneDir, resolution);
1498
+ if (!scanDir) {
1499
+ return {
1500
+ confidence: "medium",
1501
+ scanDir: cloneDir,
1502
+ };
1503
+ }
1504
+ return {
1505
+ confidence: scanDir === cloneDir ? "medium" : "high",
1506
+ scanDir,
1507
+ };
1508
+ }
1509
+ if (target.type === "pypi") {
1510
+ return resolvePythonSourceDirectory(cloneDir, target);
1511
+ }
1512
+ return {
1513
+ confidence: "low",
1514
+ scanDir: cloneDir,
1515
+ };
1516
+ }
1517
+
1518
+ function collectPythonHeuristicFiles(scanDir) {
1519
+ const candidates = [];
1520
+ const queue = [scanDir];
1521
+ while (queue.length && candidates.length < PYTHON_HEURISTIC_FILE_LIMIT) {
1522
+ const currentDir = queue.shift();
1523
+ let entries = [];
1524
+ try {
1525
+ entries = readdirSync(currentDir, { withFileTypes: true });
1526
+ } catch {
1527
+ continue;
1528
+ }
1529
+ for (const entry of entries) {
1530
+ const entryPath = join(currentDir, entry.name);
1531
+ if (entry.isDirectory()) {
1532
+ if (!PYTHON_SKIP_DIRS.has(entry.name)) {
1533
+ queue.push(entryPath);
1534
+ }
1535
+ continue;
1536
+ }
1537
+ if (
1538
+ entry.isFile() &&
1539
+ PYTHON_HEURISTIC_FILENAMES.has(entry.name) &&
1540
+ candidates.length < PYTHON_HEURISTIC_FILE_LIMIT
1541
+ ) {
1542
+ candidates.push(entryPath);
1543
+ }
1544
+ }
1545
+ }
1546
+ return candidates;
1547
+ }
1548
+
1549
+ function inspectPythonHeuristicFile(filePath) {
1550
+ let fileSize;
1551
+ try {
1552
+ fileSize = statSync(filePath).size;
1553
+ } catch {
1554
+ return undefined;
1555
+ }
1556
+ if (fileSize > PYTHON_HEURISTIC_MAX_FILE_BYTES) {
1557
+ return undefined;
1558
+ }
1559
+ let fileContent;
1560
+ try {
1561
+ fileContent = readFileSync(filePath, "utf8");
1562
+ } catch {
1563
+ return undefined;
1564
+ }
1565
+ const indicators = [];
1566
+ if (PYTHON_EXECUTION_PATTERN.test(fileContent)) {
1567
+ indicators.push("process-or-dynamic-execution");
1568
+ }
1569
+ if (PYTHON_NETWORK_PATTERN.test(fileContent)) {
1570
+ indicators.push("network-access");
1571
+ }
1572
+ if (PYTHON_OBFUSCATION_PATTERN.test(fileContent)) {
1573
+ indicators.push("encoded-loader");
1574
+ }
1575
+ if (
1576
+ filePath.endsWith("setup.py") &&
1577
+ PYTHON_SETUP_CMDCLASS_PATTERN.test(fileContent)
1578
+ ) {
1579
+ indicators.push("setup-cmdclass");
1580
+ }
1581
+ return indicators.length ? indicators : undefined;
1582
+ }
1583
+
1584
+ /**
1585
+ * Build shallow predictive findings for suspicious Python packaging files.
1586
+ *
1587
+ * Phase 1 intentionally focuses on high-signal packaging surfaces (`setup.py`
1588
+ * and package `__init__.py`) until deeper Python static analysis is added.
1589
+ *
1590
+ * @param {string} scanDir cloned repository scan directory
1591
+ * @param {object} target audit target
1592
+ * @returns {object[]} predictive findings
1593
+ */
1594
+ export function buildPythonSourceHeuristicFindings(scanDir, target) {
1595
+ if (!scanDir || target?.type !== "pypi") {
1596
+ return [];
1597
+ }
1598
+ const findings = [];
1599
+ collectPythonHeuristicFiles(scanDir).forEach((filePath) => {
1600
+ const indicators = inspectPythonHeuristicFile(filePath);
1601
+ if (!indicators?.length) {
1602
+ return;
1603
+ }
1604
+ const relativeFile = relative(scanDir, filePath) || filePath;
1605
+ if (
1606
+ relativeFile.endsWith("setup.py") &&
1607
+ indicators.includes("encoded-loader") &&
1608
+ (indicators.includes("process-or-dynamic-execution") ||
1609
+ indicators.includes("network-access") ||
1610
+ indicators.includes("setup-cmdclass"))
1611
+ ) {
1612
+ findings.push({
1613
+ category: "package-integrity",
1614
+ description:
1615
+ "setup.py contains encoded or dynamically executed packaging logic, which is a strong signal of install-time code injection risk.",
1616
+ evidence: {
1617
+ indicators: indicators.join(","),
1618
+ },
1619
+ location: {
1620
+ bomRef: target.bomRefs?.[0],
1621
+ file: relativeFile,
1622
+ purl: target.purl,
1623
+ },
1624
+ message: `PyPI package '${target.name}@${target.version}' contains suspicious setup.py execution patterns in '${relativeFile}'.`,
1625
+ mitigation:
1626
+ "Inspect setup.py before installation, compare against prior releases, and avoid executing packaging hooks until the encoded or dynamic logic is explained and validated.",
1627
+ ruleId: "PYSRC-001",
1628
+ severity: "high",
1629
+ });
1630
+ return;
1631
+ }
1632
+ if (
1633
+ relativeFile.endsWith("__init__.py") &&
1634
+ (indicators.includes("process-or-dynamic-execution") ||
1635
+ indicators.includes("network-access"))
1636
+ ) {
1637
+ findings.push({
1638
+ category: "package-integrity",
1639
+ description:
1640
+ "__init__.py appears to perform process spawning, dynamic execution, or network access during import, which is unusual for a package initializer.",
1641
+ evidence: {
1642
+ indicators: indicators.join(","),
1643
+ },
1644
+ location: {
1645
+ bomRef: target.bomRefs?.[0],
1646
+ file: relativeFile,
1647
+ purl: target.purl,
1648
+ },
1649
+ message: `PyPI package '${target.name}@${target.version}' contains suspicious import-time logic in '${relativeFile}'.`,
1650
+ mitigation:
1651
+ "Review the initializer for import-time side effects, compare it to prior versions, and quarantine the release until maintainers confirm the added behavior.",
1652
+ ruleId: "PYSRC-002",
1653
+ severity: indicators.includes("encoded-loader") ? "high" : "medium",
1654
+ });
1655
+ }
1656
+ });
1657
+ return findings;
1658
+ }
1659
+
1660
+ /**
1661
+ * Build cdxgen options for a child source scan.
1662
+ *
1663
+ * @param {object} options CLI options
1664
+ * @param {object} target audit target
1665
+ * @returns {object} createBom options
1666
+ */
1667
+ function buildChildOptions(options, target) {
1668
+ const projectType =
1669
+ target.type === "npm"
1670
+ ? ["js"]
1671
+ : target.type === "pypi"
1672
+ ? ["py"]
1673
+ : target.type === "cargo"
1674
+ ? ["cargo", "github"]
1675
+ : [target.type];
1676
+ return {
1677
+ deep: true,
1678
+ failOnError: false,
1679
+ filePath: options.workspaceDir || process.cwd(),
1680
+ includeFormulation: true,
1681
+ installDeps: false,
1682
+ multiProject: true,
1683
+ profile: "threat-modeling",
1684
+ projectType,
1685
+ specVersion: DEFAULT_CDX_SPEC_VERSION,
1686
+ };
1687
+ }
1688
+
1689
+ function normalizeFindingFile(filePath) {
1690
+ return String(filePath)
1691
+ .replace(/\\/g, "/")
1692
+ .replace(/\/+/g, "/")
1693
+ .replace(/^\.\//, "");
1694
+ }
1695
+
1696
+ function sourceFindingKey(finding) {
1697
+ if (!finding?.ruleId || !finding?.location?.file) {
1698
+ return undefined;
1699
+ }
1700
+ return `${finding.ruleId}|${normalizeFindingFile(finding.location.file)}`;
1701
+ }
1702
+
1703
+ /**
1704
+ * Recheck high/critical findings against the default branch to identify issues
1705
+ * that have already been fixed upstream. This reduces false positives caused by
1706
+ * scanning a version-specific tag where an issue existed but has since been
1707
+ * resolved in the project's main development line.
1708
+ *
1709
+ * @param {object[]} findings findings from version-specific scan
1710
+ * @param {object} target audit target
1711
+ * @param {object} resolution repository resolution metadata
1712
+ * @param {string[]} categories active audit categories
1713
+ * @param {object} options CLI options
1714
+ * @returns {Promise<{findings: object[], fixedInDefaultBranch: number}|undefined>}
1715
+ */
1716
+ async function recheckFindingsAgainstDefaultBranch(
1717
+ findings,
1718
+ target,
1719
+ resolution,
1720
+ categories,
1721
+ options,
1722
+ ) {
1723
+ let defaultBranchDir;
1724
+ try {
1725
+ defaultBranchDir = safeMkdtempSync(
1726
+ join(getTmpDir(), `${targetSlug(target)}-default-`),
1727
+ );
1728
+ await cloneRepositoryToDirWithRetry(
1729
+ resolution.repoUrl,
1730
+ defaultBranchDir,
1731
+ undefined,
1732
+ );
1733
+ const defaultSourceSelection = resolveTargetSourceDirectory(
1734
+ defaultBranchDir,
1735
+ target,
1736
+ resolution,
1737
+ );
1738
+ const childOptions = {
1739
+ ...buildChildOptions(options, target),
1740
+ executeOsQuery,
1741
+ };
1742
+ const bomNSData =
1743
+ (await createBom(defaultSourceSelection.scanDir, childOptions)) || {};
1744
+ if (!bomNSData?.bomJson) {
1745
+ return undefined;
1746
+ }
1747
+ const processedBomNSData = postProcess(
1748
+ bomNSData,
1749
+ childOptions,
1750
+ defaultSourceSelection.scanDir,
1751
+ );
1752
+ const defaultBranchFindings = await auditBom(processedBomNSData.bomJson, {
1753
+ bomAuditCategories: categories.join(","),
1754
+ bomAuditMinSeverity: options.minSeverity || "low",
1755
+ bomAuditRulesDir: options.rulesDir,
1756
+ getBomWithOras,
1757
+ });
1758
+ const defaultBranchSourceFindings = defaultBranchFindings.concat(
1759
+ buildPythonSourceHeuristicFindings(
1760
+ defaultSourceSelection.scanDir,
1761
+ target,
1762
+ ),
1763
+ );
1764
+ // Build a set of finding signatures present in the default branch
1765
+ const defaultBranchFindingKeys = new Set();
1766
+ for (const finding of defaultBranchSourceFindings) {
1767
+ const key = sourceFindingKey(finding);
1768
+ if (key) {
1769
+ defaultBranchFindingKeys.add(key);
1770
+ }
1771
+ }
1772
+ // Retain findings that are still present in the default branch or are
1773
+ // contextual/heuristic findings (not from rule evaluation against source).
1774
+ // Source-derived findings have a location.file pointing to a repo file,
1775
+ // while contextual findings reference purls or bomRefs only.
1776
+ const retainedFindings = [];
1777
+ let fixedCount = 0;
1778
+ for (const finding of findings) {
1779
+ const isSourceDerivedFinding = Boolean(finding?.location?.file);
1780
+ if (!isSourceDerivedFinding) {
1781
+ // Contextual/heuristic findings are not source-dependent
1782
+ retainedFindings.push(finding);
1783
+ continue;
1784
+ }
1785
+ if (defaultBranchFindingKeys.has(sourceFindingKey(finding))) {
1786
+ retainedFindings.push(finding);
1787
+ } else {
1788
+ fixedCount += 1;
1789
+ thoughtLog("Finding fixed in default branch, removing.", {
1790
+ ruleId: finding.ruleId,
1791
+ file: finding?.location?.file,
1792
+ purl: target.purl,
1793
+ });
1794
+ }
1795
+ }
1796
+ if (fixedCount === 0) {
1797
+ return undefined;
1798
+ }
1799
+ return {
1800
+ findings: retainedFindings,
1801
+ fixedInDefaultBranch: fixedCount,
1802
+ };
1803
+ } catch (error) {
1804
+ const errorMessage = error?.message ?? String(error);
1805
+ thoughtLog("Default branch recheck failed, keeping original findings.", {
1806
+ error: errorMessage,
1807
+ errorType: error?.errorType,
1808
+ purl: target.purl,
1809
+ });
1810
+ return undefined;
1811
+ } finally {
1812
+ if (defaultBranchDir) {
1813
+ cleanupSourceDir(defaultBranchDir);
1814
+ }
1815
+ }
1816
+ }
1817
+
1818
+ /**
1819
+ * Analyze a single purl target by generating a child SBOM and auditing it.
1820
+ *
1821
+ * @param {object} target audit target
1822
+ * @param {object} options CLI options
1823
+ * @returns {Promise<object>} analyzed target result
1824
+ */
1825
+ export async function auditTarget(target, options) {
1826
+ const categories = options.categories?.length
1827
+ ? options.categories
1828
+ : DEFAULT_AUDIT_CATEGORIES;
1829
+ const targetIndex = options._targetIndex || 0;
1830
+ const targetTotal = options._targetTotal || 0;
1831
+ const targetLabel = formatTargetLabel(target);
1832
+ const originalFetchPackageMetadata = readEnvironmentVariable(
1833
+ "CDXGEN_FETCH_PKG_METADATA",
1834
+ );
1835
+ let checkout;
1836
+ let processedBomJson;
1837
+ let resolution;
1838
+ let sourceSelection;
1839
+ let cacheHit = false;
1840
+ let sanitizedRepoUrl;
1841
+ let versionMatched = false;
1842
+ try {
1843
+ const cachedChildBom = loadCachedChildBom(options.workspaceDir, target);
1844
+ if (cachedChildBom) {
1845
+ cacheHit = true;
1846
+ processedBomJson = cachedChildBom.bomJson;
1847
+ resolution = cachedChildBom.resolution;
1848
+ sanitizedRepoUrl = cachedChildBom.repoUrl;
1849
+ sourceSelection = {
1850
+ confidence: cachedChildBom.sourceDirectoryConfidence,
1851
+ scanDir: cachedChildBom.scanDir,
1852
+ };
1853
+ versionMatched = cachedChildBom.versionMatched;
1854
+ emitProgress(options, {
1855
+ index: targetIndex,
1856
+ label: targetLabel,
1857
+ target,
1858
+ total: targetTotal,
1859
+ type: "target:stage",
1860
+ stage: "reusing cached child SBOM",
1861
+ });
1862
+ } else {
1863
+ emitProgress(options, {
1864
+ index: targetIndex,
1865
+ label: targetLabel,
1866
+ target,
1867
+ total: targetTotal,
1868
+ type: "target:stage",
1869
+ stage: "resolving repository metadata",
1870
+ });
1871
+ resolution = await resolveGitUrlFromPurl(target.purl, {
1872
+ fetchPomXmlAsJson,
1873
+ });
1874
+ if (!resolution?.repoUrl) {
1875
+ return persistAuditArtifacts(
1876
+ {
1877
+ assessment: scoreTargetRisk([], target, {
1878
+ skipReason:
1879
+ "Unable to resolve repository URL from purl metadata.",
1880
+ }),
1881
+ error: "Unable to resolve repository URL from purl metadata.",
1882
+ findings: [],
1883
+ resolution,
1884
+ status: "skipped",
1885
+ target,
1886
+ },
1887
+ options,
1888
+ );
1889
+ }
1890
+ sanitizedRepoUrl = sanitizeRemoteUrlForLogs(resolution.repoUrl);
1891
+ thoughtLog("Preparing predictive audit target.", {
1892
+ purl: target.purl,
1893
+ repoUrl: sanitizedRepoUrl,
1894
+ });
1895
+ const gitRef = findGitRefForPurlVersion(resolution.repoUrl, resolution);
1896
+ versionMatched = Boolean(gitRef);
1897
+ emitProgress(options, {
1898
+ index: targetIndex,
1899
+ label: targetLabel,
1900
+ target,
1901
+ total: targetTotal,
1902
+ type: "target:stage",
1903
+ stage: gitRef ? `cloning source at ref ${gitRef}` : "cloning source",
1904
+ });
1905
+ checkout = await ensureCheckout(
1906
+ target,
1907
+ resolution,
1908
+ options.workspaceDir,
1909
+ gitRef,
1910
+ );
1911
+ sourceSelection = resolveTargetSourceDirectory(
1912
+ checkout.cloneDir,
1913
+ target,
1914
+ resolution,
1915
+ );
1916
+ const childOptions = {
1917
+ ...buildChildOptions(options, target),
1918
+ executeOsQuery,
1919
+ };
1920
+ process.env.CDXGEN_FETCH_PKG_METADATA = "true";
1921
+ emitProgress(options, {
1922
+ index: targetIndex,
1923
+ label: targetLabel,
1924
+ target,
1925
+ total: targetTotal,
1926
+ type: "target:stage",
1927
+ stage: "generating child SBOM",
1928
+ });
1929
+ const bomNSData =
1930
+ (await createBom(sourceSelection.scanDir, childOptions)) || {};
1931
+ if (!bomNSData?.bomJson) {
1932
+ return persistAuditArtifacts(
1933
+ {
1934
+ assessment: scoreTargetRisk([], target, {
1935
+ errorMessage:
1936
+ "Unable to generate a child SBOM for the resolved source repository.",
1937
+ scanError: true,
1938
+ }),
1939
+ error:
1940
+ "Unable to generate a child SBOM for the resolved source repository.",
1941
+ errorType: "sbom-generation",
1942
+ findings: [],
1943
+ repoUrl: sanitizedRepoUrl,
1944
+ resolution,
1945
+ status: "error",
1946
+ target,
1947
+ },
1948
+ options,
1949
+ );
1950
+ }
1951
+ const processedBomNSData = postProcess(
1952
+ bomNSData,
1953
+ childOptions,
1954
+ sourceSelection.scanDir,
1955
+ );
1956
+ processedBomJson = processedBomNSData.bomJson;
1957
+ writeCachedChildBom(options.workspaceDir, target, {
1958
+ bomJson: processedBomJson,
1959
+ repoUrl: sanitizedRepoUrl,
1960
+ resolution,
1961
+ scanDir: sourceSelection.scanDir,
1962
+ sourceDirectoryConfidence: sourceSelection.confidence,
1963
+ versionMatched,
1964
+ });
1965
+ }
1966
+ emitProgress(options, {
1967
+ index: targetIndex,
1968
+ label: targetLabel,
1969
+ target,
1970
+ total: targetTotal,
1971
+ type: "target:stage",
1972
+ stage: "evaluating audit rules",
1973
+ });
1974
+ const findings = await auditBom(processedBomJson, {
1975
+ bomAuditCategories: categories.join(","),
1976
+ bomAuditMinSeverity: options.minSeverity || "low",
1977
+ bomAuditRulesDir: options.rulesDir,
1978
+ getBomWithOras,
1979
+ });
1980
+ // Evaluate the license policy against this dependency's child SBOM so
1981
+ // source-derived (and deeper transitive) licenses are covered, not just the
1982
+ // licenses declared in the parent BOM.
1983
+ const licenseViolations = options._licensePolicy
1984
+ ? collectPolicyViolations(processedBomJson, options._licensePolicy, {
1985
+ includeWarnings: true,
1986
+ })
1987
+ : [];
1988
+ const contextualFindings = buildTargetContextFindings(target);
1989
+ const pythonSourceFindings = buildPythonSourceHeuristicFindings(
1990
+ sourceSelection.scanDir,
1991
+ target,
1992
+ );
1993
+ let predictiveFindings = findings.concat(
1994
+ contextualFindings,
1995
+ pythonSourceFindings,
1996
+ );
1997
+ let assessment = scoreTargetRisk(predictiveFindings, target, {
1998
+ bomJson: processedBomJson,
1999
+ repoReused: Boolean(checkout?.reused || cacheHit),
2000
+ resolution,
2001
+ sourceDirectoryConfidence: sourceSelection.confidence,
2002
+ versionMatched,
2003
+ });
2004
+ // When we scanned a version-specific ref and the severity is high or
2005
+ // critical, recheck against the default branch to reduce false positives
2006
+ // from issues already fixed upstream.
2007
+ if (
2008
+ versionMatched &&
2009
+ !options.skipDefaultBranchRecheck &&
2010
+ severityMeetsThreshold(assessment.severity, "high") &&
2011
+ resolution?.repoUrl
2012
+ ) {
2013
+ emitProgress(options, {
2014
+ index: targetIndex,
2015
+ label: targetLabel,
2016
+ target,
2017
+ total: targetTotal,
2018
+ type: "target:stage",
2019
+ stage: "rechecking findings against default branch",
2020
+ });
2021
+ const recheckResult = await recheckFindingsAgainstDefaultBranch(
2022
+ predictiveFindings,
2023
+ target,
2024
+ resolution,
2025
+ categories,
2026
+ options,
2027
+ );
2028
+ if (recheckResult) {
2029
+ predictiveFindings = recheckResult.findings;
2030
+ assessment = scoreTargetRisk(predictiveFindings, target, {
2031
+ bomJson: processedBomJson,
2032
+ repoReused: Boolean(checkout?.reused || cacheHit),
2033
+ resolution,
2034
+ sourceDirectoryConfidence: sourceSelection.confidence,
2035
+ versionMatched,
2036
+ });
2037
+ assessment.defaultBranchRechecked = true;
2038
+ assessment.findingsFixedInDefaultBranch =
2039
+ recheckResult.fixedInDefaultBranch;
2040
+ }
2041
+ }
2042
+ return persistAuditArtifacts(
2043
+ {
2044
+ assessment,
2045
+ cacheHit,
2046
+ findings: predictiveFindings,
2047
+ licenseViolations,
2048
+ repoUrl: sanitizedRepoUrl,
2049
+ resolution,
2050
+ scanDir: sourceSelection.scanDir,
2051
+ sourceDirectoryConfidence: sourceSelection.confidence,
2052
+ status: "audited",
2053
+ target,
2054
+ },
2055
+ options,
2056
+ processedBomJson,
2057
+ );
2058
+ } catch (error) {
2059
+ return persistAuditArtifacts(
2060
+ {
2061
+ assessment: scoreTargetRisk([], target, {
2062
+ errorMessage: error.message,
2063
+ scanError: true,
2064
+ }),
2065
+ error: error.message,
2066
+ errorType: error?.errorType || "runtime",
2067
+ findings: [],
2068
+ repoUrl: sanitizedRepoUrl,
2069
+ resolution,
2070
+ sourceDirectoryConfidence: sourceSelection?.confidence,
2071
+ status: "error",
2072
+ target,
2073
+ },
2074
+ options,
2075
+ processedBomJson,
2076
+ );
2077
+ } finally {
2078
+ if (originalFetchPackageMetadata === undefined) {
2079
+ delete process.env.CDXGEN_FETCH_PKG_METADATA;
2080
+ } else {
2081
+ process.env.CDXGEN_FETCH_PKG_METADATA = originalFetchPackageMetadata;
2082
+ }
2083
+ if (checkout?.cleanup) {
2084
+ cleanupSourceDir(checkout.cloneDir);
2085
+ }
2086
+ }
2087
+ }
2088
+
2089
+ /**
2090
+ * Build an aggregate summary for all analyzed targets.
2091
+ *
2092
+ * @param {object[]} inputBoms loaded BOMs
2093
+ * @param {object[]} results target results
2094
+ * @param {object[]} skipped skipped component entries
2095
+ * @returns {object} summary object
2096
+ */
2097
+ function summarizeAudit(inputBoms, results, skipped) {
2098
+ const analysisErrorCounts = {};
2099
+ const severityCounts = {
2100
+ critical: 0,
2101
+ high: 0,
2102
+ low: 0,
2103
+ medium: 0,
2104
+ none: 0,
2105
+ };
2106
+ let scannedTargets = 0;
2107
+ let erroredTargets = 0;
2108
+ for (const result of results) {
2109
+ const severity = result?.assessment?.severity || "none";
2110
+ severityCounts[severity] = (severityCounts[severity] || 0) + 1;
2111
+ if (result.status === "audited") {
2112
+ scannedTargets += 1;
2113
+ }
2114
+ if (result.status === "error") {
2115
+ erroredTargets += 1;
2116
+ const errorType = result?.errorType || "runtime";
2117
+ analysisErrorCounts[errorType] =
2118
+ (analysisErrorCounts[errorType] || 0) + 1;
2119
+ }
2120
+ }
2121
+ return {
2122
+ analysisErrorCounts,
2123
+ erroredTargets,
2124
+ inputBomCount: inputBoms.length,
2125
+ scannedTargets,
2126
+ severityCounts,
2127
+ skippedTargets:
2128
+ skipped.length +
2129
+ results.filter((result) => result.status === "skipped").length,
2130
+ totalTargets: results.length,
2131
+ };
2132
+ }
2133
+
2134
+ function normalizeRepoGroupingValue(repoUrl) {
2135
+ if (!repoUrl || typeof repoUrl !== "string") {
2136
+ return undefined;
2137
+ }
2138
+ return repoUrl
2139
+ .trim()
2140
+ .replace(/\.git$/i, "")
2141
+ .toLowerCase();
2142
+ }
2143
+
2144
+ function preferredResult(left, right) {
2145
+ const leftSeverity = SEVERITY_ORDER[left?.assessment?.severity] ?? -1;
2146
+ const rightSeverity = SEVERITY_ORDER[right?.assessment?.severity] ?? -1;
2147
+ if (leftSeverity !== rightSeverity) {
2148
+ return leftSeverity > rightSeverity ? left : right;
2149
+ }
2150
+ const leftScore = left?.assessment?.score || 0;
2151
+ const rightScore = right?.assessment?.score || 0;
2152
+ return leftScore >= rightScore ? left : right;
2153
+ }
2154
+
2155
+ function dedupeFindings(findings) {
2156
+ const seen = new Set();
2157
+ const deduped = [];
2158
+ for (const finding of findings || []) {
2159
+ const key = [
2160
+ finding?.ruleId,
2161
+ finding?.message,
2162
+ finding?.location?.file,
2163
+ finding?.location?.purl,
2164
+ finding?.location?.bomRef,
2165
+ ].join("|");
2166
+ if (seen.has(key)) {
2167
+ continue;
2168
+ }
2169
+ seen.add(key);
2170
+ deduped.push(finding);
2171
+ }
2172
+ return deduped;
2173
+ }
2174
+
2175
+ function getNamespaceGroupingKey(result) {
2176
+ if (
2177
+ result?.status !== "audited" ||
2178
+ result?.target?.type !== "npm" ||
2179
+ !result?.target?.namespace ||
2180
+ (result?.assessment?.severity || "none") === "none"
2181
+ ) {
2182
+ return undefined;
2183
+ }
2184
+ const categories = Object.keys(
2185
+ result.assessment?.categoryCounts || {},
2186
+ ).sort();
2187
+ const ruleIds = [
2188
+ ...new Set((result.findings || []).map((f) => f.ruleId).filter(Boolean)),
2189
+ ].sort();
2190
+ if (!categories.length || !ruleIds.length) {
2191
+ return undefined;
2192
+ }
2193
+ return `${result.target.namespace}|${categories.join(",")}|${ruleIds.join(",")}`;
2194
+ }
2195
+
2196
+ function getCargoRepositoryGroupingKey(result) {
2197
+ const normalizedRepoUrl = normalizeRepoGroupingValue(result?.repoUrl);
2198
+ if (
2199
+ result?.status !== "audited" ||
2200
+ result?.target?.type !== "cargo" ||
2201
+ !normalizedRepoUrl ||
2202
+ (result?.assessment?.severity || "none") === "none" ||
2203
+ !Array.isArray(result?.findings) ||
2204
+ !result.findings.length
2205
+ ) {
2206
+ return undefined;
2207
+ }
2208
+ const categories = new Set();
2209
+ const ruleIds = new Set();
2210
+ for (const finding of result.findings) {
2211
+ if (!finding?.category || !finding?.ruleId) {
2212
+ return undefined;
2213
+ }
2214
+ if (finding.category === "ci-permission") {
2215
+ return undefined;
2216
+ }
2217
+ categories.add(finding.category);
2218
+ ruleIds.add(finding.ruleId);
2219
+ }
2220
+ if (!categories.size || !ruleIds.size) {
2221
+ return undefined;
2222
+ }
2223
+ return `${normalizedRepoUrl}|cargo|${[...categories].sort().join(",")}|${[...ruleIds].sort().join(",")}`;
2224
+ }
2225
+
2226
+ function getSharedRepoCiGroupingKey(result) {
2227
+ const normalizedRepoUrl = normalizeRepoGroupingValue(result?.repoUrl);
2228
+ if (
2229
+ result?.status !== "audited" ||
2230
+ !normalizedRepoUrl ||
2231
+ !Array.isArray(result?.findings) ||
2232
+ !result.findings.length
2233
+ ) {
2234
+ return undefined;
2235
+ }
2236
+ const categories = new Set();
2237
+ const ruleIds = new Set();
2238
+ for (const finding of result.findings) {
2239
+ const category = finding?.category;
2240
+ const ruleId = finding?.ruleId;
2241
+ const findingFile = finding?.location?.file || "";
2242
+ if (
2243
+ category !== "ci-permission" ||
2244
+ !ruleId?.startsWith("CI-") ||
2245
+ (findingFile && !findingFile.includes(".github/workflows"))
2246
+ ) {
2247
+ return undefined;
2248
+ }
2249
+ categories.add(category);
2250
+ ruleIds.add(ruleId);
2251
+ }
2252
+ return `${normalizedRepoUrl}|${[...categories].sort().join(",")}|${[...ruleIds].sort().join(",")}`;
2253
+ }
2254
+
2255
+ function getGroupingDescriptor(result, sharedRepoCiGroupCounts) {
2256
+ const sharedRepoCiKey = getSharedRepoCiGroupingKey(result);
2257
+ if (sharedRepoCiKey) {
2258
+ const groupSize = sharedRepoCiGroupCounts?.get(sharedRepoCiKey) || 0;
2259
+ if (groupSize > 1) {
2260
+ return {
2261
+ key: sharedRepoCiKey,
2262
+ kind: "shared-repo-ci",
2263
+ };
2264
+ }
2265
+ }
2266
+ const cargoRepositoryKey = getCargoRepositoryGroupingKey(result);
2267
+ if (cargoRepositoryKey) {
2268
+ return {
2269
+ key: cargoRepositoryKey,
2270
+ kind: "cargo-repository",
2271
+ };
2272
+ }
2273
+ const namespaceKey = getNamespaceGroupingKey(result);
2274
+ if (!namespaceKey) {
2275
+ return undefined;
2276
+ }
2277
+ return {
2278
+ key: namespaceKey,
2279
+ kind: "npm-namespace",
2280
+ };
2281
+ }
2282
+
2283
+ function consolidateCargoRepositoryResult(group) {
2284
+ const representative = group.reduce((best, result) =>
2285
+ preferredResult(best, result),
2286
+ );
2287
+ const allBomRefs = [
2288
+ ...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
2289
+ ];
2290
+ const groupedPurls = [
2291
+ ...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
2292
+ ];
2293
+ const mergedFindings = dedupeFindings(
2294
+ group.flatMap((result) => result.findings || []),
2295
+ );
2296
+ const categoryCounts = {};
2297
+ for (const result of group) {
2298
+ for (const [category, count] of Object.entries(
2299
+ result.assessment?.categoryCounts || {},
2300
+ )) {
2301
+ categoryCounts[category] = (categoryCounts[category] || 0) + count;
2302
+ }
2303
+ }
2304
+ const reasons = [
2305
+ `${group.length} Cargo packages resolved to the same repository and shared the same predictive pattern, so cdx-audit consolidated them into one alert.`,
2306
+ ...(representative.assessment?.reasons || []),
2307
+ ];
2308
+ return {
2309
+ ...representative,
2310
+ assessment: {
2311
+ ...representative.assessment,
2312
+ categoryCounts,
2313
+ findingsCount: mergedFindings.length,
2314
+ reasons: [...new Set(reasons)],
2315
+ },
2316
+ findings: mergedFindings,
2317
+ grouping: {
2318
+ kind: "cargo-repository",
2319
+ label: `cargo:${representative.repoUrl}`,
2320
+ memberCount: group.length,
2321
+ repoUrl: representative.repoUrl,
2322
+ groupedPurls,
2323
+ },
2324
+ target: {
2325
+ ...representative.target,
2326
+ bomRefs: allBomRefs,
2327
+ name: "*",
2328
+ version: undefined,
2329
+ },
2330
+ };
2331
+ }
2332
+
2333
+ function consolidateSharedRepoCiResult(group) {
2334
+ const representative = group.reduce((best, result) =>
2335
+ preferredResult(best, result),
2336
+ );
2337
+ const allBomRefs = [
2338
+ ...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
2339
+ ];
2340
+ const groupedPurls = [
2341
+ ...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
2342
+ ];
2343
+ const mergedFindings = dedupeFindings(
2344
+ group.flatMap((result) => result.findings || []),
2345
+ );
2346
+ const reasons = [
2347
+ `${group.length} packages resolved to the same repository and shared the same CI findings, so cdx-audit consolidated them into one alert.`,
2348
+ ...(representative.assessment?.reasons || []),
2349
+ ];
2350
+ return {
2351
+ ...representative,
2352
+ assessment: {
2353
+ ...representative.assessment,
2354
+ categoryCounts: {
2355
+ "ci-permission": mergedFindings.length,
2356
+ },
2357
+ findingsCount: mergedFindings.length,
2358
+ reasons: [...new Set(reasons)],
2359
+ },
2360
+ findings: mergedFindings,
2361
+ grouping: {
2362
+ kind: "shared-repo-ci",
2363
+ label: representative.repoUrl,
2364
+ memberCount: group.length,
2365
+ repoUrl: representative.repoUrl,
2366
+ groupedPurls,
2367
+ },
2368
+ target: {
2369
+ ...representative.target,
2370
+ bomRefs: allBomRefs,
2371
+ name: "*",
2372
+ version: undefined,
2373
+ },
2374
+ };
2375
+ }
2376
+
2377
+ function consolidateNamespaceResult(group) {
2378
+ const representative = group.reduce((best, result) =>
2379
+ preferredResult(best, result),
2380
+ );
2381
+ const allBomRefs = [
2382
+ ...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
2383
+ ];
2384
+ const groupedPurls = [
2385
+ ...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
2386
+ ];
2387
+ const mergedFindings = dedupeFindings(
2388
+ group.flatMap((result) => result.findings || []),
2389
+ );
2390
+ const categoryCounts = {};
2391
+ for (const result of group) {
2392
+ for (const [category, count] of Object.entries(
2393
+ result.assessment?.categoryCounts || {},
2394
+ )) {
2395
+ categoryCounts[category] = (categoryCounts[category] || 0) + count;
2396
+ }
2397
+ }
2398
+ const reasons = [
2399
+ `${group.length} npm packages under namespace '${representative.target.namespace}' shared the same predictive pattern and were consolidated into one alert.`,
2400
+ ...(representative.assessment?.reasons || []),
2401
+ ];
2402
+ return {
2403
+ ...representative,
2404
+ assessment: {
2405
+ ...representative.assessment,
2406
+ categoryCounts,
2407
+ findingsCount: mergedFindings.length,
2408
+ reasons: [...new Set(reasons)],
2409
+ },
2410
+ findings: mergedFindings,
2411
+ grouping: {
2412
+ kind: "npm-namespace",
2413
+ label: `npm:${representative.target.namespace}/*`,
2414
+ memberCount: group.length,
2415
+ namespace: representative.target.namespace,
2416
+ groupedPurls,
2417
+ },
2418
+ target: {
2419
+ ...representative.target,
2420
+ bomRefs: allBomRefs,
2421
+ name: "*",
2422
+ version: undefined,
2423
+ },
2424
+ };
2425
+ }
2426
+
2427
+ /**
2428
+ * Consolidate per-target audit results into shared repo/CI alert groups.
2429
+ *
2430
+ * Groups results that share a common repository/CI provenance (or cargo
2431
+ * repository/namespace) into consolidated alert entries while preserving
2432
+ * single-target results unchanged.
2433
+ *
2434
+ * @param {object[]} results Per-target audit result objects
2435
+ * @returns {object[]} Grouped result objects with consolidated entries where applicable
2436
+ */
2437
+ export function groupAuditResults(results) {
2438
+ const groupedResults = [];
2439
+ const orderedEntries = [];
2440
+ const resultGroups = new Map();
2441
+ const sharedRepoCiGroupCounts = new Map();
2442
+ for (const result of results) {
2443
+ const sharedRepoCiKey = getSharedRepoCiGroupingKey(result);
2444
+ if (!sharedRepoCiKey) {
2445
+ continue;
2446
+ }
2447
+ sharedRepoCiGroupCounts.set(
2448
+ sharedRepoCiKey,
2449
+ (sharedRepoCiGroupCounts.get(sharedRepoCiKey) || 0) + 1,
2450
+ );
2451
+ }
2452
+ for (const result of results) {
2453
+ const descriptor = getGroupingDescriptor(result, sharedRepoCiGroupCounts);
2454
+ if (!descriptor) {
2455
+ orderedEntries.push({ result, type: "single" });
2456
+ continue;
2457
+ }
2458
+ if (!resultGroups.has(descriptor.key)) {
2459
+ resultGroups.set(descriptor.key, []);
2460
+ orderedEntries.push({ descriptor, type: "group" });
2461
+ }
2462
+ resultGroups.get(descriptor.key).push(result);
2463
+ }
2464
+ for (const entry of orderedEntries) {
2465
+ if (entry.type === "single") {
2466
+ groupedResults.push(entry.result);
2467
+ continue;
2468
+ }
2469
+ const group = resultGroups.get(entry.descriptor.key) || [];
2470
+ if (group.length <= 1) {
2471
+ groupedResults.push(group[0]);
2472
+ continue;
2473
+ }
2474
+ groupedResults.push(
2475
+ entry.descriptor.kind === "shared-repo-ci"
2476
+ ? consolidateSharedRepoCiResult(group)
2477
+ : entry.descriptor.kind === "cargo-repository"
2478
+ ? consolidateCargoRepositoryResult(group)
2479
+ : consolidateNamespaceResult(group),
2480
+ );
2481
+ }
2482
+ return groupedResults;
2483
+ }
2484
+
2485
+ function summarizeGroupedResults(results) {
2486
+ const severityCounts = {
2487
+ critical: 0,
2488
+ high: 0,
2489
+ low: 0,
2490
+ medium: 0,
2491
+ none: 0,
2492
+ };
2493
+ for (const result of results) {
2494
+ const severity = result?.assessment?.severity || "none";
2495
+ severityCounts[severity] = (severityCounts[severity] || 0) + 1;
2496
+ }
2497
+ return {
2498
+ groupedResultCount: results.length,
2499
+ groupedSeverityCounts: severityCounts,
2500
+ };
2501
+ }
2502
+
2503
+ /**
2504
+ * Run the predictive audit flow from one or more already-loaded CycloneDX BOM inputs.
2505
+ *
2506
+ * @param {{ source: string, bomJson: object }[]} inputBoms loaded CycloneDX BOM objects
2507
+ * @param {object} options CLI options
2508
+ * @returns {Promise<object>} aggregate audit report
2509
+ */
2510
+ export async function runAuditFromBoms(inputBoms, options) {
2511
+ if (!inputBoms.length) {
2512
+ throw new Error("No CycloneDX BOM inputs were found.");
2513
+ }
2514
+ const shouldEnrichRegistryMetadata =
2515
+ !isDryRun && options.trusted !== "include";
2516
+ if (shouldEnrichRegistryMetadata) {
2517
+ await enrichInputBomsWithRegistryMetadata(inputBoms);
2518
+ }
2519
+ const targetSelectionOptions = {
2520
+ allowlistFile: options.allowlistFile,
2521
+ maxTargets: options.maxTargets,
2522
+ prioritizeDirectRuntime: options.prioritizeDirectRuntime,
2523
+ scope: options.scope,
2524
+ trusted: options.trusted,
2525
+ };
2526
+ const extractedTargets = collectAuditTargets(
2527
+ inputBoms,
2528
+ targetSelectionOptions,
2529
+ );
2530
+ if (isDryRun) {
2531
+ // Dry-run mode intentionally stops after target planning. Registry metadata
2532
+ // enrichment, repository cloning, and child SBOM generation are all
2533
+ // side-effecting or outbound behaviors that the predictive audit must avoid.
2534
+ const report = {
2535
+ dryRun: true,
2536
+ generatedAt: new Date().toISOString(),
2537
+ groupedResults: [],
2538
+ inputs: inputBoms.map((inputBom) => inputBom.source),
2539
+ results: extractedTargets.targets.map((target) => ({
2540
+ assessment: {
2541
+ categoryCounts: {},
2542
+ confidenceLabel: "none",
2543
+ findingsCount: 0,
2544
+ reasons: [
2545
+ "Dry run mode planned this predictive audit target from the input BOM but skipped registry metadata fetches, upstream repository cloning, and child SBOM generation.",
2546
+ ],
2547
+ score: 0,
2548
+ severity: "none",
2549
+ },
2550
+ findings: [],
2551
+ status: "skipped",
2552
+ target,
2553
+ })),
2554
+ skipped: extractedTargets.skipped,
2555
+ summary: {
2556
+ predictiveDryRun: true,
2557
+ },
2558
+ tool: {
2559
+ name: "cdx-audit",
2560
+ version: readPackageVersion(),
2561
+ },
2562
+ };
2563
+ Object.assign(
2564
+ report.summary,
2565
+ summarizeAudit(inputBoms, report.results, extractedTargets.skipped),
2566
+ summarizeGroupedResults(report.groupedResults),
2567
+ );
2568
+ recordActivity({
2569
+ kind: "audit",
2570
+ reason:
2571
+ "Dry run mode planned predictive BOM audit targets from the input BOM but skipped registry metadata fetches, repository cloning, and child SBOM generation.",
2572
+ status: extractedTargets.targets.length ? "blocked" : "completed",
2573
+ target: "predictive-dependency-audit",
2574
+ });
2575
+ return report;
2576
+ }
2577
+ const results = [];
2578
+ const preflightMessage = buildPredictiveAuditPreflightMessage(
2579
+ extractedTargets,
2580
+ options,
2581
+ );
2582
+ if (preflightMessage) {
2583
+ emitProgress(options, {
2584
+ message: preflightMessage,
2585
+ total: extractedTargets.targets.length,
2586
+ type: "run:info",
2587
+ });
2588
+ }
2589
+ if (extractedTargets.targets.length) {
2590
+ emitProgress(options, {
2591
+ total: extractedTargets.targets.length,
2592
+ type: "run:start",
2593
+ });
2594
+ // Every target's repository lookup is known here, before the first clone,
2595
+ // so they are resolved in one round rather than one per iteration below.
2596
+ await prefetchGitUrlSources(
2597
+ extractedTargets.targets.map((target) => target.purl),
2598
+ );
2599
+ }
2600
+ for (const [index, target] of extractedTargets.targets.entries()) {
2601
+ const targetIndex = index + 1;
2602
+ emitProgress(options, {
2603
+ index: targetIndex,
2604
+ label: formatTargetLabel(target),
2605
+ target,
2606
+ total: extractedTargets.targets.length,
2607
+ type: "target:start",
2608
+ });
2609
+ const result = await auditTarget(target, {
2610
+ ...options,
2611
+ _targetIndex: targetIndex,
2612
+ _targetTotal: extractedTargets.targets.length,
2613
+ });
2614
+ results.push(result);
2615
+ emitProgress(options, {
2616
+ index: targetIndex,
2617
+ label: formatTargetLabel(target),
2618
+ result,
2619
+ target,
2620
+ total: extractedTargets.targets.length,
2621
+ type: "target:finish",
2622
+ });
2623
+ }
2624
+ const groupedResults = groupAuditResults(results);
2625
+ const report = {
2626
+ generatedAt: new Date().toISOString(),
2627
+ inputs: inputBoms.map((inputBom) => inputBom.source),
2628
+ groupedResults,
2629
+ results,
2630
+ skipped: extractedTargets.skipped,
2631
+ summary: summarizeAudit(inputBoms, results, extractedTargets.skipped),
2632
+ tool: {
2633
+ name: "cdx-audit",
2634
+ version: readPackageVersion(),
2635
+ },
2636
+ };
2637
+ Object.assign(report.summary, summarizeGroupedResults(groupedResults));
2638
+ if (options.reportsDir) {
2639
+ const aggregateFile = join(
2640
+ resolve(options.reportsDir),
2641
+ "aggregate-report.json",
2642
+ );
2643
+ writeJsonFile(aggregateFile, report);
2644
+ report.aggregateReportFile = aggregateFile;
2645
+ }
2646
+ if (extractedTargets.targets.length) {
2647
+ emitProgress(options, {
2648
+ summary: report.summary,
2649
+ type: "run:finish",
2650
+ });
2651
+ }
2652
+ return report;
2653
+ }
2654
+
2655
+ /**
2656
+ * Run the predictive audit flow from one or more CycloneDX BOM inputs.
2657
+ *
2658
+ * @param {object} options CLI options
2659
+ * @returns {Promise<object>} aggregate audit report
2660
+ */
2661
+ export async function runAudit(options) {
2662
+ const inputBoms = loadInputBoms(options);
2663
+ const workspaceContext = prepareWorkspaceContext(options);
2664
+ const licensePolicy = options.licensePolicy
2665
+ ? loadPolicy(options.licensePolicy)
2666
+ : null;
2667
+ try {
2668
+ const auditOptions = {
2669
+ ...options,
2670
+ workspaceDir: workspaceContext.workspaceDir,
2671
+ // Preloaded so each target's child SBOM can be evaluated in auditTarget
2672
+ // without re-reading the policy file per dependency.
2673
+ _licensePolicy: licensePolicy,
2674
+ };
2675
+ const report = options.directBomAudit
2676
+ ? await runDirectBomAuditFromBoms(inputBoms, auditOptions)
2677
+ : await runAuditFromBoms(inputBoms, auditOptions);
2678
+ attachLicenseViolations(report, inputBoms, licensePolicy);
2679
+ return report;
2680
+ } finally {
2681
+ if (workspaceContext.cleanupOnFinish) {
2682
+ cleanupSourceDir(workspaceContext.workspaceDir);
2683
+ }
2684
+ }
2685
+ }
2686
+
2687
+ /**
2688
+ * Aggregates license compliance violations onto the report. Violations come
2689
+ * from two sources: the input BOM(s) directly, and — in predictive mode — the
2690
+ * per-dependency child SBOMs generated from cloned upstream sources (carried on
2691
+ * each target result). Duplicates across both are collapsed so the same
2692
+ * component/license/alert is reported once. No-op without a policy.
2693
+ *
2694
+ * @param {object} report aggregate audit report (mutated)
2695
+ * @param {object[]} inputBoms loaded input BOMs
2696
+ * @param {object|null} policy preloaded compliance policy
2697
+ * @returns {void}
2698
+ */
2699
+ export function attachLicenseViolations(report, inputBoms, policy) {
2700
+ if (!report || !policy) {
2701
+ return;
2702
+ }
2703
+ const seen = new Set();
2704
+ const violations = [];
2705
+ const add = (violation, source) => {
2706
+ const key = `${violation.ref}|${violation.license}|${violation.alert}`;
2707
+ if (seen.has(key)) {
2708
+ return;
2709
+ }
2710
+ seen.add(key);
2711
+ violations.push({ ...violation, source });
2712
+ };
2713
+ // Direct violations from the input BOM(s).
2714
+ for (const inputBom of inputBoms) {
2715
+ for (const violation of collectPolicyViolations(inputBom.bomJson, policy, {
2716
+ includeWarnings: true,
2717
+ })) {
2718
+ add(violation, inputBom.source);
2719
+ }
2720
+ }
2721
+ // Violations surfaced from each dependency's child SBOM (predictive mode).
2722
+ for (const result of report.results || []) {
2723
+ for (const violation of result.licenseViolations || []) {
2724
+ add(violation, formatTargetLabel(result.target) || result.repoUrl);
2725
+ }
2726
+ }
2727
+ report.licenseViolations = violations;
2728
+ report.summary = report.summary || {};
2729
+ report.summary.licenseViolations = violations.length;
2730
+ report.summary.licenseViolationsByAlert = violations.reduce(
2731
+ (counts, violation) => {
2732
+ counts[violation.alert] = (counts[violation.alert] || 0) + 1;
2733
+ return counts;
2734
+ },
2735
+ {},
2736
+ );
2737
+ }
2738
+
2739
+ /**
2740
+ * Render a report and compute the proper process exit code.
2741
+ *
2742
+ * @param {object} report aggregate report
2743
+ * @param {object} options CLI options
2744
+ * @returns {{ exitCode: number, output: string }} rendered output and exit code
2745
+ */
2746
+ export function finalizeAuditReport(report, options) {
2747
+ const output = renderAuditReport(options.report, report, {
2748
+ minSeverity: options.minSeverity,
2749
+ });
2750
+ // Prohibited (error-level) license violations are a hard fail regardless of
2751
+ // the supply-chain severity threshold.
2752
+ const licensePolicyFail = (report?.licenseViolations || []).some(
2753
+ (violation) => violation.alert === "error",
2754
+ );
2755
+ if (report?.auditMode === "direct") {
2756
+ const shouldFail = (report.results || []).some((result) =>
2757
+ (result.findings || []).some((finding) =>
2758
+ severityMeetsThreshold(
2759
+ finding?.severity || "none",
2760
+ options.failSeverity || "high",
2761
+ ),
2762
+ ),
2763
+ );
2764
+ return {
2765
+ exitCode: shouldFail || licensePolicyFail ? 3 : 0,
2766
+ output,
2767
+ };
2768
+ }
2769
+ const effectiveResults = report.groupedResults?.length
2770
+ ? report.groupedResults
2771
+ : report.results;
2772
+ const shouldFail = effectiveResults.some(
2773
+ (result) =>
2774
+ !result?.error &&
2775
+ severityMeetsThreshold(
2776
+ result?.assessment?.severity || "none",
2777
+ options.failSeverity || "high",
2778
+ ),
2779
+ );
2780
+ return {
2781
+ exitCode: shouldFail || licensePolicyFail ? 3 : 0,
2782
+ output,
2783
+ };
2784
+ }
2785
+
2786
+ /**
2787
+ * Build a result file name for user-provided report output paths.
2788
+ *
2789
+ * @param {object} options CLI options
2790
+ * @returns {string | undefined} output file path
2791
+ */
2792
+ export function defaultOutputFile(options) {
2793
+ if (!options.reportsDir) {
2794
+ return undefined;
2795
+ }
2796
+ return join(
2797
+ resolve(options.reportsDir),
2798
+ `cdx-audit-report.${options.report || "console"}.txt`,
2799
+ );
2800
+ }