@cdxgen/cdxgen 13.5.0 → 13.6.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 (182) hide show
  1. package/README.md +6 -6
  2. package/bin/cdxgen.js +4 -0
  3. package/bin/evinse.js +3 -0
  4. package/bin/validate.js +6 -0
  5. package/data/license-aliases.json +24 -2
  6. package/data/remediations.json +124 -0
  7. package/data/rules/ai-bom.yaml +1 -1
  8. package/data/rules/brotli-decoder.yaml +107 -0
  9. package/data/rules/vscode-extensions.yaml +75 -0
  10. package/data/spdx-license-list.json +121 -7
  11. package/data/spdx-licenses.json +69 -0
  12. package/data/spdx.schema.json +845 -2
  13. package/lib/cli/bomAssembly.js +92 -3
  14. package/lib/cli/index.js +108 -1
  15. package/lib/cli/jsBom.js +153 -64
  16. package/lib/cli/jvmBom.js +237 -46
  17. package/lib/cli/managedBom.js +142 -16
  18. package/lib/cli/nativeBom.js +42 -5
  19. package/lib/core/boundedFile.js +344 -0
  20. package/lib/core/brotliProbe.js +182 -0
  21. package/lib/core/env.js +8 -0
  22. package/lib/core/fs.js +12 -0
  23. package/lib/core/httpClient.js +98 -1
  24. package/lib/core/lockPaths.js +38 -0
  25. package/lib/core/paths.js +24 -1
  26. package/lib/core/processTree.js +5 -0
  27. package/lib/core/toolRegistry.js +170 -0
  28. package/lib/ecosystems/cppEvidence.js +263 -106
  29. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  30. package/lib/ecosystems/ecosystems.js +25 -10
  31. package/lib/ecosystems/gemHostProbe.js +301 -0
  32. package/lib/ecosystems/gradleutils.js +828 -224
  33. package/lib/ecosystems/parsers-bazel.js +107 -6
  34. package/lib/ecosystems/parsers-js.js +505 -22
  35. package/lib/ecosystems/parsers-misc.js +209 -20
  36. package/lib/ecosystems/parsers-python.js +89 -2
  37. package/lib/ecosystems/pythonutils.js +128 -2
  38. package/lib/ecosystems/rubyutils.js +538 -31
  39. package/lib/ecosystems/scalacliutils.js +2 -0
  40. package/lib/ecosystems/scalasem.js +796 -151
  41. package/lib/ecosystems/vendoredCode.js +237 -3
  42. package/lib/ecosystems/venvProbe.js +360 -0
  43. package/lib/evinser/evinser.js +346 -43
  44. package/lib/helpers/agenticToolutils.js +413 -93
  45. package/lib/helpers/vsixutils.js +1574 -315
  46. package/lib/inventory/atomUtils.js +77 -0
  47. package/lib/inventory/brotliDecoder.js +142 -0
  48. package/lib/inventory/buildToolRateLimit.js +1 -1
  49. package/lib/inventory/cbomutils.js +56 -7
  50. package/lib/inventory/deps.js +35 -0
  51. package/lib/inventory/dosai.js +139 -5
  52. package/lib/inventory/dotnetRuntime.js +217 -0
  53. package/lib/inventory/envcontext.js +307 -13
  54. package/lib/inventory/evidenceUtils.js +11 -0
  55. package/lib/inventory/fetchBatch.js +1 -1
  56. package/lib/inventory/formulationParsers.js +31 -3
  57. package/lib/inventory/gtfobins.js +13 -0
  58. package/lib/inventory/hostRuntimeProbe.js +686 -0
  59. package/lib/inventory/jvmLocalRepos.js +9 -2
  60. package/lib/inventory/jvmToolEnv.js +29 -0
  61. package/lib/inventory/licenseEnhancer.js +12 -0
  62. package/lib/inventory/mavenCentralMirror.js +534 -0
  63. package/lib/inventory/nodeBuildInfo.js +218 -0
  64. package/lib/inventory/nodeHostProbe.js +202 -0
  65. package/lib/inventory/osPackageResolver.js +182 -0
  66. package/lib/inventory/phpBuildInfo.js +10 -59
  67. package/lib/inventory/phpHostProbe.js +32 -488
  68. package/lib/inventory/rubyBuildInfo.js +972 -0
  69. package/lib/inventory/rubyHostProbe.js +218 -0
  70. package/lib/inventory/scalaCoords.js +4 -3
  71. package/lib/inventory/scalaDedupe.js +440 -0
  72. package/lib/inventory/source.js +49 -2
  73. package/lib/managers/binary.js +337 -3
  74. package/lib/managers/docker.js +79 -0
  75. package/lib/parsers/largeJson.js +28 -11
  76. package/lib/parsers/vscodeExtensionId.js +183 -0
  77. package/lib/server/server.js +12 -1
  78. package/lib/stages/postgen/ruleEngine.js +13 -5
  79. package/lib/stages/pregen/envAudit.js +1 -0
  80. package/lib/stages/pregen/pregen.js +114 -57
  81. package/lib/validator/bomValidator.js +6 -1
  82. package/lib/validator/generated/validate-1.6.mjs +1 -1
  83. package/lib/validator/generated/validate-1.7.mjs +1 -1
  84. package/lib/validator/generated/validate-2.0.mjs +1 -1
  85. package/lib/validator/index.js +1 -1
  86. package/package.json +3 -2
  87. package/types/lib/cli/bomAssembly.d.ts +13 -1
  88. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/cli/jsBom.d.ts +1 -17
  91. package/types/lib/cli/jsBom.d.ts.map +1 -1
  92. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  93. package/types/lib/cli/managedBom.d.ts.map +1 -1
  94. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  95. package/types/lib/core/boundedFile.d.ts +70 -0
  96. package/types/lib/core/boundedFile.d.ts.map +1 -0
  97. package/types/lib/core/brotliProbe.d.ts +37 -0
  98. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  99. package/types/lib/core/env.d.ts +4 -0
  100. package/types/lib/core/env.d.ts.map +1 -1
  101. package/types/lib/core/fs.d.ts.map +1 -1
  102. package/types/lib/core/httpClient.d.ts +24 -0
  103. package/types/lib/core/httpClient.d.ts.map +1 -1
  104. package/types/lib/core/lockPaths.d.ts +19 -0
  105. package/types/lib/core/lockPaths.d.ts.map +1 -0
  106. package/types/lib/core/processTree.d.ts +2 -0
  107. package/types/lib/core/processTree.d.ts.map +1 -1
  108. package/types/lib/core/toolRegistry.d.ts +36 -0
  109. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  110. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  111. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  112. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  113. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  114. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  115. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  116. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  117. package/types/lib/ecosystems/gradleutils.d.ts +47 -13
  118. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  121. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  123. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  125. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  126. package/types/lib/ecosystems/pythonutils.d.ts +20 -2
  127. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  128. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  129. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  131. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  132. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  133. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  134. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  135. package/types/lib/evinser/evinser.d.ts +63 -8
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/inventory/atomUtils.d.ts +19 -0
  138. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  139. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  140. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  141. package/types/lib/inventory/cbomutils.d.ts +17 -0
  142. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  143. package/types/lib/inventory/deps.d.ts.map +1 -1
  144. package/types/lib/inventory/dosai.d.ts +17 -1
  145. package/types/lib/inventory/dosai.d.ts.map +1 -1
  146. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  147. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  148. package/types/lib/inventory/envcontext.d.ts +52 -6
  149. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  150. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  151. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  152. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  153. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  154. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  155. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  156. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  157. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  158. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  159. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  160. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  161. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  162. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  163. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  164. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  165. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  166. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  167. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  168. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  169. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  170. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  171. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  172. package/types/lib/inventory/source.d.ts +10 -0
  173. package/types/lib/inventory/source.d.ts.map +1 -1
  174. package/types/lib/managers/binary.d.ts.map +1 -1
  175. package/types/lib/managers/docker.d.ts +12 -0
  176. package/types/lib/managers/docker.d.ts.map +1 -1
  177. package/types/lib/parsers/largeJson.d.ts +4 -3
  178. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  179. package/types/lib/server/server.d.ts.map +1 -1
  180. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  182. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -30,6 +30,7 @@ import {
30
30
  prefetchedResponse,
31
31
  prefetchJson,
32
32
  } from "./fetchBatch.js";
