@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
@@ -21,6 +21,15 @@ import {
21
21
 
22
22
  const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
23
23
 
24
+ // The dependency groups bun writes for a package or workspace, with the kind
25
+ // of edge each one declares.
26
+ const DEPENDENCY_GROUP_KINDS = [
27
+ ["dependencies", "dependency"],
28
+ ["optionalDependencies", "optional"],
29
+ ["peerDependencies", "peer"],
30
+ ["devDependencies", "dev"],
31
+ ];
32
+
24
33
  /**
25
34
  * Split a bun.lock package descriptor (eg `@babel/parser@7.29.7`,
26
35
  * `left-pad@1.3.0` or `foo@git+https://github.com/foo/bar#abcdef`) into its
@@ -53,10 +62,36 @@ export function parseBunDescriptor(descriptor) {
53
62
  return { group, name, version };
54
63
  }
55
64
 
65
+ /**
66
+ * Split a bun.lock package key into its package-name segments. A key is the
67
+ * dependency path bun nested the package under (eg `@isaacs/cliui/wrap-ansi`),
68
+ * where every segment is a package name, so scoped names (`@scope/name`)
69
+ * count as a single segment despite containing `/`.
70
+ *
71
+ * @param {string} key The lockfile package key.
72
+ * @returns {string[]} The package names composing the key.
73
+ */
74
+ function keySegments(key) {
75
+ const parts = key.split("/");
76
+ const segments = [];
77
+ for (let i = 0; i < parts.length; i++) {
78
+ if (parts[i].startsWith("@") && i + 1 < parts.length) {
79
+ segments.push(`${parts[i]}/${parts[i + 1]}`);
80
+ i++;
81
+ } else {
82
+ segments.push(parts[i]);
83
+ }
84
+ }
85
+ return segments;
86
+ }
87
+
56
88
  /**
57
89
  * Determine whether a bun version specifier points at a non-registry source
58
90
  * (git, tarball URL, workspace or local path).
59
91
  *
92
+ * Bun writes a local tarball as a bare path (`localdep@./localdep.tgz`) rather
93
+ * than a `file:` specifier, so relative and absolute paths count too.
94
+ *
60
95
  * @param {string} version The specifier extracted from the descriptor.
61
96
  * @returns {boolean} True when the specifier is not a plain semver version.
62
97
  */
@@ -65,6 +100,9 @@ function isNonRegistrySpecifier(version) {
65
100
  return false;
66
101
  }
67
102
  return (
103
+ version.startsWith("./") ||
104
+ version.startsWith("../") ||
105
+ version.startsWith("/") ||
68
106
  version.startsWith("git") ||
69
107
  version.includes("://") ||
70
108
  version.startsWith("github:") ||
@@ -77,14 +115,42 @@ function isNonRegistrySpecifier(version) {
77
115
  }
78
116
 
79
117
  /**
80
- * Parse a bun text lockfile (`bun.lock`, lockfileVersion 1).
118
+ * Normalize a lockfile `os`/`cpu` value. Bun writes a single string for the
119
+ * common one-value case (eg `"darwin"`), a negated string (eg `"!linux"`), an
120
+ * array for several values and the string `"none"` when the manifest named no
121
+ * platform bun recognizes (eg `cpu: ["wasm32"]`), which carries no usable
122
+ * value, so both shapes have to be handled (see `Negatable.toJson` in bun's
123
+ * npm.zig, `Negatable::to_json` in the Rust port).
124
+ *
125
+ * @param {string|string[]} value The raw `os`/`cpu` metadata value.
126
+ * @returns {string|undefined} Comma-joined value, or undefined when absent.
127
+ */
128
+ function normalizeOsCpu(value) {
129
+ if (Array.isArray(value)) {
130
+ return value.length ? value.join(", ") : undefined;
131
+ }
132
+ if (typeof value === "string" && value.length && value !== "none") {
133
+ return value;
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ /**
139
+ * Parse a bun text lockfile (`bun.lock`, lockfileVersion 1-3; v1 was written
140
+ * by the Zig implementation in bun 1.2.x-1.3.x and v2/v3 by the Rust one in
141
+ * bun 1.4+ - the content shape is identical, only parse strictness changed).
81
142
  *
82
143
  * Bun's text lockfile is JSONC (JSON with trailing commas). It records the
83
144
  * workspace roots under `workspaces` and the fully resolved dependency tree
84
- * under `packages`, where each entry is an array of the form
85
- * `["name@version", "registry", { dependencies, optionalDependencies, bin,
86
- * os, cpu, ... }, "sha512-..."]`. Nested duplicate versions are keyed by their
87
- * dependency path (eg `"parent/child"`).
145
+ * under `packages`, where nested duplicate versions are keyed by their
146
+ * dependency path (eg `"parent/child"`). The entry layout after the leading
147
+ * `"name@version"` descriptor depends on the resolution type (see the
148
+ * Stringifier in bun's bun.lock.rs):
149
+ * - npm: `["name@version", tarballUrlOrEmpty, { dependencies, bin, os, ... }, "sha512-..."]`
150
+ * - git/github: `["name@git+repo", { ... }, ".bun-tag", "sha512-..."]`
151
+ * - tarball/folder/symlink: `["name@url", { ... }, "sha512-..."]`
152
+ * - workspace: `["name@workspace:path"]`
153
+ * so the elements are recognised by shape instead of fixed indices.
88
154
  *
89
155
  * The binary lockfile (`bun.lockb`) is intentionally not supported - callers
90
156
  * should ask users to regenerate it with `bun install --save-text-lockfile`.
@@ -126,35 +192,65 @@ export async function parseBunLock(bunLockFile, options = {}) {
126
192
  // nested duplicate versions by dependency path (eg `parent/child`), which we
127
193
  // handle per-lookup in resolveDepRef below.
128
194
  const infoForKey = new Map();
129
- const refToKey = new Map();
130
195
  for (const [key, entry] of Object.entries(packages)) {
131
196
  if (!Array.isArray(entry) || !entry.length) {
132
197
  continue;
133
198
  }
134
199
  const descriptor = entry[0];
135
- const registry = entry.length > 1 ? entry[1] : "";
136
- const meta = entry.length > 2 && entry[2] ? entry[2] : {};
137
- const integrity = entry.length > 3 ? entry[3] : undefined;
200
+ if (typeof descriptor !== "string") {
201
+ continue;
202
+ }
138
203
  const { group, name, version } = parseBunDescriptor(descriptor);
139
- if (!name || !version) {
204
+ if (!name || !version || version === "root:") {
140
205
  continue;
141
206
  }
207
+ const isWorkspaceDep = version.startsWith("workspace:");
142
208
  const isGitDep =
143
209
  version.startsWith("git") ||
144
210
  version.startsWith("github:") ||
145
211
  version.startsWith("gitlab:") ||
146
212
  version.startsWith("bitbucket:");
213
+ const isNonRegistry = isGitDep || isNonRegistrySpecifier(version);
214
+ // The elements after the descriptor depend on the resolution type, so
215
+ // recognise them by shape: the metadata object, the integrity hash and,
216
+ // for npm packages on a custom registry, the tarball URL. The git
217
+ // `.bun-tag` string is not needed.
218
+ let meta = {};
219
+ let integrity;
220
+ let tarballUrl;
221
+ let sawMeta = false;
222
+ for (const el of entry.slice(1)) {
223
+ if (el && typeof el === "object" && !Array.isArray(el)) {
224
+ if (!sawMeta) {
225
+ meta = el;
226
+ sawMeta = true;
227
+ }
228
+ } else if (typeof el === "string" && el.length) {
229
+ if (/^sha\d+-/.test(el)) {
230
+ integrity = integrity || el;
231
+ } else if (!isNonRegistry) {
232
+ tarballUrl = tarballUrl || el;
233
+ }
234
+ }
235
+ }
236
+ // Workspace members point back at their manifest via `workspace:<path>`;
237
+ // use the version declared there for the component identity.
238
+ let componentVersion = version;
239
+ if (isWorkspaceDep) {
240
+ const wsEntry = workspaces[version.slice("workspace:".length)];
241
+ componentVersion = wsEntry?.version || null;
242
+ }
147
243
  let qualifiers = null;
148
244
  if (isGitDep) {
149
245
  qualifiers = buildNpmGitPurlQualifiers(version, group, npmrcConfig);
150
- } else if (isNonRegistrySpecifier(version)) {
246
+ } else if (isNonRegistry && !isWorkspaceDep) {
151
247
  qualifiers = { download_url: version };
152
248
  }
153
249
  const purlString = build({
154
250
  type: "npm",
155
251
  namespace: group || null,
156
252
  name: name,
157
- version: version || null,
253
+ version: componentVersion,
158
254
  qualifiers: qualifiers || null,
159
255
  });
160
256
  const bomRef = decodeURIComponent(purlString);
@@ -162,116 +258,178 @@ export async function parseBunLock(bunLockFile, options = {}) {
162
258
  group,
163
259
  name,
164
260
  version,
165
- registry,
261
+ componentVersion,
262
+ registry: tarballUrl || "",
166
263
  meta,
167
264
  integrity,
168
265
  purlString,
169
266
  bomRef,
170
267
  isGitDep,
171
- isNonRegistry: isNonRegistrySpecifier(version),
268
+ isWorkspaceDep,
269
+ isNonRegistry,
172
270
  });
173
- if (!refToKey.has(bomRef)) {
174
- refToKey.set(bomRef, key);
175
- }
176
271
  }
177
272
 
178
- // Resolve a dependency name referenced from `parentKey` to the bom-ref of the
179
- // concrete package bun installed for it. Prefer the nested path key, then
180
- // fall back to the top-level entry.
273
+ // Resolve a dependency name referenced from `parentKey` to the bom-ref of
274
+ // the concrete package bun installed for it. Bun walks up the parent's key
275
+ // one package segment at a time (eg for `a/@scope/b/c` it tries
276
+ // `a/@scope/b/c/<dep>`, `a/@scope/b/<dep>`, `a/<dep>`, then `<dep>`) and
277
+ // uses the first entry it finds, so a version nested at an intermediate
278
+ // level must win over the top-level one.
181
279
  const resolveDepRef = (parentKey, depName) => {
182
- const nestedKey = parentKey ? `${parentKey}/${depName}` : depName;
183
- if (infoForKey.has(nestedKey)) {
184
- return infoForKey.get(nestedKey).bomRef;
185
- }
186
- if (infoForKey.has(depName)) {
187
- return infoForKey.get(depName).bomRef;
280
+ const segments = parentKey ? keySegments(parentKey) : [];
281
+ for (let i = segments.length; i >= 0; i--) {
282
+ const key = [...segments.slice(0, i), depName].join("/");
283
+ if (infoForKey.has(key)) {
284
+ return infoForKey.get(key).bomRef;
285
+ }
188
286
  }
189
287
  return undefined;
190
288
  };
191
289
 
192
- // Track packages surfaced as optional / peer dependencies anywhere in the
193
- // tree so they can be annotated with the matching cdx properties.
194
- const optionalRefs = new Set();
195
- const peerRefs = new Set();
196
-
197
- // Collect the runtime dependency refs declared by a package's metadata.
198
- const runtimeDepRefs = (parentKey, meta) => {
199
- const refs = new Set();
200
- const collect = (depBlock, markSet) => {
201
- if (!depBlock) {
202
- return;
203
- }
204
- for (const depName of Object.keys(depBlock)) {
290
+ // Resolve the dependency groups an entry declares to the packages bun
291
+ // installed for them, tagging each edge with how it was declared. A name
292
+ // listed in `optionalPeers` is an optional peer: bun binds it to a package
293
+ // some other edge installed, but never installs one for it. Registry
294
+ // packages never carry devDependencies (bun does not install them), while
295
+ // workspaces and `file:` folders do.
296
+ const addEdges = (edges, parentKey, meta) => {
297
+ const optionalPeers = new Set(
298
+ Array.isArray(meta.optionalPeers) ? meta.optionalPeers : [],
299
+ );
300
+ for (const [group, kind] of DEPENDENCY_GROUP_KINDS) {
301
+ for (const depName of Object.keys(meta[group] || {})) {
205
302
  const ref = resolveDepRef(parentKey, depName);
206
- if (ref) {
207
- refs.add(ref);
208
- if (markSet) {
209
- markSet.add(ref);
210
- }
303
+ if (!ref) {
304
+ continue;
305
+ }
306
+ const edgeKind =
307
+ kind === "peer" && optionalPeers.has(depName) ? "optionalPeer" : kind;
308
+ if (!edges.has(ref)) {
309
+ edges.set(ref, new Set());
211
310
  }
311
+ edges.get(ref).add(edgeKind);
212
312
  }
213
- };
214
- collect(meta.dependencies, null);
215
- collect(meta.optionalDependencies, optionalRefs);
216
- collect(meta.peerDependencies, peerRefs);
217
- return refs;
313
+ }
218
314
  };
219
315
 
220
- // Seed a production-reachability walk from the non-dev dependencies of every
221
- // workspace root so dev-only packages can be scoped as optional.
222
- const rootProdRefs = new Set();
223
- for (const wsEntry of Object.values(workspaces)) {
224
- if (!wsEntry) {
225
- continue;
226
- }
227
- for (const depName of Object.keys(wsEntry.dependencies || {})) {
228
- const ref = resolveDepRef("", depName);
229
- if (ref) {
230
- rootProdRefs.add(ref);
231
- }
316
+ // Collect the edges of every package, unioned per bom-ref since the same
317
+ // version can be nested under several keys (or aliases). A workspace
318
+ // member's own entry has no metadata: its dependencies come from its
319
+ // `workspaces` entry and resolve under the member's package name (bun nests
320
+ // a member's own versions there, eg `pkg-a/strip-ansi`). Bun marks each
321
+ // copy installed from a parent's bundleDependencies, so a version counts as
322
+ // bundled only when no copy of it was installed on its own.
323
+ const edgesByRef = new Map();
324
+ const memberRefs = new Set();
325
+ const bundledRefs = new Set();
326
+ const unbundledRefs = new Set();
327
+ for (const [key, info] of infoForKey.entries()) {
328
+ if (!edgesByRef.has(info.bomRef)) {
329
+ edgesByRef.set(info.bomRef, new Map());
232
330
  }
233
- for (const depName of Object.keys(wsEntry.optionalDependencies || {})) {
234
- const ref = resolveDepRef("", depName);
235
- if (ref) {
236
- rootProdRefs.add(ref);
237
- optionalRefs.add(ref);
238
- }
331
+ if (info.meta.bundled === true) {
332
+ bundledRefs.add(info.bomRef);
333
+ } else {
334
+ unbundledRefs.add(info.bomRef);
239
335
  }
240
- for (const depName of Object.keys(wsEntry.peerDependencies || {})) {
241
- const ref = resolveDepRef("", depName);
242
- if (ref) {
243
- peerRefs.add(ref);
336
+ const edges = edgesByRef.get(info.bomRef);
337
+ if (info.isWorkspaceDep) {
338
+ memberRefs.add(info.bomRef);
339
+ const wsEntry = workspaces[info.version.slice("workspace:".length)];
340
+ if (wsEntry) {
341
+ const fullName = info.group ? `${info.group}/${info.name}` : info.name;
342
+ addEdges(edges, wsEntry.name || fullName, wsEntry);
244
343
  }
344
+ } else {
345
+ addEdges(edges, key, info.meta);
245
346
  }
246
347
  }
247
- const prodRefs = new Set();
248
- const queue = [...rootProdRefs];
249
- while (queue.length) {
250
- const ref = queue.shift();
251
- if (prodRefs.has(ref)) {
252
- continue;
253
- }
254
- prodRefs.add(ref);
255
- const key = refToKey.get(ref);
256
- if (!key) {
257
- continue;
348
+ // The root resolves at the top level. Bun makes every workspace member a
349
+ // dependency of the root, whether or not the root declares it.
350
+ const rootEdges = new Map();
351
+ if (workspaces[""]) {
352
+ addEdges(rootEdges, "", workspaces[""]);
353
+ }
354
+ for (const ref of memberRefs) {
355
+ if (!rootEdges.has(ref)) {
356
+ rootEdges.set(ref, new Set());
258
357
  }
259
- for (const childRef of runtimeDepRefs(key, infoForKey.get(key).meta)) {
260
- if (!prodRefs.has(childRef)) {
261
- queue.push(childRef);
358
+ rootEdges.get(ref).add("workspace");
359
+ }
360
+
361
+ // Walk the graph from the root following only the given edge kinds.
362
+ const reachableVia = (kinds) => {
363
+ const seen = new Set();
364
+ const stack = [];
365
+ const visit = (edges) => {
366
+ for (const [ref, edgeKinds] of edges) {
367
+ if (seen.has(ref)) {
368
+ continue;
369
+ }
370
+ for (const kind of edgeKinds) {
371
+ if (kinds.includes(kind)) {
372
+ seen.add(ref);
373
+ stack.push(ref);
374
+ break;
375
+ }
376
+ }
262
377
  }
378
+ };
379
+ visit(rootEdges);
380
+ while (stack.length) {
381
+ visit(edgesByRef.get(stack.pop()));
263
382
  }
264
- }
383
+ return seen;
384
+ };
385
+ // Like npm's dev/optional/peer flags, a package is development-only,
386
+ // optional or peer-only when every path from the root reaches it through
387
+ // that kind of edge. Production follows required peers (bun auto-installs
388
+ // them) but not optional peers, which never install anything themselves.
389
+ const installedRefs = reachableVia([
390
+ "dependency",
391
+ "optional",
392
+ "peer",
393
+ "optionalPeer",
394
+ "dev",
395
+ "workspace",
396
+ ]);
397
+ const prodRefs = reachableVia([
398
+ "dependency",
399
+ "optional",
400
+ "peer",
401
+ "workspace",
402
+ ]);
403
+ const nonOptionalRefs = reachableVia([
404
+ "dependency",
405
+ "peer",
406
+ "dev",
407
+ "workspace",
408
+ ]);
409
+ const nonPeerRefs = reachableVia([
410
+ "dependency",
411
+ "optional",
412
+ "dev",
413
+ "workspace",
414
+ ]);
265
415
 
266
416
  // Second pass: emit the package list and dependency graph.
267
417
  const seenRefs = new Set();
268
- for (const [key, info] of infoForKey.entries()) {
418
+ for (const info of infoForKey.values()) {
269
419
  if (seenRefs.has(info.bomRef)) {
270
420
  continue;
271
421
  }
272
422
  seenRefs.add(info.bomRef);
273
- const { group, name, version, registry, meta, integrity, purlString } =
274
- info;
423
+ const {
424
+ group,
425
+ name,
426
+ version,
427
+ componentVersion,
428
+ registry,
429
+ meta,
430
+ integrity,
431
+ purlString,
432
+ } = info;
275
433
  const properties = [{ name: "internal:SrcFile", value: bunLockFile }];
276
434
  const externalReferences = [];
277
435
 
@@ -286,7 +444,7 @@ export async function parseBunLock(bunLockFile, options = {}) {
286
444
  defaultRegistry,
287
445
  group,
288
446
  name,
289
- version,
447
+ componentVersion,
290
448
  );
291
449
  }
292
450
  if (resolvedUrl) {
@@ -318,6 +476,13 @@ export async function parseBunLock(bunLockFile, options = {}) {
318
476
  if (info.isNonRegistry) {
319
477
  properties.push({ name: "cdx:npm:isRegistryDependency", value: "false" });
320
478
  }
479
+ if (info.isWorkspaceDep) {
480
+ properties.push({ name: "cdx:npm:isWorkspace", value: "true" });
481
+ }
482
+ // npm lockfiles record the same bundleDependencies marker as `inBundle`.
483
+ if (bundledRefs.has(info.bomRef) && !unbundledRefs.has(info.bomRef)) {
484
+ properties.push({ name: "cdx:npm:inBundle", value: "true" });
485
+ }
321
486
  if (meta.bin) {
322
487
  const binValue =
323
488
  typeof meta.bin === "object"
@@ -326,17 +491,19 @@ export async function parseBunLock(bunLockFile, options = {}) {
326
491
  properties.push({ name: "cdx:npm:bin", value: binValue });
327
492
  properties.push({ name: "cdx:npm:has_binary", value: "true" });
328
493
  }
329
- if (Array.isArray(meta.os) && meta.os.length) {
330
- properties.push({ name: "cdx:npm:os", value: meta.os.join(", ") });
494
+ const osValue = normalizeOsCpu(meta.os);
495
+ if (osValue) {
496
+ properties.push({ name: "cdx:npm:os", value: osValue });
331
497
  }
332
- if (Array.isArray(meta.cpu) && meta.cpu.length) {
333
- properties.push({ name: "cdx:npm:cpu", value: meta.cpu.join(", ") });
498
+ const cpuValue = normalizeOsCpu(meta.cpu);
499
+ if (cpuValue) {
500
+ properties.push({ name: "cdx:npm:cpu", value: cpuValue });
334
501
  }
335
502
 
336
503
  const pkgObj = {
337
504
  group: group || "",
338
505
  name,
339
- version,
506
+ version: componentVersion ?? undefined,
340
507
  purl: purlString,
341
508
  "bom-ref": info.bomRef,
342
509
  _integrity: integrity || undefined,
@@ -360,21 +527,22 @@ export async function parseBunLock(bunLockFile, options = {}) {
360
527
  }
361
528
  // Packages that are not reachable through production dependencies are
362
529
  // development-only tooling.
363
- if (!prodRefs.has(info.bomRef)) {
530
+ const ref = info.bomRef;
531
+ if (!prodRefs.has(ref)) {
364
532
  pkgObj.scope = "optional";
365
533
  setNpmDevelopmentProperty(pkgObj);
366
534
  }
367
- if (optionalRefs.has(info.bomRef)) {
535
+ if (installedRefs.has(ref) && !nonOptionalRefs.has(ref)) {
368
536
  pkgObj.scope = "optional";
369
537
  setNpmOptionalProperty(pkgObj);
370
538
  }
371
- if (peerRefs.has(info.bomRef)) {
539
+ if (installedRefs.has(ref) && !nonPeerRefs.has(ref)) {
372
540
  setNpmPeerProperty(pkgObj);
373
541
  }
374
542
  pkgList.push(pkgObj);
375
543
  dependenciesList.push({
376
- ref: info.bomRef,
377
- dependsOn: [...runtimeDepRefs(key, meta)].sort(),
544
+ ref,
545
+ dependsOn: [...edgesByRef.get(ref).keys()].sort(),
378
546
  });
379
547
  }
380
548
 
@@ -382,7 +550,7 @@ export async function parseBunLock(bunLockFile, options = {}) {
382
550
  if (options.parentComponent?.["bom-ref"]) {
383
551
  dependenciesList.push({
384
552
  ref: options.parentComponent["bom-ref"],
385
- dependsOn: [...rootProdRefs].sort(),
553
+ dependsOn: [...rootEdges.keys()].sort(),
386
554
  });
387
555
  }
388
556