@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.
- package/README.md +6 -6
- package/bin/cdxgen.js +4 -0
- package/bin/evinse.js +3 -0
- package/bin/validate.js +6 -0
- package/data/license-aliases.json +24 -2
- package/data/remediations.json +124 -0
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/vscode-extensions.yaml +75 -0
- package/data/spdx-license-list.json +121 -7
- package/data/spdx-licenses.json +69 -0
- package/data/spdx.schema.json +845 -2
- package/lib/cli/bomAssembly.js +92 -3
- package/lib/cli/index.js +108 -1
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +237 -46
- package/lib/cli/managedBom.js +142 -16
- package/lib/cli/nativeBom.js +42 -5
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +8 -0
- package/lib/core/fs.js +12 -0
- package/lib/core/httpClient.js +98 -1
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -0
- package/lib/core/toolRegistry.js +170 -0
- package/lib/ecosystems/cppEvidence.js +263 -106
- package/lib/ecosystems/cppIncludeResolver.js +16 -5
- package/lib/ecosystems/ecosystems.js +25 -10
- package/lib/ecosystems/gemHostProbe.js +301 -0
- package/lib/ecosystems/gradleutils.js +828 -224
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-misc.js +209 -20
- package/lib/ecosystems/parsers-python.js +89 -2
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +538 -31
- package/lib/ecosystems/scalacliutils.js +2 -0
- package/lib/ecosystems/scalasem.js +796 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +346 -43
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/atomUtils.js +77 -0
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/deps.js +35 -0
- package/lib/inventory/dosai.js +139 -5
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/envcontext.js +307 -13
- package/lib/inventory/evidenceUtils.js +11 -0
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +31 -3
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/jvmLocalRepos.js +9 -2
- package/lib/inventory/jvmToolEnv.js +29 -0
- package/lib/inventory/licenseEnhancer.js +12 -0
- package/lib/inventory/mavenCentralMirror.js +534 -0
- package/lib/inventory/nodeBuildInfo.js +218 -0
- package/lib/inventory/nodeHostProbe.js +202 -0
- package/lib/inventory/osPackageResolver.js +182 -0
- package/lib/inventory/phpBuildInfo.js +10 -59
- package/lib/inventory/phpHostProbe.js +32 -488
- package/lib/inventory/rubyBuildInfo.js +972 -0
- package/lib/inventory/rubyHostProbe.js +218 -0
- package/lib/inventory/scalaCoords.js +4 -3
- package/lib/inventory/scalaDedupe.js +440 -0
- package/lib/inventory/source.js +49 -2
- package/lib/managers/binary.js +337 -3
- package/lib/managers/docker.js +79 -0
- package/lib/parsers/largeJson.js +28 -11
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +12 -1
- package/lib/stages/postgen/ruleEngine.js +13 -5
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +114 -57
- package/lib/validator/bomValidator.js +6 -1
- package/lib/validator/generated/validate-1.6.mjs +1 -1
- package/lib/validator/generated/validate-1.7.mjs +1 -1
- package/lib/validator/generated/validate-2.0.mjs +1 -1
- package/lib/validator/index.js +1 -1
- package/package.json +3 -2
- package/types/lib/cli/bomAssembly.d.ts +13 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +1 -17
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +70 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -0
- package/types/lib/core/brotliProbe.d.ts +37 -0
- package/types/lib/core/brotliProbe.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +4 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +24 -0
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/lockPaths.d.ts +19 -0
- package/types/lib/core/lockPaths.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +2 -0
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts +36 -0
- package/types/lib/core/toolRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
- package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
- package/types/lib/ecosystems/gradleutils.d.ts +47 -13
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +10 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +20 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +144 -12
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
- package/types/lib/ecosystems/venvProbe.d.ts +40 -0
- package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +19 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/brotliDecoder.d.ts +58 -0
- package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +17 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +52 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/gtfobins.d.ts.map +1 -1
- package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
- package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
- package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
- package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
- package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
- package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +38 -0
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
- package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
- package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +10 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +12 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/largeJson.d.ts +4 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +12 -8
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
package/lib/cli/managedBom.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
1741
|
-
//
|
|
1742
|
-
//
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
) {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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,
|
package/lib/cli/nativeBom.js
CHANGED
|
@@ -45,7 +45,12 @@ import {
|
|
|
45
45
|
emptyCppBuildContext,
|
|
46
46
|
resolveCppBuildContext,
|
|
47
47
|
} from "../ecosystems/cppBuildContext.js";
|
|
48
|
-
import {
|
|
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
|
-
|
|
1811
|
-
|
|
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
|
+
}
|