@cdxgen/cdxgen 13.1.0 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -1,8 +1,19 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
2
3
 
3
4
  import { shouldFetchPackageMetadata } from "../core/env.js";
5
+ import {
6
+ getAllFiles,
7
+ getTmpDir,
8
+ safeExistsSync,
9
+ safeMkdtempSync,
10
+ safeRmSync,
11
+ } from "../core/fs.js";
4
12
  import { sanitizeIngestedPurl } from "../inventory/purl.js";
5
- import { getNpmMetadata } from "./ecosystems.js";
13
+ import { extractJarArchive, getNpmMetadata } from "./ecosystems.js";
14
+ import { parsePnpmLock } from "./parsers-js.js";
15
+ import { parseComposerJson, parseComposerLock } from "./parsers-misc.js";
16
+ import { parseGemfileLockData } from "./rubyutils.js";
6
17
 
7
18
  /**
8
19
  * Parse caxa self-extracting executable metadata.
@@ -51,16 +62,24 @@ export async function parseCaxaMetadata(mfile) {
51
62
  value: "true",
52
63
  });
53
64
  }
65
+ // The binary-analysis method names the executable the component was found
66
+ // in, which only a parent component can supply. Any `*metadata.json` in
67
+ // the scanned tree reaches this parser, so a document with components and
68
+ // no parent is ordinary input rather than a broken caxa build.
69
+ const methods = [];
70
+ if (parentComponent?.name) {
71
+ methods.push({
72
+ technique: "binary-analysis",
73
+ confidence: 1,
74
+ value: parentComponent.name,
75
+ });
76
+ }
54
77
  comp.evidence = {
55
78
  identity: {
56
79
  field: "purl",
57
80
  confidence: 1,
58
81
  methods: [
59
- {
60
- technique: "binary-analysis",
61
- confidence: 1,
62
- value: parentComponent.name,
63
- },
82
+ ...methods,
64
83
  {
65
84
  technique: "manifest-analysis",
66
85
  confidence: 1,
@@ -75,3 +94,424 @@ export async function parseCaxaMetadata(mfile) {
75
94
  }
76
95
  return mdata;
77
96
  }
97
+
98
+ // Manifests and SBOMs read from an app are data the binary ships, so each is
99
+ // size-capped before it is parsed.
100
+ const MAX_APP_DOCUMENT_BYTES = 64 * 1024 * 1024;
101
+
102
+ function readAppJson(file) {
103
+ try {
104
+ if (statSync(file).size > MAX_APP_DOCUMENT_BYTES) {
105
+ return undefined;
106
+ }
107
+ return JSON.parse(readFileSync(file, "utf-8"));
108
+ } catch (_e) {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ // A path a manifest in the app names must stay inside the directory it is
114
+ // relative to.
115
+ function resolveInside(baseDir, relativePath) {
116
+ if (typeof relativePath !== "string" || !relativePath.length) {
117
+ return undefined;
118
+ }
119
+ if (isAbsolute(relativePath)) {
120
+ return undefined;
121
+ }
122
+ const resolved = join(baseDir, relativePath);
123
+ const back = relative(baseDir, resolved);
124
+ if (!back || back.startsWith("..") || isAbsolute(back)) {
125
+ return undefined;
126
+ }
127
+ return resolved;
128
+ }
129
+
130
+ function toPosix(filePath) {
131
+ return filePath.split(sep).join("/");
132
+ }
133
+
134
+ // Parsers record the files they read by absolute path, which for an extracted
135
+ // app is a temporary directory. The path inside the app is the one that means
136
+ // something to a reader of the SBOM.
137
+ function relativizeToApp(component, appDir) {
138
+ const inApp = (value) =>
139
+ typeof value === "string" && value.startsWith(`${appDir}${sep}`)
140
+ ? toPosix(relative(appDir, value))
141
+ : value;
142
+ for (const property of component.properties || []) {
143
+ if (property.name === "internal:SrcFile") {
144
+ property.value = inApp(property.value);
145
+ }
146
+ }
147
+ const identities = component.evidence?.identity;
148
+ for (const identity of Array.isArray(identities)
149
+ ? identities
150
+ : [identities]) {
151
+ for (const method of identity?.methods || []) {
152
+ method.value = inApp(method.value);
153
+ }
154
+ }
155
+ for (const occurrence of component.evidence?.occurrences || []) {
156
+ occurrence.location = inApp(occurrence.location);
157
+ }
158
+ return component;
159
+ }
160
+
161
+ // Components taken from the app's own SBOMs keep their bom-refs even when
162
+ // their purl has to be repaired, since the dependency graph refers to them by
163
+ // that ref. A component that has only a purl is referred to by it.
164
+ function ingestComponent(component) {
165
+ const ref = component["bom-ref"];
166
+ sanitizeIngestedPurl(component);
167
+ if (ref) {
168
+ component["bom-ref"] = ref;
169
+ } else if (!component["bom-ref"] && component.purl) {
170
+ component["bom-ref"] = decodeURIComponent(component.purl);
171
+ }
172
+ return component;
173
+ }
174
+
175
+ /**
176
+ * Index the npm packages of an extracted caxa app by directory. Each entry is
177
+ * the directory of a package.json relative to the app, mapped to the bom-ref
178
+ * of the metadata component with the same name and version.
179
+ */
180
+ function indexAppPackages(appDir, components) {
181
+ const refs = new Set(components.map((c) => c["bom-ref"]));
182
+ const packageDirs = [];
183
+ const packageJsons = getAllFiles(appDir, "**/package.json", {
184
+ includeDot: true,
185
+ includeNodeModulesDir: true,
186
+ });
187
+ for (const file of packageJsons) {
188
+ const pkg = readAppJson(file);
189
+ if (!pkg?.name || !pkg?.version) {
190
+ continue;
191
+ }
192
+ const ref = `pkg:npm/${pkg.name}@${pkg.version}`;
193
+ if (refs.has(ref)) {
194
+ packageDirs.push({ dir: toPosix(relative(appDir, dirname(file))), ref });
195
+ }
196
+ }
197
+ // Innermost first, so a nested node_modules package owns its own files.
198
+ packageDirs.sort((a, b) => b.dir.length - a.dir.length);
199
+ return packageDirs;
200
+ }
201
+
202
+ function ownerOf(packageDirs, relativeFile) {
203
+ return packageDirs.find(
204
+ ({ dir }) => dir === "" || relativeFile.startsWith(`${dir}/`),
205
+ );
206
+ }
207
+
208
+ function addDependencies(dependencyMap, ref, dependsOn) {
209
+ if (!ref || !dependsOn?.length) {
210
+ return;
211
+ }
212
+ const existing = dependencyMap.get(ref) || new Set();
213
+ for (const target of dependsOn) {
214
+ if (target && target !== ref) {
215
+ existing.add(target);
216
+ }
217
+ }
218
+ dependencyMap.set(ref, existing);
219
+ }
220
+
221
+ // Refs that nothing in the list depends on: the entry points of a sub-graph.
222
+ function unrequiredRefs(components, dependencies) {
223
+ const required = new Set(dependencies.flatMap((d) => d.dependsOn || []));
224
+ return components
225
+ .map((c) => c["bom-ref"])
226
+ .filter((ref) => ref && !required.has(ref));
227
+ }
228
+
229
+ /**
230
+ * Read the integrity of each installed npm package from the lockfile pnpm or
231
+ * npm leaves in the app's node_modules (or the app's own lockfile).
232
+ *
233
+ * @returns {Promise<Map<string, string>>} npm bom-ref to integrity string
234
+ */
235
+ async function readAppIntegrity(appDir) {
236
+ const integrity = new Map();
237
+ for (const lockFile of [
238
+ "node_modules/.package-lock.json",
239
+ "package-lock.json",
240
+ ]) {
241
+ const lock = readAppJson(join(appDir, lockFile));
242
+ for (const [installPath, entry] of Object.entries(lock?.packages || {})) {
243
+ const name =
244
+ entry?.name || installPath.split("node_modules/").pop() || undefined;
245
+ if (installPath && name && entry?.version && entry?.integrity) {
246
+ integrity.set(`pkg:npm/${name}@${entry.version}`, entry.integrity);
247
+ }
248
+ }
249
+ }
250
+ for (const lockFile of ["node_modules/.pnpm/lock.yaml", "pnpm-lock.yaml"]) {
251
+ const file = join(appDir, lockFile);
252
+ if (!safeExistsSync(file)) {
253
+ continue;
254
+ }
255
+ try {
256
+ const { pkgList } = await parsePnpmLock(file);
257
+ for (const pkg of pkgList || []) {
258
+ if (pkg?._integrity && pkg.name && pkg.version) {
259
+ const name = pkg.group ? `${pkg.group}/${pkg.name}` : pkg.name;
260
+ integrity.set(`pkg:npm/${name}@${pkg.version}`, pkg._integrity);
261
+ }
262
+ }
263
+ } catch (_e) {
264
+ // An unreadable lockfile leaves the components without hashes.
265
+ }
266
+ }
267
+ return integrity;
268
+ }
269
+
270
+ /**
271
+ * Add the native tools a package ships, as described by a cdxgen plugins
272
+ * manifest (`plugins-manifest.json`, from @cdxgen/cdxgen-plugins-bin*). Only
273
+ * entries whose binary is present are used, so a pruned bundle lists only what
274
+ * it kept. Their own dependencies come from the aggregate
275
+ * `sbom-postbuild.cdx.json` next to the manifest, or else from each entry's
276
+ * `sbomFile`, limited to what the present tools reach.
277
+ */
278
+ function addPluginTools(appDir, packageDirs, state) {
279
+ const manifests = getAllFiles(appDir, "**/plugins-manifest.json", {
280
+ includeDot: true,
281
+ includeNodeModulesDir: true,
282
+ });
283
+ for (const manifestFile of manifests) {
284
+ const owner = ownerOf(packageDirs, toPosix(relative(appDir, manifestFile)));
285
+ const manifest = readAppJson(manifestFile);
286
+ if (!owner || !Array.isArray(manifest?.plugins)) {
287
+ continue;
288
+ }
289
+ const packageDir = join(appDir, owner.dir);
290
+ const ownerComponent = state.byRef.get(owner.ref);
291
+ const lazyMembers = new Set(
292
+ (ownerComponent?.properties || [])
293
+ .filter((p) => p.name === "cdx:caxa:lazyMember")
294
+ .map((p) => p.value),
295
+ );
296
+ const toolRefs = [];
297
+ const sbomDocuments = [];
298
+ for (const plugin of manifest.plugins) {
299
+ const component = plugin?.component;
300
+ const binary = resolveInside(packageDir, plugin?.binaryPath);
301
+ if (
302
+ !component?.name ||
303
+ typeof component["bom-ref"] !== "string" ||
304
+ !binary ||
305
+ !safeExistsSync(binary)
306
+ ) {
307
+ continue;
308
+ }
309
+ const tool = ingestComponent(structuredClone(component));
310
+ tool.properties = tool.properties || [];
311
+ tool.properties.push({
312
+ name: "internal:SrcFile",
313
+ value: toPosix(relative(appDir, manifestFile)),
314
+ });
315
+ if (lazyMembers.has(plugin.binaryPath)) {
316
+ tool.properties.push({
317
+ name: "cdx:caxa:lazyMember",
318
+ value: plugin.binaryPath,
319
+ });
320
+ }
321
+ if (state.addComponent(tool)) {
322
+ toolRefs.push(tool["bom-ref"]);
323
+ }
324
+ const sbomFile = resolveInside(packageDir, plugin.sbomFile);
325
+ if (sbomFile && safeExistsSync(sbomFile)) {
326
+ sbomDocuments.push({ file: sbomFile, rootRef: tool["bom-ref"] });
327
+ }
328
+ }
329
+ if (!toolRefs.length) {
330
+ continue;
331
+ }
332
+ addDependencies(state.dependencyMap, owner.ref, toolRefs);
333
+ const aggregate = join(dirname(manifestFile), "sbom-postbuild.cdx.json");
334
+ const documents = safeExistsSync(aggregate)
335
+ ? [{ file: aggregate }]
336
+ : sbomDocuments;
337
+ addReachableSbomContents(documents, toolRefs, state);
338
+ }
339
+ }
340
+
341
+ // Merge the components and dependencies of the tools' own SBOMs, keeping only
342
+ // what the given tools reach. A per-tool SBOM's root is renamed to the tool's
343
+ // ref, since the manifest and the tool's build may name it differently.
344
+ function addReachableSbomContents(documents, toolRefs, state) {
345
+ const candidates = new Map();
346
+ const edges = new Map();
347
+ for (const { file, rootRef } of documents) {
348
+ const bom = readAppJson(file);
349
+ if (!bom) {
350
+ continue;
351
+ }
352
+ const sbomRoot = bom.metadata?.component?.["bom-ref"];
353
+ const rename = (ref) => (rootRef && ref === sbomRoot ? rootRef : ref);
354
+ for (const component of bom.components || []) {
355
+ if (component?.["bom-ref"] && !candidates.has(component["bom-ref"])) {
356
+ candidates.set(component["bom-ref"], component);
357
+ }
358
+ }
359
+ for (const dependency of bom.dependencies || []) {
360
+ addDependencies(
361
+ edges,
362
+ rename(dependency.ref),
363
+ (dependency.dependsOn || []).map(rename),
364
+ );
365
+ }
366
+ }
367
+ const reached = new Set(toolRefs);
368
+ const queue = [...toolRefs];
369
+ while (queue.length) {
370
+ for (const target of edges.get(queue.shift()) || []) {
371
+ if (!reached.has(target) && candidates.has(target)) {
372
+ reached.add(target);
373
+ queue.push(target);
374
+ }
375
+ }
376
+ }
377
+ for (const ref of reached) {
378
+ const component = candidates.get(ref);
379
+ if (component && !toolRefs.includes(ref)) {
380
+ state.addComponent(ingestComponent(structuredClone(component)));
381
+ }
382
+ addDependencies(
383
+ state.dependencyMap,
384
+ ref,
385
+ [...(edges.get(ref) || [])].filter((target) => reached.has(target)),
386
+ );
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Add the PHP (composer.lock), Ruby (Gemfile.lock) and Java (*.jar) packages
392
+ * that npm packages of the app vendor, each linked from the npm package that
393
+ * contains it.
394
+ */
395
+ async function addEmbeddedPackages(appDir, packageDirs, state) {
396
+ const options = { includeDot: true, includeNodeModulesDir: true };
397
+ const embedded = [];
398
+ for (const composerLock of getAllFiles(appDir, "**/composer.lock", options)) {
399
+ const composerJson = join(dirname(composerLock), "composer.json");
400
+ const { rootRequires } = safeExistsSync(composerJson)
401
+ ? parseComposerJson(composerJson)
402
+ : { rootRequires: {} };
403
+ embedded.push({
404
+ file: composerLock,
405
+ result: parseComposerLock(composerLock, rootRequires),
406
+ });
407
+ }
408
+ for (const gemLock of getAllFiles(appDir, "**/Gemfile.lock", options)) {
409
+ embedded.push({
410
+ file: gemLock,
411
+ result: await parseGemfileLockData(
412
+ readFileSync(gemLock, "utf-8"),
413
+ gemLock,
414
+ ),
415
+ });
416
+ }
417
+ const jars = getAllFiles(appDir, "**/*.jar", options);
418
+ if (jars.length) {
419
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "caxa-jars-"));
420
+ try {
421
+ for (const jar of jars) {
422
+ embedded.push({
423
+ file: jar,
424
+ result: { pkgList: (await extractJarArchive(jar, tempDir)) || [] },
425
+ });
426
+ }
427
+ } finally {
428
+ safeRmSync(tempDir, { recursive: true, force: true });
429
+ }
430
+ }
431
+ for (const { file, result } of embedded) {
432
+ const owner = ownerOf(packageDirs, toPosix(relative(appDir, file)));
433
+ const pkgList = result?.pkgList || [];
434
+ if (!owner || !pkgList.length) {
435
+ continue;
436
+ }
437
+ for (const pkg of pkgList) {
438
+ pkg.properties = pkg.properties || [];
439
+ if (!pkg.properties.some((p) => p.name === "internal:SrcFile")) {
440
+ pkg.properties.push({ name: "internal:SrcFile", value: file });
441
+ }
442
+ state.addComponent(relativizeToApp(ingestComponent(pkg), appDir));
443
+ }
444
+ const dependencies = result.dependenciesList || [];
445
+ for (const dependency of dependencies) {
446
+ addDependencies(
447
+ state.dependencyMap,
448
+ dependency.ref,
449
+ dependency.dependsOn,
450
+ );
451
+ }
452
+ // The lockfile's direct dependencies, or failing that every package in
453
+ // it that nothing else requires, hang off the npm package.
454
+ const direct = (result.rootList || [])
455
+ .map((pkg) => pkg["bom-ref"])
456
+ .filter(Boolean);
457
+ addDependencies(
458
+ state.dependencyMap,
459
+ owner.ref,
460
+ direct.length ? direct : unrequiredRefs(pkgList, dependencies),
461
+ );
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Complete caxa metadata from the application the binary extracts (or was
467
+ * built from): npm integrity from the lockfile pnpm or npm leaves in
468
+ * node_modules, the native tools a cdxgen plugins manifest describes, with
469
+ * their own dependencies, and the PHP, Ruby and Java packages vendored inside
470
+ * npm packages. Everything found is linked from the npm package whose
471
+ * directory contains it; npm packages that are not in the metadata are
472
+ * ignored.
473
+ *
474
+ * @param {Object} mdata Parsed caxa metadata (see parseCaxaMetadata)
475
+ * @param {string} appDir Directory of the extracted app
476
+ * @returns {Promise<Object>} The metadata, with components and dependencies added
477
+ */
478
+ export async function enrichCaxaMetadataFromApp(mdata, appDir) {
479
+ if (!mdata?.components?.length || !appDir || !safeExistsSync(appDir)) {
480
+ return mdata;
481
+ }
482
+ const byRef = new Map(mdata.components.map((c) => [c["bom-ref"], c]));
483
+ const dependencyMap = new Map();
484
+ for (const dependency of mdata.dependencies || []) {
485
+ addDependencies(dependencyMap, dependency.ref, dependency.dependsOn);
486
+ }
487
+ const state = {
488
+ byRef,
489
+ dependencyMap,
490
+ addComponent(component) {
491
+ const ref = component?.["bom-ref"];
492
+ if (!ref || byRef.has(ref)) {
493
+ return false;
494
+ }
495
+ // It ships in the binary, unless its own SBOM said otherwise.
496
+ component.scope = component.scope || "required";
497
+ byRef.set(ref, component);
498
+ mdata.components.push(component);
499
+ return true;
500
+ },
501
+ };
502
+ const integrity = await readAppIntegrity(appDir);
503
+ for (const component of mdata.components) {
504
+ const value = integrity.get(component["bom-ref"]);
505
+ if (value && !component.hashes?.length && !component._integrity) {
506
+ component._integrity = value;
507
+ }
508
+ }
509
+ const packageDirs = indexAppPackages(appDir, mdata.components);
510
+ addPluginTools(appDir, packageDirs, state);
511
+ await addEmbeddedPackages(appDir, packageDirs, state);
512
+ mdata.dependencies = [...dependencyMap].map(([ref, dependsOn]) => ({
513
+ ref,
514
+ dependsOn: [...dependsOn].sort(),
515
+ }));
516
+ return mdata;
517
+ }
@@ -584,4 +584,70 @@ export function collapseCmakeVersions(pkgList) {
584
584
  return collapsed;
585
585
  }
586
586
 
587
+ /** `cdx:cmake:depKind` values for dependencies downloaded at configure time. */
588
+ const FETCHED_KINDS = new Set(["fetch", "cpm", "external-project"]);
589
+
590
+ function cmakeProperty(pkg, name) {
591
+ return pkg?.properties?.find((prop) => prop.name === name)?.value;
592
+ }
593
+
594
+ /**
595
+ * Keep one component per dependency downloaded at configure time.
596
+ *
597
+ * The same `FetchContent_Declare` may be read from several files, and a
598
+ * `find_package(<name>)` names the package a `FetchContent_Declare(<name>)` or
599
+ * `CPMAddPackage` fetches. Within the components scraped from CMake files, the
600
+ * first fetched component of each name stands for all of them, and a plain
601
+ * `find_package` entry of that name is dropped.
602
+ *
603
+ * @param {Object[]} pkgList Components scraped from CMake-like files
604
+ * @returns {Object[]} The components, in first-seen order
605
+ */
606
+ export function preferFetchedCmakeDependencies(pkgList) {
607
+ if (!Array.isArray(pkgList)) {
608
+ return [];
609
+ }
610
+ const fetched = new Map();
611
+ for (const pkg of pkgList) {
612
+ const key = pkg?.name?.toLowerCase();
613
+ if (
614
+ key &&
615
+ FETCHED_KINDS.has(cmakeProperty(pkg, "cdx:cmake:depKind")) &&
616
+ !fetched.has(key)
617
+ ) {
618
+ fetched.set(key, pkg);
619
+ }
620
+ }
621
+ return pkgList.filter((pkg) => {
622
+ const key = pkg?.name?.toLowerCase();
623
+ if (!key || !fetched.has(key)) {
624
+ return true;
625
+ }
626
+ const kind = cmakeProperty(pkg, "cdx:cmake:depKind");
627
+ return FETCHED_KINDS.has(kind) ? fetched.get(key) === pkg : !!kind;
628
+ });
629
+ }
630
+
631
+ /**
632
+ * Drop the components scraped from CMake files for a dependency the build tree
633
+ * has resolved: the gitclone script names the repository and tag that were
634
+ * really checked out, so its component replaces the declared one.
635
+ *
636
+ * @param {Object[]} pkgList Components collected so far
637
+ * @param {string} name Name of the resolved dependency
638
+ * @returns {Object[]} The components without the scraped entries for `name`
639
+ */
640
+ export function withoutScrapedFetchDependency(pkgList, name) {
641
+ const key = name?.toLowerCase();
642
+ return pkgList.filter(
643
+ (pkg) =>
644
+ !(
645
+ key &&
646
+ pkg?.name?.toLowerCase() === key &&
647
+ cmakeProperty(pkg, "cdx:cmake:resolvedVia") === "cmake-lists" &&
648
+ FETCHED_KINDS.has(cmakeProperty(pkg, "cdx:cmake:depKind"))
649
+ ),
650
+ );
651
+ }
652
+
587
653
  export { GIT_COMMAND, toRepoRelative };