@cdxgen/cdxgen 13.2.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 (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -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/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. 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.
@@ -83,3 +94,424 @@ export async function parseCaxaMetadata(mfile) {
83
94
  }
84
95
  return mdata;
85
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 };
@@ -30,6 +30,30 @@ import {
30
30
  parseXmakeRequiresLock,
31
31
  } from "./parsers-misc.js";
32
32
 
33
+ /**
34
+ * Add the symbols a file uses to a component's `internal:ImportedSymbols`,
35
+ * keeping every other property and the symbols already recorded.
36
+ *
37
+ * @param {Object} apkg Component to enrich
38
+ * @param {string[]} symbols Symbol names
39
+ */
40
+ function mergeImportedSymbols(apkg, symbols) {
41
+ apkg.properties = apkg.properties || [];
42
+ const prop = apkg.properties.find(
43
+ (p) => p.name === "internal:ImportedSymbols",
44
+ );
45
+ if (prop) {
46
+ prop.value = Array.from(new Set([...prop.value.split("|"), ...symbols]))
47
+ .sort()
48
+ .join("|");
49
+ } else {
50
+ apkg.properties.push({
51
+ name: "internal:ImportedSymbols",
52
+ value: symbols.join("|"),
53
+ });
54
+ }
55
+ }
56
+
33
57
  /**
34
58
  * Method to find c/c++ modules by collecting usages with atom
35
59
  *
@@ -48,8 +72,11 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
48
72
  let parentComponent;
49
73
  const dependsOn = new Set();
50
74
 
75
+ // Components from CMake and Conan carry no group: the key spells a missing
76
+ // group the same way on insert and on lookup.
77
+ const epkgKey = (group, name) => `${group || ""}/${name}`;
51
78
  (epkgList || []).forEach((p) => {
52
- epkgMap[`${p.group}/${p.name}`] = p;
79
+ epkgMap[epkgKey(p.group, p.name)] = p;
53
80
  });
54
81
  // Let's look for any vcpkg.json file to tell us about the directory we're scanning
55
82
  // users can use this file to give us a clue even if they do not use vcpkg library manager
@@ -106,9 +133,9 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
106
133
  // Is this a dependency we haven't seen before including the all lower and upper case version?
107
134
  if (
108
135
  avcpkgName &&
109
- !epkgMap[`/${avcpkgName}`] &&
110
- !epkgMap[`/${avcpkgName.toLowerCase()}`] &&
111
- !epkgMap[`/${avcpkgName.toUpperCase()}`]
136
+ !epkgMap[epkgKey("", avcpkgName)] &&
137
+ !epkgMap[epkgKey("", avcpkgName.toLowerCase())] &&
138
+ !epkgMap[epkgKey("", avcpkgName.toUpperCase())]
112
139
  ) {
113
140
  const pkgPurl = build({
114
141
  type: pkgType,
@@ -205,6 +232,8 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
205
232
  );
206
233
  }
207
234
  const usageData = parseCUsageSlice(sliceData);
235
+ const osComponents = new Map();
236
+ const addedRefs = new Set();
208
237
  for (let afile of Object.keys(usageData)) {
209
238
  // Normalize windows separator
210
239
  afile = afile.replace("..\\", "").replace(/\\/g, "/");
@@ -235,13 +264,31 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
235
264
  if (CPP_STD_MODULES.includes(name)) {
236
265
  continue;
237
266
  }
238
- let apkg = getOSPackageForFile(afile, osPkgsList) ||
239
- epkgMap[`${group}/${name}`] || {
267
+ // The OS package list is shared by every header: a component taken from it
268
+ // is a copy, so the internal:PkgProvides the next header is matched against
269
+ // stays intact. A component the caller already listed is enriched in place
270
+ // and not listed again.
271
+ const osPkg = getOSPackageForFile(afile, osPkgsList);
272
+ const listedPkg = osPkg ? undefined : epkgMap[epkgKey(group, name)];
273
+ let apkg;
274
+ if (osPkg) {
275
+ const osRef = osPkg["bom-ref"] || osPkg.purl || osPkg.name;
276
+ apkg = osComponents.get(osRef);
277
+ if (!apkg) {
278
+ apkg = {
279
+ ...osPkg,
280
+ properties: (osPkg.properties || []).map((prop) => ({ ...prop })),
281
+ };
282
+ osComponents.set(osRef, apkg);
283
+ }
284
+ } else {
285
+ apkg = listedPkg || {
240
286
  name,
241
287
  group,
242
288
  version: "",
243
289
  type: pkgType,
244
290
  };
291
+ }
245
292
  // If this is a relative file, there is a good chance we can reuse the project group
246
293
  if (!afile.startsWith(_sep) && !group.length) {
247
294
  group = options.projectGroup || "";
@@ -291,36 +338,22 @@ export function getCppModules(src, options, osPkgsList, epkgList) {
291
338
  )
292
339
  .map((v) => v.split(":")[0])
293
340
  .sort();
294
- if (!apkg["properties"] && usymbols.length) {
295
- apkg["properties"] = [
296
- { name: "internal:ImportedSymbols", value: usymbols.join("|") },
297
- ];
298
- } else {
299
- apkg["properties"] = [];
300
- }
301
- const newProps = [];
302
- let symbolsPropertyFound = false;
303
- for (const prop of apkg["properties"]) {
304
- if (prop.name === "internal:ImportedSymbols") {
305
- symbolsPropertyFound = true;
306
- let existingSymbols = prop.value.split("|");
307
- existingSymbols = existingSymbols.concat(usymbols);
308
- prop.value = Array.from(new Set(existingSymbols)).sort().join("|");
309
- }
310
- newProps.push(prop);
311
- }
312
- if (!symbolsPropertyFound && usymbols.length) {
313
- apkg["properties"].push({
314
- name: "internal:ImportedSymbols",
315
- value: usymbols.join("|"),
316
- });
341
+ if (usymbols.length) {
342
+ mergeImportedSymbols(apkg, usymbols);
317
343
  }
318
- apkg["properties"] = newProps;
319
344
  }
320
345
  // At this point, we have a package but we don't know what it's called
321
346
  // So let's try to locate this generic package using some heuristics
322
347
  apkg = locateGenericPackage(apkg);
323
- if (!pkgAddedMap[name]) {
348
+ if (listedPkg) {
349
+ dependsOn.add(apkg["bom-ref"]);
350
+ } else if (osPkg) {
351
+ if (!addedRefs.has(apkg["bom-ref"])) {
352
+ pkgList.push(apkg);
353
+ dependsOn.add(apkg["bom-ref"]);
354
+ addedRefs.add(apkg["bom-ref"]);
355
+ }
356
+ } else if (!pkgAddedMap[name]) {
324
357
  pkgList.push(apkg);
325
358
  dependsOn.add(apkg["bom-ref"]);
326
359
  pkgAddedMap[name] = true;