@cdxgen/cdxgen 13.4.0 → 13.5.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 (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,4 +1,6 @@
1
- import { basename } from "node:path";
1
+ import { createHash } from "node:crypto";
2
+ import { basename, win32 } from "node:path";
3
+ import process from "node:process";
2
4
 
3
5
  import {
4
6
  isDryRun,
@@ -40,6 +42,18 @@ function getConfiguredHbomPathAllowlist() {
40
42
  );
41
43
  }
42
44
 
45
+ // A Windows plan can be inspected from any host (`getCommandPlan`), where
46
+ // node:path would not split a path on its backslashes.
47
+ function isWindowsStyleCommand(command) {
48
+ return command.includes("\\") || /^[A-Za-z]:/u.test(command);
49
+ }
50
+
51
+ function getHbomCommandName(command) {
52
+ return isWindowsStyleCommand(command)
53
+ ? win32.basename(command)
54
+ : basename(command);
55
+ }
56
+
43
57
  function isAllowedHbomCommand(command, allowlistValue = undefined) {
44
58
  const normalizedCommand = `${command ?? ""}`.trim();
45
59
  if (!normalizedCommand) {
@@ -51,7 +65,10 @@ function isAllowedHbomCommand(command, allowlistValue = undefined) {
51
65
  if (!allowlistEntries.length) {
52
66
  return true;
53
67
  }
54
- const commandName = basename(normalizedCommand);
68
+ // Exact and case-sensitive on purpose: cdx-hbom applies the same allowlist
69
+ // again when it spawns, so a looser preflight would pass here and then run
70
+ // nothing, which silently degrades the BOM.
71
+ const commandName = getHbomCommandName(normalizedCommand);
55
72
  return allowlistEntries.some(
56
73
  (entry) => entry === normalizedCommand || entry === commandName,
57
74
  );
@@ -61,9 +78,27 @@ function formatHbomCommandTarget(command, args = []) {
61
78
  return `${command}${args.length ? ` ${args.join(" ")}` : ""}`;
62
79
  }
63
80
 
81
+ // Windows plan entries carry a multi-kilobyte PowerShell script as their last
82
+ // argument. Like the cdx-hbom trace and evidence, the preflight names it by
83
+ // digest so a blocked run does not print every script.
84
+ function summarizeHbomPlanArgs(planEntry) {
85
+ const args = [...(planEntry.args || [])];
86
+ if (planEntry.parser !== "windows-envelope" || !args.length) {
87
+ return args;
88
+ }
89
+ const digest = createHash("sha256")
90
+ .update(String(args.at(-1)))
91
+ .digest("hex")
92
+ .slice(0, 12);
93
+ return ["-Command", planEntry.id, "script", `sha256:${digest}`];
94
+ }
95
+
64
96
  function buildHbomPlanActivities(planEntry, normalizedOptions) {
65
97
  const baseArgs = [...(planEntry.args || [])];
66
- const baseTarget = formatHbomCommandTarget(planEntry.command, baseArgs);
98
+ const baseTarget = formatHbomCommandTarget(
99
+ planEntry.command,
100
+ summarizeHbomPlanArgs(planEntry),
101
+ );
67
102
  const activities = [
68
103
  {
69
104
  args: baseArgs,
@@ -115,7 +150,7 @@ function collectDisallowedHbomCommands(
115
150
  }
116
151
  const existingEntry = disallowedCommands.get(requestedCommand) ?? {
117
152
  command: requestedCommand,
118
- commandName: basename(requestedCommand),
153
+ commandName: getHbomCommandName(requestedCommand),
119
154
  ids: new Set(),
120
155
  targets: new Set(),
121
156
  };
@@ -214,6 +249,28 @@ function createHbomAllowlistPreflightError(
214
249
  return new Error(messageLines.join("\n"));
215
250
  }
216
251
 
252
+ /**
253
+ * Whether cdx-hbom spawns no command for these collector options.
254
+ *
255
+ * Only the Linux collector reads `includeCommandEnrichment: false` as "run no
256
+ * commands", and even there the privileged dmidecode step still runs. The
257
+ * Darwin collector ignores the option, and the Windows collector always runs
258
+ * its `windows-cim-platform` batch, so those plans are checked in full.
259
+ *
260
+ * @param {object} normalizedOptions Collector options from normalizeHbomOptions
261
+ * @returns {boolean} `true` when the allowlist has nothing to check
262
+ */
263
+ function collectorSpawnsNoCommands(normalizedOptions) {
264
+ const targetPlatform = `${normalizedOptions.platform ?? process.platform}`
265
+ .trim()
266
+ .toLowerCase();
267
+ return (
268
+ normalizedOptions.includeCommandEnrichment === false &&
269
+ normalizedOptions.includePrivilegedEnrichment !== true &&
270
+ targetPlatform === "linux"
271
+ );
272
+ }
273
+
217
274
  async function enforceSecureModeHbomAllowlists(hbomModule, normalizedOptions) {
218
275
  if (isDryRun || normalizedOptions?.isDryRun || !isSecureMode) {
219
276
  return;
@@ -249,7 +306,7 @@ async function enforceSecureModeHbomAllowlists(hbomModule, normalizedOptions) {
249
306
  : [];
250
307
  } else if (
251
308
  hasCommandAllowlist &&
252
- normalizedOptions.includeCommandEnrichment !== false
309
+ !collectorSpawnsNoCommands(normalizedOptions)
253
310
  ) {
254
311
  traceActivities = hbomModule
255
312
  .getCommandPlan(normalizedOptions)
@@ -259,7 +316,7 @@ async function enforceSecureModeHbomAllowlists(hbomModule, normalizedOptions) {
259
316
  }
260
317
 
261
318
  const disallowedCommands =
262
- hasCommandAllowlist && normalizedOptions.includeCommandEnrichment !== false
319
+ hasCommandAllowlist && !collectorSpawnsNoCommands(normalizedOptions)
263
320
  ? collectDisallowedHbomCommands(traceActivities, commandAllowlistValue)
264
321
  : [];
265
322
  const disallowedPaths = hasPathAllowlist
@@ -398,8 +455,11 @@ export function normalizeHbomOptions(options = {}) {
398
455
  timeoutValue === undefined || timeoutValue === null || timeoutValue === ""
399
456
  ? undefined
400
457
  : Number.parseInt(`${timeoutValue}`, 10);
458
+ // yargs negates `--no-command-enrichment` into `commandEnrichment: false`
459
+ // and never sets `noCommandEnrichment`, so the CLIs rely on this spelling.
401
460
  const includeCommandEnrichment =
402
- options.includeCommandEnrichment ?? !options.noCommandEnrichment;
461
+ options.includeCommandEnrichment ??
462
+ (options.commandEnrichment !== false && !options.noCommandEnrichment);
403
463
  const allowPartial = options.allowPartial ?? !options.strict;
404
464
 
405
465
  return {
@@ -174,6 +174,22 @@ export function isHbomLikeBom(bomJson) {
174
174
  );
175
175
  }
176
176
 
177
+ /**
178
+ * Explain how the permission-denied enrichments of an HBOM can be recovered.
179
+ *
180
+ * Windows never uses sudo: the elevated-only batch needs an already-elevated
181
+ * session, and a network logon is denied every WMI query.
182
+ *
183
+ * @param {string|undefined} platform `cdx:hbom:platform` of the HBOM
184
+ * @returns {string} Guidance for the platform; the Linux wording for any other platform
185
+ */
186
+ export function getHbomPrivilegeGuidance(platform) {
187
+ if (`${platform ?? ""}`.toLowerCase() === "windows") {
188
+ return "Some Windows enrichments were denied. Run HBOM from a local session (a non-administrator over SSH or WinRM is denied every WMI query) and, for the TPM, BitLocker, storage-reliability and Secure Boot data, from an elevated (Run as administrator) prompt with --privileged. HBOM never prompts for elevation.";
189
+ }
190
+ return "Some Linux enrichments can likely succeed only with --privileged and a target environment that allows non-interactive sudo.";
191
+ }
192
+
177
193
  /**
178
194
  * Return a component's `cdx:hbom:hardwareClass` property value.
179
195
  *
@@ -276,6 +276,7 @@ function isFilesystemIdentity(value) {
276
276
  function createRuntimeIndexes(runtimeComponents) {
277
277
  const interfaceAddressesByName = new Map();
278
278
  const kernelModulesByName = new Map();
279
+ const logicalDrivesByDeviceId = new Map();
279
280
  const mountPathsByDevice = new Map();
280
281
  const mountPathsByPath = new Map();
281
282
  const runtimeByStorageIdentity = new Map();
@@ -306,6 +307,13 @@ function createRuntimeIndexes(runtimeComponents) {
306
307
  component["bom-ref"],
307
308
  );
308
309
  }
310
+ if (runtimeCategory === "logical_drives") {
311
+ appendMapEntry(
312
+ logicalDrivesByDeviceId,
313
+ toNormalizedIdentity(getPropertyValue(component, "device_id")),
314
+ component["bom-ref"],
315
+ );
316
+ }
309
317
  if (runtimeCategory === "mount_hardening") {
310
318
  appendMapEntry(
311
319
  mountPathsByDevice,
@@ -339,6 +347,7 @@ function createRuntimeIndexes(runtimeComponents) {
339
347
  return {
340
348
  interfaceAddressesByName,
341
349
  kernelModulesByName,
350
+ logicalDrivesByDeviceId,
342
351
  mountPathsByDevice,
343
352
  mountPathsByPath,
344
353
  runtimeByStorageIdentity,
@@ -532,6 +541,14 @@ function createDependencyEdgeList(
532
541
  "mount_hardening",
533
542
  Array.from(indexes.mountPathsByPath.get(mountPath) || []),
534
543
  );
544
+ // The Windows osquery pack has no mount_hardening; a volume's drive
545
+ // letter (C:) is the device_id of its logical drive.
546
+ addCategoryRefs(
547
+ linkedRefs,
548
+ linkedCategoryRefs,
549
+ "logical_drives",
550
+ Array.from(indexes.logicalDrivesByDeviceId.get(mountPath) || []),
551
+ );
535
552
  }
536
553
  const explicitFilesystemIdentity = isFilesystemIdentity(component.version)
537
554
  ? toNormalizedIdentity(component.version)
@@ -788,6 +805,10 @@ export function mergeHostInventoryBoms(hbomJson, obomData) {
788
805
  if (obomData.parentComponent?.name && obomData.parentComponent?.type) {
789
806
  runtimeComponents.unshift(obomData.parentComponent);
790
807
  }
808
+ // trimComponents keys a component without a purl or bom-ref on name and
809
+ // version, which would fold hardware that shares a name (every Windows
810
+ // volume without a drive letter is named "Volume") into one component.
811
+ ensureBomRefs(hbomJson);
791
812
  const mergedBomJson = {
792
813
  ...hbomJson,
793
814
  components: trimComponents([
@@ -0,0 +1,244 @@
1
+ // The one place that decides what purl an operating-system package gets.
2
+ //
3
+ // A package found by `cdxgen -t docker <image>` and by `obom` run inside that
4
+ // image must be the same package, so the OBOM rows, the file-to-package
5
+ // resolver and the container scan all name a deb, an rpm or an apk package with
6
+ // the vendor and the qualifiers set here and in getDistroInfo(): `arch`,
7
+ // `distro`, `distro_name` and, when the package has one, `epoch`. A pacman
8
+ // package has the vendor and only the `arch` qualifier the alpm type defines.
9
+ // Homebrew, MacPorts and snap packages have no distro, so their purl is the
10
+ // name and the version; a flatpak adds the `arch` and `branch` of its ref.
11
+ //
12
+ // None of these purls has a subpath. A subpath names a path inside a package;
13
+ // the dpkg directory or the source rpm of a row is not one.
14
+
15
+ import { getDistroInfo, isCleanDistroQualifier } from "./osinfo.js";
16
+ import { tryBuildPurl } from "./purl.js";
17
+
18
+ /**
19
+ * The purl types an OS scan builds from what a package manager reports.
20
+ * deb, rpm, apk and alpm belong to a distro; brew, macports, snap and flatpak
21
+ * belong to none. brew, macports, snap and flatpak are not purl-spec types;
22
+ * cdx-purl builds them as it builds `pkg:chocolatey`.
23
+ */
24
+ export const OS_PACKAGE_PURL_TYPES = new Set([
25
+ "deb",
26
+ "rpm",
27
+ "apk",
28
+ "alpm",
29
+ "brew",
30
+ "macports",
31
+ "snap",
32
+ "flatpak",
33
+ ]);
34
+
35
+ /**
36
+ * Whether a purl type is one of the OS package managers handled here.
37
+ *
38
+ * @param {string|undefined} type Purl type
39
+ * @returns {boolean} true for the types in OS_PACKAGE_PURL_TYPES
40
+ */
41
+ export function isOsPackagePurlType(type) {
42
+ return OS_PACKAGE_PURL_TYPES.has(type);
43
+ }
44
+
45
+ /**
46
+ * Whether a value names an architecture. rpm says `(none)` and trivy says
47
+ * `None` for a package that has none, such as an rpm's `gpg-pubkey` key record;
48
+ * neither is an architecture, and neither belongs in a purl.
49
+ *
50
+ * @param {string|undefined|null} arch Architecture as a package manager reports it
51
+ * @returns {boolean} true for a real architecture
52
+ */
53
+ export function isRealArchitecture(arch) {
54
+ const value = `${arch ?? ""}`.trim().toLowerCase();
55
+ return value !== "" && value !== "(none)" && value !== "none";
56
+ }
57
+
58
+ function isDigits(text) {
59
+ if (!text) {
60
+ return false;
61
+ }
62
+ for (let i = 0; i < text.length; i++) {
63
+ const code = text.charCodeAt(i);
64
+ if (code < 48 || code > 57) {
65
+ return false;
66
+ }
67
+ }
68
+ return true;
69
+ }
70
+
71
+ // An epoch is a non-negative integer, and 0 is the same as none. Anything else
72
+ // ("", "(none)", "null") is no epoch.
73
+ function normaliseEpoch(value) {
74
+ const text = `${value ?? ""}`.trim();
75
+ if (!isDigits(text)) {
76
+ return undefined;
77
+ }
78
+ const digits = text.replace(/^0+/, "");
79
+ return digits || undefined;
80
+ }
81
+
82
+ // dpkg writes a version as [epoch:]upstream[-revision]. A colon is only ever an
83
+ // epoch's separator when what comes before it is all digits.
84
+ function debVersionEpoch(version) {
85
+ const colon = version.indexOf(":");
86
+ if (colon <= 0) {
87
+ return undefined;
88
+ }
89
+ const prefix = version.slice(0, colon);
90
+ return isDigits(prefix) ? prefix : undefined;
91
+ }
92
+
93
+ /**
94
+ * The epoch of an OS package, as the `epoch` qualifier carries it.
95
+ *
96
+ * An rpm keeps its epoch in a field of its own. A deb keeps it in its version
97
+ * (`2:93`), so the version is read first. Epoch 0 and no epoch are the same,
98
+ * and apk and Homebrew packages have none.
99
+ *
100
+ * @param {{type: string, version?: string, epoch?: string|number}} pkg Package
101
+ * @returns {string|undefined} The epoch without leading zeros, or undefined
102
+ */
103
+ export function osPackageEpoch(pkg) {
104
+ switch (pkg?.type) {
105
+ case "rpm":
106
+ return normaliseEpoch(pkg.epoch);
107
+ case "deb": {
108
+ const fromVersion = debVersionEpoch(`${pkg.version ?? ""}`);
109
+ return fromVersion === undefined
110
+ ? normaliseEpoch(pkg.epoch)
111
+ : normaliseEpoch(fromVersion);
112
+ }
113
+ default:
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * The version an OS package carries on its component and in its purl.
120
+ *
121
+ * - rpm: `[epoch:]version-release`, the form the container scan writes. The
122
+ * purl spec keeps the epoch out of an rpm version, but the container scan
123
+ * puts it in the version as well as in the `epoch` qualifier, and an OBOM must
124
+ * name the package the same way.
125
+ * - deb: the version exactly as dpkg reports it, epoch included.
126
+ * - macports: `version_revision`, the form `port installed` prints.
127
+ * - apk, alpm, brew, snap and flatpak: the version as it is. A pacman version
128
+ * already holds its epoch and release (`1:1.3.2-4`).
129
+ *
130
+ * @param {{type: string, version?: string, release?: string, epoch?: string|number, revision?: string|number}} pkg Package
131
+ * @returns {string} The version, or an empty string when the package has none
132
+ */
133
+ export function osPackageVersion(pkg) {
134
+ const version = `${pkg?.version ?? ""}`;
135
+ if (!version) {
136
+ return "";
137
+ }
138
+ switch (pkg.type) {
139
+ case "rpm": {
140
+ const release = `${pkg.release ?? ""}`;
141
+ const evr = release ? `${version}-${release}` : version;
142
+ const epoch = osPackageEpoch(pkg);
143
+ return epoch ? `${epoch}:${evr}` : evr;
144
+ }
145
+ case "deb": {
146
+ if (debVersionEpoch(version) !== undefined) {
147
+ return version;
148
+ }
149
+ const epoch = normaliseEpoch(pkg.epoch);
150
+ return epoch ? `${epoch}:${version}` : version;
151
+ }
152
+ case "macports": {
153
+ // Revision 0 is a real revision: `port installed` prints `@3.6.0_0`.
154
+ const revision = `${pkg.revision ?? ""}`.trim();
155
+ return isDigits(revision) ? `${version}_${revision}` : version;
156
+ }
157
+ default:
158
+ return version;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Build the purl of an OS package.
164
+ *
165
+ * deb, rpm and apk purls take their vendor (`ubuntu`, `almalinux`, `alpine`,
166
+ * `wolfi`) and the `distro` and `distro_name` qualifiers from the distro, plus
167
+ * `arch` and `epoch` from the package. alpm purls take the vendor (`arch`,
168
+ * `manjaro`) and `arch` only: that is all the alpm type defines, and an Arch
169
+ * system is rolling, with no release to name. A qualifier with no value is
170
+ * left out. brew, macports and snap purls carry neither vendor nor qualifiers;
171
+ * a flatpak purl carries the `arch` and `branch` of the flatpak ref.
172
+ *
173
+ * @param {Object} pkg The package
174
+ * @param {string} pkg.type One of OS_PACKAGE_PURL_TYPES
175
+ * @param {string} pkg.name Package name; a flatpak's application ID
176
+ * @param {string} [pkg.version] deb: the dpkg version. rpm: the version tag only. apk: version-rN. macports: the version without its revision.
177
+ * @param {string} [pkg.release] rpm release tag
178
+ * @param {string|number} [pkg.epoch] rpm epoch tag
179
+ * @param {string|number} [pkg.revision] MacPorts revision
180
+ * @param {string} [pkg.arch] Package architecture
181
+ * @param {string} [pkg.branch] flatpak branch (`stable`, `48`)
182
+ * @param {{namespace?: string, distroId?: string, distroName?: string}} [distro]
183
+ * What getDistroInfo() reports; read from the host's os-release when left out
184
+ * @returns {string|null} The purl, or null when the parts do not form a valid one
185
+ */
186
+ export function buildOsPackagePurl(pkg, distro) {
187
+ const type = pkg?.type;
188
+ const name = `${pkg?.name ?? ""}`;
189
+ if (!isOsPackagePurlType(type) || !name) {
190
+ return null;
191
+ }
192
+ const version = osPackageVersion(pkg) || null;
193
+ switch (type) {
194
+ case "brew":
195
+ case "macports":
196
+ case "snap":
197
+ return tryBuildPurl({ type, name, version });
198
+ case "flatpak": {
199
+ const qualifiers = {};
200
+ if (isRealArchitecture(pkg.arch)) {
201
+ qualifiers.arch = `${pkg.arch}`.trim();
202
+ }
203
+ const branch = `${pkg.branch ?? ""}`.trim();
204
+ if (branch) {
205
+ qualifiers.branch = branch;
206
+ }
207
+ return tryBuildPurl({
208
+ type,
209
+ name,
210
+ version,
211
+ qualifiers: Object.keys(qualifiers).length ? qualifiers : null,
212
+ });
213
+ }
214
+ default:
215
+ break;
216
+ }
217
+ const { namespace, distroId, distroName } = distro || getDistroInfo();
218
+ if (!namespace) {
219
+ return null;
220
+ }
221
+ const qualifiers = {};
222
+ if (isRealArchitecture(pkg.arch)) {
223
+ qualifiers.arch = `${pkg.arch}`.trim();
224
+ }
225
+ if (type !== "alpm") {
226
+ if (isCleanDistroQualifier(distroId)) {
227
+ qualifiers.distro = distroId;
228
+ }
229
+ if (isCleanDistroQualifier(distroName)) {
230
+ qualifiers.distro_name = distroName;
231
+ }
232
+ const epoch = osPackageEpoch(pkg);
233
+ if (epoch) {
234
+ qualifiers.epoch = epoch;
235
+ }
236
+ }
237
+ return tryBuildPurl({
238
+ type,
239
+ namespace,
240
+ name,
241
+ version,
242
+ qualifiers: Object.keys(qualifiers).length ? qualifiers : null,
243
+ });
244
+ }