@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
@@ -6,6 +6,7 @@ import {
6
6
  extname,
7
7
  isAbsolute,
8
8
  join,
9
+ relative,
9
10
  resolve,
10
11
  } from "node:path";
11
12
  import process from "node:process";
@@ -101,11 +102,14 @@ import {
101
102
  addRubyGemsDistributionUrls,
102
103
  addRubyRuntimeComponent,
103
104
  collectGemModuleNames,
105
+ collectProjectRubyDeclarations,
104
106
  describeProjectGem,
105
107
  enrichGemsFromLocalCache,
106
108
  parseBundleConfig,
107
109
  parseGemfileLockData,
108
110
  parseGemspecData,
111
+ resolveProjectRuby,
112
+ toGemPurl,
109
113
  } from "../ecosystems/rubyutils.js";
110
114
  import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
111
115
  import {
@@ -152,7 +156,11 @@ import {
152
156
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
153
157
  import { resolvePluginBinary } from "../inventory/plugins.js";
154
158
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
155
- import { classifyProbeResult } from "../inventory/toolRequirements.js";
159
+ import { isInsideInstalledGem } from "../inventory/rubyBuildInfo.js";
160
+ import {
161
+ classifyProbeResult,
162
+ parseGemfileLockBundlerVersion,
163
+ } from "../inventory/toolRequirements.js";
156
164
  import { getPluginToolComponents } from "../managers/binary.js";
157
165
  import { getTreeWithPlugin } from "../managers/piptree.js";
158
166
  import {
@@ -1429,7 +1437,14 @@ export async function createRubyBom(path, options) {
1429
1437
  if (typeof options.includeNodeModulesDir === "undefined" || options.deep) {
1430
1438
  options.includeNodeModulesDir = true;
1431
1439
  }
1432
- const excludeList = (options.exclude || []).concat(["**/vendor/cache/**"]);
1440
+ // RubyGems caches the specs it fetched from a source under ~/.cache/gem/specs
1441
+ // and ~/.gem/specs, Marshal-dumped into files named <gem>-<version>.gemspec.
1442
+ // Those describe what a source offered, not what is installed.
1443
+ const excludeList = (options.exclude || []).concat([
1444
+ "**/vendor/cache/**",
1445
+ "**/.cache/gem/specs/**",
1446
+ "**/.gem/specs/**",
1447
+ ]);
1433
1448
  const gemLockExcludeList = (options.exclude || []).concat([
1434
1449
  "**/vendor/bundle/ruby/**/Gemfile.lock",
1435
1450
  "**/test/data/**/Gemfile*.lock",
@@ -1455,6 +1470,12 @@ export async function createRubyBom(path, options) {
1455
1470
  exclude: gemLockExcludeList,
1456
1471
  },
1457
1472
  );
1473
+ // An installed gem's own directory can hold its development Gemfile.lock,
1474
+ // nested gemspecs and manifests (rbs ships steep/Gemfile.lock, typeprof a
1475
+ // VS Code extension). They describe how the gem is developed, not what is
1476
+ // installed, and would add gems that are not there.
1477
+ const isGemInternalFile = (f) =>
1478
+ isInsideInstalledGem(f, safeExistsSync, path);
1458
1479
  let gemspecFiles = getAllFiles(
1459
1480
  path,
1460
1481
  `${options.multiProject ? "**/" : ""}*.gemspec`,
@@ -1462,7 +1483,7 @@ export async function createRubyBom(path, options) {
1462
1483
  ...options,
1463
1484
  exclude: excludeList,
1464
1485
  },
1465
- );
1486
+ ).filter((f) => !isGemInternalFile(f));
1466
1487
  let gemHome =
1467
1488
  readEnvironmentVariable("CDXGEN_GEM_HOME") ||
1468
1489
  readEnvironmentVariable("GEM_HOME");
@@ -1543,7 +1564,9 @@ export async function createRubyBom(path, options) {
1543
1564
  ...options,
1544
1565
  exclude: gemLockExcludeList,
1545
1566
  },
1546
- );
1567
+ ).filter((f) => !isGemInternalFile(f));
1568
+ // The lockfiles each Bundler release resolved
1569
+ const bundlerVersions = new Map();
1547
1570
  if (gemLockFiles.length) {
1548
1571
  for (const f of gemLockFiles) {
1549
1572
  if (DEBUG_MODE) {
@@ -1551,6 +1574,15 @@ export async function createRubyBom(path, options) {
1551
1574
  }
1552
1575
  const gemLockData = readFileSync(f, { encoding: "utf-8" });
1553
1576
  const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
1577
+ // Bundler is not listed among the specs, but runs every bundle: the
1578
+ // lockfile names the release that resolved it under BUNDLED WITH.
1579
+ const bundlerVersion = parseGemfileLockBundlerVersion(gemLockData);
1580
+ if (bundlerVersion) {
1581
+ bundlerVersions.set(bundlerVersion, [
1582
+ ...(bundlerVersions.get(bundlerVersion) || []),
1583
+ f,
1584
+ ]);
1585
+ }
1554
1586
  if (retMap.pkgList?.length) {
1555
1587
  pkgList = pkgList.concat(retMap.pkgList);
1556
1588
  pkgList = trimComponents(pkgList);
@@ -1633,6 +1665,42 @@ export async function createRubyBom(path, options) {
1633
1665
  options,
1634
1666
  ));
1635
1667
  }
1668
+ for (const [bundlerVersion, lockFiles] of [...bundlerVersions.entries()].sort(
1669
+ (a, b) => a[0].localeCompare(b[0]),
1670
+ )) {
1671
+ const purl = toGemPurl("bundler", bundlerVersion);
1672
+ const ref = decodeURIComponent(purl);
1673
+ if (pkgList.some((p) => p["bom-ref"] === ref)) {
1674
+ continue;
1675
+ }
1676
+ pkgList.push({
1677
+ name: "bundler",
1678
+ version: bundlerVersion,
1679
+ purl,
1680
+ "bom-ref": ref,
1681
+ scope: "required",
1682
+ properties: [
1683
+ { name: "cdx:gem:bundledWith", value: "true" },
1684
+ { name: "internal:SrcFile", value: lockFiles.join(", ") },
1685
+ ],
1686
+ evidence: {
1687
+ identity: [
1688
+ {
1689
+ field: "purl",
1690
+ confidence: 0.8,
1691
+ methods: [
1692
+ {
1693
+ technique: "manifest-analysis",
1694
+ confidence: 0.8,
1695
+ value: lockFiles.join(", "),
1696
+ },
1697
+ ],
1698
+ },
1699
+ ],
1700
+ },
1701
+ });
1702
+ rootList.push(ref);
1703
+ }
1636
1704
  if (rootList.length) {
1637
1705
  dependencies = mergeDependencies(
1638
1706
  dependencies,
@@ -1737,18 +1805,76 @@ export async function createRubyBom(path, options) {
1737
1805
  pkgList = await enrichGemsFromLocalCache(pkgList, { gemHome });
1738
1806
  // The download URL of a gem from the public registry needs no network call
1739
1807
  pkgList = addRubyGemsDistributionUrls(pkgList);
1740
- // Gems declare the Ruby they run on; surface it as the runtime they depend
1741
- // on. The component says the runtime is external, which only CycloneDX 1.7
1742
- // can express, and a container image ships its own Ruby.
1743
- if (
1744
- options.specVersion >= 1.7 &&
1745
- !hasAnyProjectType(["oci"], options, false)
1746
- ) {
1747
- ({ pkgList, dependencies } = addRubyRuntimeComponent(
1748
- pkgList,
1749
- parentComponent,
1750
- dependencies,
1751
- ));
1808
+ // The project and its gems declare the Ruby they run on; surface it as the
1809
+ // runtime they depend on. A pinned Ruby (Gemfile.lock, .ruby-version,
1810
+ // .tool-versions, mise, the Gemfile) is a version with a CPE, which every
1811
+ // spec version can carry. A bare range needs CycloneDX 1.7's versionRange
1812
+ // and isExternal. A container image ships its own Ruby, which the image
1813
+ // scan reads from the interpreter's files.
1814
+ if (!hasAnyProjectType(["oci"], options, false)) {
1815
+ // Each project of a multi-project scan pins its own Ruby
1816
+ const rootDir = resolve(path);
1817
+ const projectDirs = [rootDir];
1818
+ if (options.multiProject) {
1819
+ for (const f of [...gemLockFiles, ...gemFiles]) {
1820
+ const dir = dirname(resolve(f));
1821
+ if (!projectDirs.includes(dir)) {
1822
+ projectDirs.push(dir);
1823
+ }
1824
+ }
1825
+ }
1826
+ const rubies = [];
1827
+ for (const dir of projectDirs) {
1828
+ const ruby = resolveProjectRuby(collectProjectRubyDeclarations(dir));
1829
+ if (!ruby) {
1830
+ continue;
1831
+ }
1832
+ if (dir !== rootDir && ruby.source) {
1833
+ ruby.source = relative(rootDir, join(dir, ruby.source)).replaceAll(
1834
+ "\\",
1835
+ "/",
1836
+ );
1837
+ }
1838
+ if (ruby.conflicts?.length) {
1839
+ console.log(
1840
+ `NOTE: The project in ${dir} declares more than one Ruby. The BOM records ${ruby.engine} ${ruby.version} from ${ruby.source}; also declared: ${ruby.conflicts.join(", ")}.`,
1841
+ );
1842
+ }
1843
+ rubies.push(ruby);
1844
+ }
1845
+ // One runtime per distinct pinned Ruby; the scanned directory's first
1846
+ const pinned = [];
1847
+ for (const ruby of rubies) {
1848
+ if (
1849
+ ruby.version &&
1850
+ !pinned.some(
1851
+ (other) =>
1852
+ other.engine === ruby.engine && other.version === ruby.version,
1853
+ )
1854
+ ) {
1855
+ pinned.push(ruby);
1856
+ }
1857
+ }
1858
+ const [primary, ...others] = pinned.length ? pinned : rubies.slice(0, 1);
1859
+ if (options.specVersion >= 1.7 || primary?.version) {
1860
+ ({ pkgList, dependencies } = addRubyRuntimeComponent(
1861
+ pkgList,
1862
+ parentComponent,
1863
+ dependencies,
1864
+ primary,
1865
+ ));
1866
+ }
1867
+ // The gems were linked to the first; the others hang off the project
1868
+ for (const ruby of others) {
1869
+ let extra;
1870
+ ({ pkgList: extra, dependencies } = addRubyRuntimeComponent(
1871
+ [],
1872
+ parentComponent,
1873
+ dependencies,
1874
+ ruby,
1875
+ ));
1876
+ pkgList = pkgList.concat(extra);
1877
+ }
1752
1878
  }
1753
1879
  return buildBomNSData(options, pkgList, "gem", {
1754
1880
  src: path,
@@ -45,7 +45,12 @@ import {
45
45
  emptyCppBuildContext,
46
46
  resolveCppBuildContext,
47
47
  } from "../ecosystems/cppBuildContext.js";
48
- import { getCppModules, hasCppSources } from "../ecosystems/cppEvidence.js";
48
+ import {
49
+ getCppModules,
50
+ hasCppSources,
51
+ installedVcpkgPorts,
52
+ parseVcpkgManifest,
53
+ } from "../ecosystems/cppEvidence.js";
49
54
  import {
50
55
  getCratesMetadata,
51
56
  getSwiftPackageMetadata,
@@ -132,6 +137,10 @@ import { resolveZigGraph } from "../ecosystems/zigResolver.js";
132
137
  import { cmakeBuildDirCandidates } from "../inventory/cmakeBuildDirs.js";
133
138
  import { mergeDependencies, trimComponents } from "../inventory/depsUtils.js";
134
139
  import { convertOSQueryResults } from "../inventory/evidenceUtils.js";
140
+ import {
141
+ clojureMirrorArgs,
142
+ leiningenMirrorArgs,
143
+ } from "../inventory/mavenCentralMirror.js";
135
144
  import { applyPurl, nixBomRef, tryBuildPurl } from "../inventory/purl.js";
136
145
  import {
137
146
  classifyProbeResult,
@@ -1806,10 +1815,33 @@ export function createCppBom(path, options) {
1806
1815
  // The need for java >= 21 with atom is causing confusions since there could be C projects
1807
1816
  // inside of other project types. So we currently limit this analyis only when -t argument
1808
1817
  // is used.
1809
- if (
1810
- !hasAnyProjectType(["docker", "oci", "container", "os"], options, false) &&
1811
- (!options.createMultiXBom || options.deep)
1812
- ) {
1818
+ if (!isImageScan && options.createMultiXBom && !options.deep) {
1819
+ // The atom analysis below is skipped in this mode, but a vcpkg manifest is
1820
+ // read without running anything, and its declared ports are as much a
1821
+ // part of the project as CMake's find_package names.
1822
+ const manifest = parseVcpkgManifest(path, {
1823
+ installedPorts: installedVcpkgPorts(path, options),
1824
+ isListed: (name) =>
1825
+ pkgList.some(
1826
+ (p) => !p.group && p.name?.toLowerCase() === `${name}`.toLowerCase(),
1827
+ ),
1828
+ });
1829
+ if (manifest.pkgList.length) {
1830
+ pkgList = pkgList.concat(manifest.pkgList);
1831
+ if (!parentComponent && manifest.parentComponent) {
1832
+ parentComponent = manifest.parentComponent;
1833
+ }
1834
+ if (parentComponent?.["bom-ref"]) {
1835
+ dependencies = mergeDependencies(dependencies, [
1836
+ {
1837
+ ref: parentComponent["bom-ref"],
1838
+ dependsOn: manifest.pkgList.map((p) => p["bom-ref"]),
1839
+ },
1840
+ ]);
1841
+ }
1842
+ }
1843
+ }
1844
+ if (!isImageScan && (!options.createMultiXBom || options.deep)) {
1813
1845
  let osPkgsList = [];
1814
1846
  // Case 1: Development libraries installed in this OS environment might be used for build
1815
1847
  // We collect OS packages with the word dev in the name using osquery here
@@ -1967,6 +1999,10 @@ export function createClojureBom(path, options) {
1967
1999
  if (readEnvironmentVariable("LEIN_ARGS")) {
1968
2000
  LEIN_ARGS = readEnvironmentVariable("LEIN_ARGS").split(" ");
1969
2001
  }
2002
+ const leinProjectContent = safeExistsSync(leinFiles[0])
2003
+ ? readFileSync(leinFiles[0], { encoding: "utf-8" })
2004
+ : undefined;
2005
+ LEIN_ARGS = leiningenMirrorArgs(LEIN_ARGS, leinProjectContent);
1970
2006
  // `lein deps :tree-data` evaluates project.clj, which is a Clojure
1971
2007
  // program, so it belongs to the dependency-tooling surface disabled by
1972
2008
  // --no-install-deps and the pre-build lifecycle.
@@ -2058,6 +2094,7 @@ export function createClojureBom(path, options) {
2058
2094
  if (readEnvironmentVariable("CLJ_ARGS")) {
2059
2095
  CLJ_ARGS = readEnvironmentVariable("CLJ_ARGS").split(" ");
2060
2096
  }
2097
+ CLJ_ARGS = clojureMirrorArgs(CLJ_ARGS);
2061
2098
  for (const f of ednFiles) {
2062
2099
  const basePath = dirname(f);
2063
2100
  console.log("Executing", CLJ_CMD, "in", basePath);
@@ -0,0 +1,344 @@
1
+ // Bounded, read-only access to files inside an extracted image or a live host.
2
+ //
3
+ // Every file a scan opens may be hostile: only regular files that stay inside
4
+ // the extracted root are opened, a symlink is never followed, a FIFO is never
5
+ // opened, and reads are capped. A missing or odd file yields no result rather
6
+ // than throwing.
7
+
8
+ import { createHash } from "node:crypto";
9
+ import {
10
+ closeSync,
11
+ constants,
12
+ fstatSync,
13
+ lstatSync,
14
+ openSync,
15
+ readSync,
16
+ realpathSync,
17
+ } from "node:fs";
18
+ import { resolve, sep } from "node:path";
19
+ import process from "node:process";
20
+
21
+ const READ_CHUNK_BYTES = 1024 * 1024;
22
+
23
+ // O_NOFOLLOW refuses a symlink swapped in after lstat, and O_NONBLOCK keeps a
24
+ // FIFO swapped in the same way from blocking the open. Neither exists on
25
+ // Windows, where neither race applies.
26
+ const OPEN_FLAGS =
27
+ constants.O_RDONLY |
28
+ (constants.O_NOFOLLOW || 0) |
29
+ (constants.O_NONBLOCK || 0);
30
+
31
+ /**
32
+ * The largest file scanFileMarkers reads. Larger files are skipped whole
33
+ * rather than read in part, so a marker past the cap is never mistaken for an
34
+ * absent one; the result says the file was skipped.
35
+ *
36
+ * @type {number}
37
+ */
38
+ export const MAX_MARKER_SCAN_BYTES = 2 * 1024 * 1024 * 1024;
39
+
40
+ /**
41
+ * Open a path only when it is a regular file that does not resolve outside
42
+ * rootDir. lstat runs first so a FIFO is rejected without opening it (an open
43
+ * on a FIFO blocks until a writer appears) and a symlink is never followed.
44
+ *
45
+ * @param {string} absPath absolute path
46
+ * @param {string|undefined} rootDir extracted root the path must stay under
47
+ * @returns {{fd: number, size: number}|undefined} an open descriptor the
48
+ * caller closes, or undefined
49
+ */
50
+ export function openRegularFile(absPath, rootDir) {
51
+ let fd;
52
+ try {
53
+ if (typeof absPath !== "string" || !absPath) {
54
+ return undefined;
55
+ }
56
+ const lst = lstatSync(absPath);
57
+ if (!lst.isFile()) {
58
+ return undefined;
59
+ }
60
+ if (rootDir) {
61
+ const root = realpathSync(resolve(rootDir));
62
+ const real = realpathSync(absPath);
63
+ const rootPrefix = root.endsWith(sep) ? root : root + sep;
64
+ if (real !== root && !real.startsWith(rootPrefix)) {
65
+ return undefined;
66
+ }
67
+ }
68
+ fd = openSync(absPath, OPEN_FLAGS);
69
+ const stats = fstatSync(fd);
70
+ if (!stats.isFile()) {
71
+ closeSync(fd);
72
+ return undefined;
73
+ }
74
+ return { fd, size: stats.size };
75
+ } catch {
76
+ if (fd !== undefined) {
77
+ try {
78
+ closeSync(fd);
79
+ } catch {
80
+ // already closed
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Read a small text file in full, only when it is a regular file no larger
89
+ * than maxBytes. A FIFO, a directory, a symlink or an oversized file reads as
90
+ * undefined.
91
+ *
92
+ * @param {string} absPath absolute path
93
+ * @param {number} maxBytes largest size read
94
+ * @param {string|undefined} [rootDir] root the path must stay under
95
+ * @returns {string|undefined} the text, or undefined
96
+ */
97
+ export function readRegularTextFile(absPath, maxBytes, rootDir) {
98
+ const opened = openRegularFile(absPath, rootDir);
99
+ if (!opened) {
100
+ return undefined;
101
+ }
102
+ try {
103
+ if (opened.size > maxBytes) {
104
+ return undefined;
105
+ }
106
+ const buffer = Buffer.alloc(opened.size);
107
+ let total = 0;
108
+ while (total < opened.size) {
109
+ const read = readSync(
110
+ opened.fd,
111
+ buffer,
112
+ total,
113
+ opened.size - total,
114
+ total,
115
+ );
116
+ if (read <= 0) {
117
+ break;
118
+ }
119
+ total += read;
120
+ }
121
+ return buffer.toString("utf8", 0, total);
122
+ } catch {
123
+ return undefined;
124
+ } finally {
125
+ try {
126
+ closeSync(opened.fd);
127
+ } catch {
128
+ // already closed
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Find byte markers in a file with bounded chunked reads.
135
+ *
136
+ * Each hit records up to `beforeBytes` bytes before the marker and up to
137
+ * `contextBytes` bytes after it, so a caller can read a version that follows a
138
+ * marker or check what a string is part of. Consecutive reads overlap by the
139
+ * longest marker plus the context length, so a marker split across two reads
140
+ * is seen whole once. Only bytes that were read are searched.
141
+ *
142
+ * @param {string} absPath absolute path of the file
143
+ * @param {string|undefined} rootDir extracted root the file must stay under
144
+ * @param {Buffer[]} markers byte markers to look for
145
+ * @param {Object} [options] options
146
+ * @param {number} [options.maxBytes] files larger than this are not read
147
+ * @param {number} [options.contextBytes=64] bytes kept after each hit
148
+ * @param {number} [options.beforeBytes=1] bytes kept before each hit
149
+ * @param {number} [options.maxHits=16] hits kept per marker
150
+ * @returns {{hits: {before: Buffer, after: Buffer, offset: number}[][], size: number, skipped?: string}|undefined}
151
+ * hits per marker, in marker order, and the file size; `skipped` is
152
+ * `too-large` when the file is over the cap and was not read. Undefined when
153
+ * the file cannot be opened.
154
+ */
155
+ export function scanFileMarkers(
156
+ absPath,
157
+ rootDir,
158
+ markers,
159
+ {
160
+ maxBytes = MAX_MARKER_SCAN_BYTES,
161
+ contextBytes = 64,
162
+ beforeBytes = 1,
163
+ maxHits = 16,
164
+ } = {},
165
+ ) {
166
+ if (!Array.isArray(markers) || !markers.length) {
167
+ return undefined;
168
+ }
169
+ const opened = openRegularFile(absPath, rootDir);
170
+ if (!opened) {
171
+ return undefined;
172
+ }
173
+ const hits = markers.map(() => []);
174
+ try {
175
+ const { fd, size } = opened;
176
+ if (size > maxBytes) {
177
+ return { hits, size, skipped: "too-large" };
178
+ }
179
+ if (size <= 0) {
180
+ return { hits, size };
181
+ }
182
+ const longest = Math.max(...markers.map((marker) => marker.length));
183
+ const overlap = longest + contextBytes;
184
+ const bufferSize = Math.min(size, READ_CHUNK_BYTES) + overlap;
185
+ const buffer = Buffer.allocUnsafe(bufferSize);
186
+ // The bytes just before the current window, for a hit near its start.
187
+ let previousTail = Buffer.alloc(0);
188
+ for (let position = 0; position < size; position += READ_CHUNK_BYTES) {
189
+ const want = Math.min(bufferSize, size - position);
190
+ const read = readSync(fd, buffer, 0, want, position);
191
+ if (read <= 0) {
192
+ break;
193
+ }
194
+ // What a previous, longer read left in the buffer past `read` is never
195
+ // searched.
196
+ const window = buffer.subarray(0, read);
197
+ // Only hits starting inside this window are new; the overlap lets the
198
+ // next window see a marker and its context that cross the edge. The
199
+ // last window has no next one, so everything it read is searched.
200
+ const isLast = position + read >= size;
201
+ const searchLimit = isLast ? read : Math.min(READ_CHUNK_BYTES, read);
202
+ for (let index = 0; index < markers.length; index++) {
203
+ const marker = markers[index];
204
+ const found = hits[index];
205
+ let offset = window.indexOf(marker);
206
+ while (
207
+ offset !== -1 &&
208
+ offset < searchLimit &&
209
+ found.length < maxHits
210
+ ) {
211
+ const afterStart = offset + marker.length;
212
+ const before =
213
+ offset >= beforeBytes
214
+ ? window.subarray(offset - beforeBytes, offset)
215
+ : Buffer.concat([
216
+ previousTail,
217
+ window.subarray(0, offset),
218
+ ]).subarray(-beforeBytes);
219
+ found.push({
220
+ offset: position + offset,
221
+ before: Buffer.from(before),
222
+ after: Buffer.from(
223
+ window.subarray(
224
+ Math.min(afterStart, read),
225
+ Math.min(read, afterStart + contextBytes),
226
+ ),
227
+ ),
228
+ });
229
+ offset = window.indexOf(marker, offset + 1);
230
+ }
231
+ }
232
+ if (read < bufferSize) {
233
+ break;
234
+ }
235
+ previousTail = Buffer.from(
236
+ window.subarray(
237
+ Math.max(0, READ_CHUNK_BYTES - beforeBytes),
238
+ READ_CHUNK_BYTES,
239
+ ),
240
+ );
241
+ }
242
+ return { hits, size };
243
+ } catch {
244
+ return undefined;
245
+ } finally {
246
+ try {
247
+ closeSync(opened.fd);
248
+ } catch {
249
+ // already closed
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Compute the digest of a regular file with bounded chunked reads, under the
256
+ * same rules as readRegularTextFile: a FIFO, a directory, a symlink, a file
257
+ * that resolves outside rootDir or one larger than maxBytes yields undefined.
258
+ *
259
+ * @param {string} absPath absolute path
260
+ * @param {number} maxBytes largest size hashed
261
+ * @param {string|undefined} [rootDir] root the path must stay under
262
+ * @param {string} [algorithm="sha256"] node:crypto hash algorithm
263
+ * @returns {string|undefined} the hex digest, or undefined
264
+ */
265
+ export function hashRegularFile(
266
+ absPath,
267
+ maxBytes,
268
+ rootDir,
269
+ algorithm = "sha256",
270
+ ) {
271
+ const opened = openRegularFile(absPath, rootDir);
272
+ if (!opened) {
273
+ return undefined;
274
+ }
275
+ try {
276
+ if (opened.size > maxBytes) {
277
+ return undefined;
278
+ }
279
+ const hash = createHash(algorithm);
280
+ const buffer = Buffer.alloc(Math.min(READ_CHUNK_BYTES, opened.size || 1));
281
+ let total = 0;
282
+ while (total < opened.size) {
283
+ const read = readSync(
284
+ opened.fd,
285
+ buffer,
286
+ 0,
287
+ Math.min(buffer.length, opened.size - total),
288
+ total,
289
+ );
290
+ if (read <= 0) {
291
+ break;
292
+ }
293
+ hash.update(buffer.subarray(0, read));
294
+ total += read;
295
+ }
296
+ return hash.digest("hex");
297
+ } catch {
298
+ return undefined;
299
+ } finally {
300
+ try {
301
+ closeSync(opened.fd);
302
+ } catch {
303
+ // already closed
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * True when the process runs as root, where a symlink planted in a user's
310
+ * home could point a scan at files only root can read.
311
+ *
312
+ * @returns {boolean} true for uid 0
313
+ */
314
+ export function isRunningAsRoot() {
315
+ return typeof process.getuid === "function" && process.getuid() === 0;
316
+ }
317
+
318
+ /**
319
+ * Read a small text file a user keeps in their home, such as an editor's or an
320
+ * agent's configuration. Dotfile managers often make these symlinks, so a
321
+ * symlink to a regular file is followed, except when the process runs as root.
322
+ * The target must still be a regular file no larger than maxBytes.
323
+ *
324
+ * @param {string} absPath absolute path
325
+ * @param {number} maxBytes largest size read
326
+ * @returns {string|undefined} the text, or undefined
327
+ */
328
+ export function readUserFileText(absPath, maxBytes) {
329
+ if (typeof absPath !== "string" || !absPath) {
330
+ return undefined;
331
+ }
332
+ let target = absPath;
333
+ try {
334
+ if (lstatSync(absPath).isSymbolicLink()) {
335
+ if (isRunningAsRoot()) {
336
+ return undefined;
337
+ }
338
+ target = realpathSync(absPath);
339
+ }
340
+ } catch {
341
+ return undefined;
342
+ }
343
+ return readRegularTextFile(target, maxBytes);
344
+ }