33
+ import { mavenCentralFetchUrl } from "./mavenCentralMirror.js";
33
34
  import { canonicalPurlFromLooseString } from "./purl.js";
34
35
 
35
36
  /** Raised when a caller omits a registry helper that the purl type requires. */
@@ -795,6 +796,48 @@ export function getGitRemoteHost(remote) {
795
796
  return host.toLowerCase();
796
797
  }
797
798
 
799
+ /**
800
+ * Remove credentials from git output before it is logged. Every URL in the
801
+ * text loses its user name and password, which git copies into its error
802
+ * messages from the remote it was given.
803
+ *
804
+ * @param {string|undefined} text Output of a git command
805
+ * @param {string|undefined} repoUrl Remote the command was given
806
+ * @returns {string|undefined} Output with credentials replaced by ***
807
+ */
808
+ export function redactGitOutput(text, repoUrl) {
809
+ if (!text || typeof text !== "string") {
810
+ return text;
811
+ }
812
+ let redacted = text;
813
+ if (repoUrl && typeof repoUrl === "string") {
814
+ redacted = redacted.split(repoUrl).join(sanitizeRemoteUrlForLogs(repoUrl));
815
+ }
816
+ let from = 0;
817
+ while (from < redacted.length) {
818
+ const schemeEnd = redacted.indexOf("://", from);
819
+ if (schemeEnd === -1) {
820
+ break;
821
+ }
822
+ const authorityStart = schemeEnd + 3;
823
+ let authorityEnd = authorityStart;
824
+ while (
825
+ authorityEnd < redacted.length &&
826
+ !"/?#'\"<> \t\r\n".includes(redacted[authorityEnd])
827
+ ) {
828
+ authorityEnd++;
829
+ }
830
+ const at = redacted.lastIndexOf("@", authorityEnd - 1);
831
+ if (at >= authorityStart) {
832
+ redacted = `${redacted.slice(0, authorityStart)}***:***${redacted.slice(at)}`;
833
+ from = authorityStart + "***:***".length;
834
+ } else {
835
+ from = authorityEnd;
836
+ }
837
+ }
838
+ return redacted;
839
+ }
840
+
798
841
  /**
799
842
  * Clone a git repository into a temporary directory.
800
843
  *
@@ -859,8 +902,11 @@ export function gitClone(repoUrl, branch = null) {
859
902
  `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
860
903
  );
861
904
  const result = hardenedGitCommand(gitArgs);
862
- if (result.status !== 0) {
863
- console.error(result.stderr);
905
+ if (result.status !== 0 || result.error) {
906
+ console.error(redactGitOutput(result.stderr, repoUrl));
907
+ // The directory was created by this call, so it is always safe to remove.
908
+ safeRmSync(tempDir, { recursive: true, force: true });
909
+ throw new Error("Git clone failed.");
864
910
  }
865
911
 
866
912
  return tempDir;
@@ -1588,6 +1634,7 @@ export async function resolveGitUrlFromPurl(purlString, helpers = {}) {
1588
1634
  break;
1589
1635
  }
1590
1636
  const mavenCentralUrl =
1637
+ mavenCentralFetchUrl() ||
1591
1638
  readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
1592
1639
  "https://repo1.maven.org/maven2/";
1593
1640
  registry = mavenCentralUrl.endsWith("/")
@@ -38,14 +38,24 @@ import {
38
38
  safeRmSync,
39
39
  safeSpawnSync,
40
40
  } from "../core/fs.js";
41
+ import { thoughtLog } from "../core/logger.js";
41
42
  import { mapWithConcurrency } from "../core/parallel.js";
42
43
  import { isValidDriveRoot } from "../core/paths.js";
43
44
  import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
45
+ import {
46
+ attachBrotliDecoder,
47
+ createBrotliInventory,
48
+ finishBrotliInventory,
49
+ MAX_BROTLI_PROBE_BYTES,
50
+ MAX_BROTLI_PROBES,
51
+ readBrotliDecoder,
52
+ } from "../inventory/brotliDecoder.js";
44
53
  import {
45
54
  CertificateCollector,
46
55
  classifyCryptoFile,
47
56
  } from "../inventory/certificates.js";
48
57
  import { createContainerRiskProperties } from "../inventory/containerRisk.js";
58
+ import { collectDotnetRuntimes } from "../inventory/dotnetRuntime.js";
49
59
  import {
50
60
  attachIdentityTools,
51
61
  extractToolRefs,
@@ -55,6 +65,13 @@ import {
55
65
  readGoBuildVersion,
56
66
  } from "../inventory/goBuildInfo.js";
57
67
  import { createGtfoBinsProperties } from "../inventory/gtfobins.js";
68
+ import {
69
+ attachNodeBuild,
70
+ createNodeRuntimeInventory,
71
+ isNodeBinaryName,
72
+ MAX_NODE_PROBES,
73
+ readNodeBuildVersion,
74
+ } from "../inventory/nodeBuildInfo.js";
58
75
  import {
59
76
  canonicalDistroQualifier,
60
77
  getDistroInfo,
@@ -66,6 +83,7 @@ import {
66
83
  import { isRealArchitecture } from "../inventory/osPackagePurl.js";
67
84
  import {
68
85
  collectExecutables,
86
+ collectNativeExtensions,
69
87
  collectSharedLibs,
70
88
  } from "../inventory/osPackageResolver.js";
71
89
  import {
@@ -87,6 +105,15 @@ import {
87
105
  tryBuildPurl,
88
106
  } from "../inventory/purl.js";
89
107
  import { collectRootfsCertificates } from "../inventory/rootfsCerts.js";
108
+ import {
109
+ attachRubyBuild,
110
+ createRubyRuntimeInventory,
111
+ deriveOsPackagedRubyRuntimes,
112
+ isLibrubyName,
113
+ isRubyBinaryName,
114
+ MAX_RUBY_PROBES,
115
+ readRubyBuildVersion,
116
+ } from "../inventory/rubyBuildInfo.js";
90
117
  import {
91
118
  adjustLicenseInformation,
92
119
  findLicenseId,
@@ -538,6 +565,15 @@ const COMMON_RUNTIMES = [
538
565
  "openjdk25-jre",
539
566
  ];
540
567
 
568
+ // Runtime packages whose name carries the series, such as Debian's ruby3.1
569
+ // and libruby3.1, openSUSE's libruby3_4-3_4 and Wolfi's ruby-3.3.
570
+ const VERSIONED_RUNTIME_PATTERNS = [
571
+ /^(lib)?ruby\d+\.\d+(t64)?$/,
572
+ /^ruby-\d+\.\d+$/,
573
+ /^ruby-libs$/,
574
+ /^libruby\d+_\d+-\d+_\d+$/,
575
+ ];
576
+
541
577
  /**
542
578
  * Run the `cargo-auditable` companion binary against the given source path and
543
579
  * return its stdout.
@@ -829,7 +865,7 @@ export function dropRedundantSourceComponents(pkgList) {
829
865
  const installedPurls = new Set();
830
866
  for (const comp of pkgList) {
831
867
  if (comp?.purl && !comp?.tags?.includes("source")) {
832
- installedPurls.add(comp.purl);
868
+ installedPurls.add(withoutArch(comp.purl));
833
869
  }
834
870
  }
835
871
  if (!installedPurls.size) {
@@ -837,13 +873,33 @@ export function dropRedundantSourceComponents(pkgList) {
837
873
  }
838
874
  for (let i = pkgList.length - 1; i >= 0; i--) {
839
875
  const comp = pkgList[i];
840
- if (comp?.tags?.includes("source") && installedPurls.has(comp.purl)) {
876
+ if (
877
+ comp?.tags?.includes("source") &&
878
+ installedPurls.has(withoutArch(comp.purl))
879
+ ) {
841
880
  pkgList.splice(i, 1);
842
881
  }
843
882
  }
844
883
  return pkgList;
845
884
  }
846
885
 
886
+ // The arch qualifier deb and rpm purls give a source package.
887
+ const SOURCE_PACKAGE_ARCH = { deb: "source", rpm: "src" };
888
+
889
+ // A source shadow is named like an installed package of the same name and
890
+ // version, whatever arch either names, so arch is left out of the comparison.
891
+ function withoutArch(purl) {
892
+ const [base, query] = `${purl}`.split("?");
893
+ if (!query) {
894
+ return base;
895
+ }
896
+ const kept = query
897
+ .split("&")
898
+ .filter((pair) => !pair.startsWith("arch="))
899
+ .sort();
900
+ return kept.length ? `${base}?${kept.join("&")}` : base;
901
+ }
902
+
847
903
  /**
848
904
  * Get the packages installed in the container image filesystem.
849
905
  *
@@ -1346,6 +1402,14 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1346
1402
  if (epoch) {
1347
1403
  newCompQualifiers.epoch = epoch;
1348
1404
  }
1405
+ // A source package is built for no architecture. The deb and
1406
+ // rpm purl types name it `source` and `src`; copying the arch
1407
+ // of whichever binary led here (`all` from a -doc package)
1408
+ // misnames it.
1409
+ const sourceArch = SOURCE_PACKAGE_ARCH[purlObj.type];
1410
+ if (sourceArch && newCompQualifiers.arch) {
1411
+ newCompQualifiers.arch = sourceArch;
1412
+ }
1349
1413
  newComp.purl = build({
1350
1414
  type: purlObj.type,
1351
1415
  namespace: purlObj.namespace || null,
@@ -1381,6 +1445,7 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1381
1445
  components: ownedFileComponents,
1382
1446
  dependenciesList: ownedFileDependencies,
1383
1447
  ownedFilePaths,
1448
+ ownerByPath,
1384
1449
  services: ownedServices,
1385
1450
  } = await createOSPackageFileComponents(src, osPackageEntries);
1386
1451
  if (ownedFileComponents.length) {
@@ -1455,9 +1520,61 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1455
1520
  if (phpBuildInventory.dependencies.length) {
1456
1521
  dependenciesList.push(...phpBuildInventory.dependencies);
1457
1522
  }
1523
+ // Node.js from the Docker Official images, the release tarballs, nvm, fnm or
1524
+ // volta is unpackaged, so its version is only visible in the binary.
1525
+ const nodeBuildInventory = collectNodeBuildInfo(executables, sharedLibs, src);
1526
+ if (nodeBuildInventory.runtimeComponents.length) {
1527
+ pkgList.push(...nodeBuildInventory.runtimeComponents);
1528
+ }
1529
+ if (nodeBuildInventory.dependencies.length) {
1530
+ dependenciesList.push(...nodeBuildInventory.dependencies);
1531
+ }
1532
+ // Ruby from the Docker Official images, ruby-build, rbenv, rvm, asdf, mise
1533
+ // or chruby is unpackaged; its version is in rbconfig.rb and libruby.
1534
+ const rubyBuildInventory = collectRubyBuildInfo(executables, sharedLibs, src);
1535
+ if (rubyBuildInventory.runtimeComponents.length) {
1536
+ pkgList.push(...rubyBuildInventory.runtimeComponents);
1537
+ }
1538
+ if (rubyBuildInventory.dependencies.length) {
1539
+ dependenciesList.push(...rubyBuildInventory.dependencies);
1540
+ }
1541
+ // A Ruby an OS package installed is named by that package; it also gets
1542
+ // its upstream identity, without a CPE, so a BOM names the interpreter both
1543
+ // ways.
1544
+ pkgList.push(...deriveOsPackagedRubyRuntimes(pkgList));
1545
+ // The .NET images and dotnet-install.sh put the runtime under a dotnet root
1546
+ // without a package manager; its deps.json names the runtime pack.
1547
+ const dotnetRuntimes = collectDotnetRuntimes(src, ownedFilePaths);
1548
+ if (dotnetRuntimes.length) {
1549
+ pkgList.push(...dotnetRuntimes);
1550
+ for (const runtime of dotnetRuntimes) {
1551
+ dependenciesList.push({ ref: runtime["bom-ref"], dependsOn: [] });
1552
+ }
1553
+ }
1554
+ // A Brotli decoder compiled into a native file that no package manager owns
1555
+ // (Node.js, Python wheels, PHP and Ruby extensions, the .NET runtime, static
1556
+ // binaries) is only visible by reading the file.
1557
+ const brotliInventory = await collectBrotliDecoders(
1558
+ executables,
1559
+ sharedLibs,
1560
+ src,
1561
+ ownedFilePaths,
1562
+ );
1563
+
1564
+ if (brotliInventory.components.length) {
1565
+ pkgList.push(...brotliInventory.components);
1566
+ }
1567
+ if (brotliInventory.dependencies.length) {
1568
+ dependenciesList.push(...brotliInventory.dependencies);
1569
+ }
1458
1570
  return {
1459
1571
  osPackages: pkgList,
1460
1572
  osPackageFiles,
1573
+ // Extension modules that carry a Brotli decoder. They are reported apart
1574
+ // from sharedLibs so the unpackaged-library count still counts what the
1575
+ // loader's directories hold.
1576
+ nativeExtensionFiles: brotliInventory.extensionFiles,
1577
+ ownerByPath,
1461
1578
  dependenciesList,
1462
1579
  allTypes: Array.from(allTypes).sort(),
1463
1580
  bundledSdks: Array.from(bundledSdks).sort(),
@@ -2696,6 +2813,7 @@ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2696
2813
  const components = [];
2697
2814
  const dependenciesList = [];
2698
2815
  const ownedFilePaths = new Set();
2816
+ const ownerByPath = new Map();
2699
2817
  const services = [];
2700
2818
  const componentByPath = new Map();
2701
2819
  for (const packageEntry of osPackageEntries) {
@@ -2706,6 +2824,10 @@ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2706
2824
  continue;
2707
2825
  }
2708
2826
  ownedFilePaths.add(filePath);
2827
+ const ownerKey = filePath.replaceAll("\\", "/").replace(/^\/+/, "");
2828
+ if (!ownerByPath.has(ownerKey)) {
2829
+ ownerByPath.set(ownerKey, packageEntry.packageRef);
2830
+ }
2709
2831
  let fileComponent = componentByPath.get(filePath);
2710
2832
  if (!fileComponent) {
2711
2833
  fileComponent = await createOSPackageFileComponent(
@@ -2744,6 +2866,7 @@ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2744
2866
  components,
2745
2867
  dependenciesList,
2746
2868
  ownedFilePaths: Array.from(ownedFilePaths).sort(),
2869
+ ownerByPath,
2747
2870
  services: dedupeServices(services),
2748
2871
  };
2749
2872
  }
@@ -3229,7 +3352,10 @@ function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
3229
3352
  bundledRuntimes.add(comp.name);
3230
3353
  }
3231
3354
  // TODO: Need to test this for a range of base images
3232
- if (COMMON_RUNTIMES.includes(comp.name)) {
3355
+ if (
3356
+ COMMON_RUNTIMES.includes(comp.name) ||
3357
+ VERSIONED_RUNTIME_PATTERNS.some((pattern) => pattern.test(comp.name))
3358
+ ) {
3233
3359
  bundledRuntimes.add(comp.name);
3234
3360
  }
3235
3361
  }
@@ -3819,6 +3945,214 @@ function collectPhpBuildInfo(executables, sharedLibs, basePath) {
3819
3945
  };
3820
3946
  }
3821
3947
 
3948
+ /**
3949
+ * Read the Node.js build version of each node-named executable and libnode
3950
+ * shared library, add the cdx:node:* properties, and link the components to
3951
+ * one platform component per distinct version. Mutates the components in
3952
+ * place, as the PHP collector does.
3953
+ *
3954
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3955
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
3956
+ * @param {string} basePath extracted rootfs the paths are relative to
3957
+ * @returns {{runtimeComponents: Object[], dependencies: Object[]}}
3958
+ */
3959
+ function collectNodeBuildInfo(executables, sharedLibs, basePath) {
3960
+ const inventory = createNodeRuntimeInventory();
3961
+ let probes = 0;
3962
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
3963
+ if (probes >= MAX_NODE_PROBES) {
3964
+ break;
3965
+ }
3966
+ const srcFile = component?.properties?.find(
3967
+ (property) => property.name === "internal:SrcFile",
3968
+ )?.value;
3969
+ if (!srcFile || !(isNodeBinaryName(srcFile) || isLibnode(srcFile))) {
3970
+ continue;
3971
+ }
3972
+ if (packageStoreOf(srcFile)) {
3973
+ continue;
3974
+ }
3975
+ probes += 1;
3976
+ const build = readNodeBuildVersion(
3977
+ join(basePath, srcFile.replace(/^\/+/, "")),
3978
+ basePath,
3979
+ );
3980
+ if (build) {
3981
+ attachNodeBuild(inventory, component, build);
3982
+ }
3983
+ }
3984
+ return {
3985
+ runtimeComponents: inventory.runtimeComponents,
3986
+ dependencies: inventory.dependencies,
3987
+ };
3988
+ }
3989
+
3990
+ /**
3991
+ * Read the Ruby build version of each unpackaged ruby executable and libruby
3992
+ * shared library, add the cdx:ruby:* properties, and link the components to
3993
+ * one platform component per distinct version. Mutates the components in
3994
+ * place, as the PHP and Node.js collectors do.
3995
+ *
3996
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3997
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
3998
+ * @param {string} basePath extracted rootfs the paths are relative to
3999
+ * @returns {{runtimeComponents: Object[], dependencies: Object[]}}
4000
+ */
4001
+ function collectRubyBuildInfo(executables, sharedLibs, basePath) {
4002
+ const inventory = createRubyRuntimeInventory();
4003
+ let probes = 0;
4004
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
4005
+ if (probes >= MAX_RUBY_PROBES) {
4006
+ break;
4007
+ }
4008
+ const srcFile = component?.properties?.find(
4009
+ (property) => property.name === "internal:SrcFile",
4010
+ )?.value;
4011
+ if (!srcFile || !(isRubyBinaryName(srcFile) || isLibrubyName(srcFile))) {
4012
+ continue;
4013
+ }
4014
+ if (packageStoreOf(srcFile)) {
4015
+ continue;
4016
+ }
4017
+ probes += 1;
4018
+ const build = readRubyBuildVersion(
4019
+ join(basePath, srcFile.replace(/^\/+/, "")),
4020
+ basePath,
4021
+ );
4022
+ if (build) {
4023
+ attachRubyBuild(inventory, component, build);
4024
+ }
4025
+ }
4026
+ return {
4027
+ runtimeComponents: inventory.runtimeComponents,
4028
+ dependencies: inventory.dependencies,
4029
+ };
4030
+ }
4031
+
4032
+ function isLibnode(srcFile) {
4033
+ const name = basename(srcFile.replaceAll("\\", "/"));
4034
+ return /^libnode\.so(\.\d+)*$/.test(name);
4035
+ }
4036
+
4037
+ /**
4038
+ * Probe unpackaged executables, shared libraries and native extension modules
4039
+ * for a compiled-in Brotli decoder. Executables and libraries already have file
4040
+ * components and are tagged in place; an extension module becomes a file
4041
+ * component only when it carries a decoder, so the BOM does not grow by every
4042
+ * extension in the image. Executables and libraries, and extension modules,
4043
+ * each get MAX_BROTLI_PROBES files, and the scan reads at most
4044
+ * MAX_BROTLI_PROBE_BYTES in all; what a cap leaves unread is logged and
4045
+ * recorded as the binary.brotli-probe.capped degradation.
4046
+ *
4047
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
4048
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
4049
+ * @param {string} basePath extracted rootfs the paths are relative to
4050
+ * @param {string[]} ownedFilePaths root-relative paths owned by OS packages
4051
+ * @returns {Promise<{components: Object[], dependencies: Object[], extensionFiles: Object[]}>}
4052
+ */
4053
+ async function collectBrotliDecoders(
4054
+ executables,
4055
+ sharedLibs,
4056
+ basePath,
4057
+ ownedFilePaths,
4058
+ ) {
4059
+ const inventory = createBrotliInventory();
4060
+ let bytesRead = 0;
4061
+ const unread = { files: 0, extensions: 0 };
4062
+ const probe = (relativePath) => {
4063
+ if (bytesRead >= MAX_BROTLI_PROBE_BYTES) {
4064
+ return undefined;
4065
+ }
4066
+ const decoder = readBrotliDecoder(join(basePath, relativePath), basePath);
4067
+ if (decoder && !decoder.skipped) {
4068
+ bytesRead += decoder.size || 0;
4069
+ }
4070
+ return decoder;
4071
+ };
4072
+ let probes = 0;
4073
+ const seenPaths = new Set();
4074
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
4075
+ const srcFile = component?.properties?.find(
4076
+ (property) => property.name === "internal:SrcFile",
4077
+ )?.value;
4078
+ if (!srcFile) {
4079
+ continue;
4080
+ }
4081
+ const relativePath = srcFile.replaceAll("\\", "/").replace(/^\/+/, "");
4082
+ seenPaths.add(relativePath);
4083
+ if (packageStoreOf(relativePath)) {
4084
+ continue;
4085
+ }
4086
+ if (probes >= MAX_BROTLI_PROBES || bytesRead >= MAX_BROTLI_PROBE_BYTES) {
4087
+ unread.files += 1;
4088
+ continue;
4089
+ }
4090
+ probes += 1;
4091
+ const decoder = probe(relativePath);
4092
+ if (decoder) {
4093
+ attachBrotliDecoder(inventory, component, decoder);
4094
+ }
4095
+ }
4096
+ const extensionHits = [];
4097
+ let extensionProbes = 0;
4098
+ for (const relativePath of collectNativeExtensions(basePath, [
4099
+ ...(ownedFilePaths || []),
4100
+ ...seenPaths,
4101
+ ])) {
4102
+ if (packageStoreOf(relativePath)) {
4103
+ continue;
4104
+ }
4105
+ if (
4106
+ extensionProbes >= MAX_BROTLI_PROBES ||
4107
+ bytesRead >= MAX_BROTLI_PROBE_BYTES
4108
+ ) {
4109
+ unread.extensions += 1;
4110
+ continue;
4111
+ }
4112
+ extensionProbes += 1;
4113
+ const decoder = probe(relativePath);
4114
+ if (decoder?.c || decoder?.go || decoder?.skipped) {
4115
+ extensionHits.push({ relativePath, decoder });
4116
+ }
4117
+ }
4118
+ if (unread.files || unread.extensions) {
4119
+ const detail = `The Brotli decoder probe left ${unread.files} executable(s) or librar(ies) and ${unread.extensions} extension module(s) unread after reaching its file or byte cap, so an embedded decoder in them is not reported.`;
4120
+ thoughtLog(detail);
4121
+ recordDegradation("binary.brotli-probe.capped", {
4122
+ ecosystem: "generic",
4123
+ tool: "brotli-probe",
4124
+ impact: "components",
4125
+ path: basePath,
4126
+ detail,
4127
+ });
4128
+ }
4129
+ const extensionFiles = [];
4130
+ if (extensionHits.length) {
4131
+ const components = await fileComponents(
4132
+ basePath,
4133
+ extensionHits.map((hit) => hit.relativePath),
4134
+ "shared_library",
4135
+ );
4136
+ for (let index = 0; index < components.length; index++) {
4137
+ const component = components[index];
4138
+ // Extension modules often share a basename (brotli.so is both a PHP and
4139
+ // a Ruby extension), so the path keeps each one a component of its own.
4140
+ const purl = tryBuildPurl({
4141
+ type: "generic",
4142
+ name: component.name,
4143
+ subpath: extensionHits[index].relativePath,
4144
+ });
4145
+ if (purl) {
4146
+ component.purl = purl;
4147
+ component["bom-ref"] = purl;
4148
+ }
4149
+ attachBrotliDecoder(inventory, component, extensionHits[index].decoder);
4150
+ extensionFiles.push(component);
4151
+ }
4152
+ }
4153
+ return { ...finishBrotliInventory(inventory), extensionFiles };
4154
+ }
4155
+
3822
4156
  async function fileComponents(basePath, fileList, fileType) {
3823
4157
  const hashResults = await mapWithConcurrency(fileList, (f) =>
3824
4158
  multiChecksumFile(["md5", "sha1"], join(basePath, f))
@@ -10,6 +10,7 @@ import {
10
10
  basename,
11
11
  isAbsolute,
12
12
  join,
13
+ posix,
13
14
  relative,
14
15
  resolve,
15
16
  win32,
@@ -2487,6 +2488,77 @@ export const exportImage = async (fullImageName, options) => {
2487
2488
  return undefined;
2488
2489
  };
2489
2490
 
2491
+ // Directories that hold far more than gems. An image that names one as its gem
2492
+ // directory would turn the gem scan into a scan of the whole tree.
2493
+ const BROAD_IMAGE_DIRS = new Set([
2494
+ "/bin",
2495
+ "/boot",
2496
+ "/dev",
2497
+ "/etc",
2498
+ "/home",
2499
+ "/lib",
2500
+ "/lib64",
2501
+ "/opt",
2502
+ "/proc",
2503
+ "/root",
2504
+ "/run",
2505
+ "/sbin",
2506
+ "/srv",
2507
+ "/sys",
2508
+ "/tmp",
2509
+ "/usr",
2510
+ "/usr/bin",
2511
+ "/usr/lib",
2512
+ "/usr/lib64",
2513
+ "/usr/local",
2514
+ "/usr/local/lib",
2515
+ "/usr/share",
2516
+ "/var",
2517
+ "/var/lib",
2518
+ ]);
2519
+ const MAX_IMAGE_GEM_DIRS = 16;
2520
+
2521
+ /**
2522
+ * The gem directories an image's environment names in GEM_HOME, GEM_PATH and
2523
+ * BUNDLE_PATH. Only absolute paths without `..` segments are kept, so an image
2524
+ * cannot steer the scan outside its own root, and each is normalized; the root
2525
+ * and broad system directories such as /usr or /var are refused, so an image
2526
+ * cannot turn the gem scan into a scan of the whole tree.
2527
+ *
2528
+ * @param {string[]|undefined} env image config Env entries, `NAME=value`
2529
+ * @returns {string[]} absolute in-image directories, deduplicated, at most
2530
+ * MAX_IMAGE_GEM_DIRS
2531
+ */
2532
+ export function imageGemDirs(env) {
2533
+ const dirs = new Set();
2534
+ if (!Array.isArray(env)) {
2535
+ return [];
2536
+ }
2537
+ for (const entry of env) {
2538
+ if (typeof entry !== "string") {
2539
+ continue;
2540
+ }
2541
+ const separator = entry.indexOf("=");
2542
+ if (separator <= 0) {
2543
+ continue;
2544
+ }
2545
+ const name = entry.slice(0, separator);
2546
+ if (!["GEM_HOME", "GEM_PATH", "BUNDLE_PATH"].includes(name)) {
2547
+ continue;
2548
+ }
2549
+ for (const value of entry.slice(separator + 1).split(":")) {
2550
+ if (!value.startsWith("/") || value.split("/").includes("..")) {
2551
+ continue;
2552
+ }
2553
+ const dir = posix.normalize(value).replace(/\/+$/, "");
2554
+ if (dir && !BROAD_IMAGE_DIRS.has(dir)) {
2555
+ dirs.add(dir);
2556
+ }
2557
+ }
2558
+ }
2559
+ return Array.from(dirs).sort().slice(0, MAX_IMAGE_GEM_DIRS);
2560
+ }
2561
+
2490
2562
  /**
2491
2563
  * Method to retrieve path list for system-level packages
2492
2564
  */
@@ -2530,6 +2602,7 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
2530
2602
  if (allLayersExplodedDir && allLayersExplodedDir !== "") {
2531
2603
  knownSysPaths = knownSysPaths.concat([
2532
2604
  join(allLayersExplodedDir, "/usr/local/go"),
2605
+ join(allLayersExplodedDir, "/usr/local/bundle"),
2533
2606
  join(allLayersExplodedDir, "/usr/local/lib"),
2534
2607
  join(allLayersExplodedDir, "/usr/local/lib64"),
2535
2608
  join(allLayersExplodedDir, "/opt"),
@@ -2544,6 +2617,7 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
2544
2617
  } else if (allLayersExplodedDir === "") {
2545
2618
  knownSysPaths = knownSysPaths.concat([
2546
2619
  join(allLayersExplodedDir, "/usr/local/go"),
2620
+ join(allLayersExplodedDir, "/usr/local/bundle"),
2547
2621
  join(allLayersExplodedDir, "/usr/local/lib"),
2548
2622
  join(allLayersExplodedDir, "/usr/local/lib64"),
2549
2623
  join(allLayersExplodedDir, "/opt"),
@@ -2554,6 +2628,11 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
2554
2628
  join(allLayersExplodedDir, "/var/lib"),
2555
2629
  ]);
2556
2630
  }
2631
+ // The official Ruby images install gems into GEM_HOME=/usr/local/bundle, and
2632
+ // an image can point GEM_HOME, GEM_PATH or BUNDLE_PATH anywhere else.
2633
+ for (const gemDir of imageGemDirs(exportData?.inspectData?.Config?.Env)) {
2634
+ knownSysPaths.push(join(allLayersExplodedDir || "", gemDir));
2635
+ }
2557
2636
  if (safeExistsSync(join(allLayersDir, "Files"))) {
2558
2637
  knownSysPaths.push(join(allLayersDir, "Files"));
2559
2638
  }