@cdxgen/cdxgen 13.4.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 +9 -9
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +29 -5
- package/bin/evinse.js +8 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/bin/validate.js +6 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/license-aliases.json +24 -2
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +407 -1
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/cbom-security.yaml +235 -4
- package/data/rules/go-binaries.yaml +46 -0
- package/data/rules/golem-go.yaml +80 -0
- package/data/rules/hbom-compliance.yaml +40 -22
- package/data/rules/hbom-performance.yaml +7 -1
- package/data/rules/host-topology.yaml +1 -1
- package/data/rules/obom-runtime.yaml +31 -2
- package/data/rules/php-runtime.yaml +143 -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/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +120 -6
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +182 -6
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +363 -66
- package/lib/cli/managedBom.js +853 -87
- package/lib/cli/nativeBom.js +267 -33
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +38 -8
- package/lib/core/fs.js +40 -19
- package/lib/core/httpClient.js +172 -46
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -0
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -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/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +858 -244
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +438 -64
- package/lib/ecosystems/parsers-python.js +100 -12
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +544 -34
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +4 -1
- package/lib/ecosystems/scalasem.js +798 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +472 -61
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +110 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +41 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +639 -63
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +475 -18
- package/lib/inventory/evidenceUtils.js +447 -23
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +32 -3
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/hostTopology.js +21 -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/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +271 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +438 -0
- package/lib/inventory/phpHostProbe.js +185 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- 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 +256 -5
- package/lib/inventory/spdx.js +144 -0
- package/lib/inventory/traceRunner.js +5 -1
- package/lib/inventory/tracedCrypto.js +129 -0
- package/lib/managers/binary.js +866 -186
- package/lib/managers/docker.js +245 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +62 -13
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +198 -2
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +5 -5
- package/lib/stages/postgen/ruleEngine.js +71 -6
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +231 -62
- package/lib/validator/bomValidator.js +32 -6
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +27 -8
- package/lib/validator/generated/validate-1.7.mjs +32 -13
- package/lib/validator/generated/validate-2.0.mjs +31 -12
- package/lib/validator/index.js +1 -1
- package/package.json +19 -18
- package/types/bin/cdxgen-secure.d.ts +3 -0
- package/types/bin/cdxgen-secure.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +22 -3
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +43 -0
- package/types/lib/cli/cliOptions.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 +20 -0
- 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 +22 -2
- 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/projectConfig.d.ts.map +1 -1
- package/types/lib/core/projectTooling.d.ts +41 -0
- package/types/lib/core/projectTooling.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +26 -0
- package/types/lib/core/propertySanitizer.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/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +57 -21
- 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-dotnet.d.ts +40 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +85 -13
- package/types/lib/ecosystems/parsers-go.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-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +14 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +19 -1
- 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/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.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 +88 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +10 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
- package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +33 -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/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/certificates.d.ts +205 -0
- package/types/lib/inventory/certificates.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +2 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts +18 -0
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +162 -7
- 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/dynamic.d.ts +6 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +92 -8
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +29 -10
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/goBuildInfo.d.ts +10 -0
- package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/gtfobins.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
- package/types/lib/inventory/hbomAnalysis.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/hostTopology.d.ts.map +1 -1
- 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/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +44 -2
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/osinfo.d.ts +31 -6
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/osqueryTransform.d.ts +51 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/phpHostProbe.d.ts +35 -0
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rootfsCerts.d.ts +27 -0
- package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
- 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 +38 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +15 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +4 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +22 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +10 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +24 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/email.d.ts +19 -0
- package/types/lib/parsers/email.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +17 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +8 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +26 -9
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Bounded probe for a Brotli decoder compiled into a native file, and for
|
|
2
|
+
// whether that copy carries the CVE-2026-111497 fix.
|
|
3
|
+
//
|
|
4
|
+
// Node.js, the Python Brotli and brotlicffi wheels, the PHP and Ruby
|
|
5
|
+
// extensions, the .NET runtime and many static binaries carry a private copy of
|
|
6
|
+
// Google's C decoder. No package manager names that copy, and its version is
|
|
7
|
+
// not stored as a string anywhere in the file, so a BOM has to read the file
|
|
8
|
+
// to say whether a vulnerable decoder ships in it.
|
|
9
|
+
//
|
|
10
|
+
// Three kinds of evidence are read, each classified by the bytes around it:
|
|
11
|
+
//
|
|
12
|
+
// - The names of the decoder's error codes, which BrotliDecoderErrorString()
|
|
13
|
+
// returns (EXUBERANT_NIBBLE is one). Upstream 1.1+ stores them prefixed,
|
|
14
|
+
// `_ERROR_FORMAT_EXUBERANT_NIBBLE`; 1.0.x and Debian's backport store the
|
|
15
|
+
// bare name between NULs. The fix, google/brotli 0ed4268, added the name
|
|
16
|
+
// BLOCK_SWITCH, so a decoder whose table has it is patched.
|
|
17
|
+
// https://github.com/google/brotli/blob/v1.2.0/c/include/brotli/decode.h#L72
|
|
18
|
+
// https://github.com/google/brotli/commit/0ed4268a4401abcb4472096340477cf4af7d1a2b
|
|
19
|
+
// - Two decoder tables, kCodeLengthCodeOrder and kCodeLengthPrefixValue
|
|
20
|
+
// (c/dec/decode.c). They stay when a linker drops the error-name table
|
|
21
|
+
// because nothing calls BrotliDecoderErrorString (--gc-sections, LTO,
|
|
22
|
+
// -dead_strip). The encoder keeps the first table but not the second. With
|
|
23
|
+
// the tables alone the decoder is present but its fix state is unknown.
|
|
24
|
+
// - Node.js's zlib.constants table, BROTLI_DECODER_ERROR_FORMAT_*. It mirrors
|
|
25
|
+
// the brotli headers Node.js was built with, and a linker that merges string
|
|
26
|
+
// tails (lld -O2, lld-link /OPT:ICF) can leave only it in place of the
|
|
27
|
+
// decoder's own names. Next to the decoder tables it stands for them; alone,
|
|
28
|
+
// as in a Node.js linked to a shared libbrotlidec, it is no decoder.
|
|
29
|
+
//
|
|
30
|
+
// The ports are told apart. The Go port (andybalholm/brotli, fixed in v1.2.7)
|
|
31
|
+
// keeps the same names in Go's undelimited string table, so a Go binary can
|
|
32
|
+
// hold both a cgo-linked C decoder and the Go port, and each gets its own
|
|
33
|
+
// fix state. The Rust port (brotli-decompressor) keeps `ERROR_FORMAT_...`
|
|
34
|
+
// strings and the same tables but is not affected; its crate name in symbols
|
|
35
|
+
// or panic paths marks it, and its evidence is ignored.
|
|
36
|
+
//
|
|
37
|
+
// Nothing is executed. A file over the read cap is reported as skipped, never
|
|
38
|
+
// as free of a decoder.
|
|
39
|
+
//
|
|
40
|
+
// The component helpers that put the answer into a BOM are in
|
|
41
|
+
// lib/inventory/brotliDecoder.js; this half lives in the core layer so the HTTP
|
|
42
|
+
// client can ask it about its own runtime.
|
|
43
|
+
|
|
44
|
+
import { scanFileMarkers } from "./boundedFile.js";
|
|
45
|
+
|
|
46
|
+
const NAME = Buffer.from("EXUBERANT_NIBBLE");
|
|
47
|
+
const FIX_NAME = Buffer.from("BLOCK_SWITCH");
|
|
48
|
+
// The Go linker writes this magic at the start of the buildinfo section.
|
|
49
|
+
// https://github.com/golang/go/blob/go1.25.0/src/debug/buildinfo/buildinfo.go#L58
|
|
50
|
+
const GO_BUILDINFO_MAGIC = Buffer.from("\xff Go buildinf:", "latin1");
|
|
51
|
+
// static const uint8_t kCodeLengthCodeOrder[18] and
|
|
52
|
+
// kCodeLengthPrefixValue[16], c/dec/decode.c.
|
|
53
|
+
const CODE_LENGTH_CODE_ORDER = Buffer.from([
|
|
54
|
+
1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
|
55
|
+
]);
|
|
56
|
+
const CODE_LENGTH_PREFIX_VALUE = Buffer.from([
|
|
57
|
+
0, 4, 3, 2, 0, 4, 3, 1, 0, 4, 3, 2, 0, 4, 3, 5,
|
|
58
|
+
]);
|
|
59
|
+
// Legacy and v0 Rust mangling length-prefix the crate name; panic messages
|
|
60
|
+
// name the crate's source path.
|
|
61
|
+
const RUST_CRATE_SYMBOL = Buffer.from("19brotli_decompressor");
|
|
62
|
+
const RUST_CRATE_PATH = Buffer.from("brotli-decompressor-");
|
|
63
|
+
|
|
64
|
+
const CONSTANT_PREFIX = Buffer.from("BROTLI_DECODER_ERROR_FORMAT_");
|
|
65
|
+
const C_PREFIX = Buffer.from("_ERROR_FORMAT_");
|
|
66
|
+
const RUST_PREFIX = Buffer.from("ERROR_FORMAT_");
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The most files of one kind (executables and libraries, or extension
|
|
70
|
+
* modules) one scan opens in search of a Brotli decoder.
|
|
71
|
+
*
|
|
72
|
+
* @type {number}
|
|
73
|
+
*/
|
|
74
|
+
export const MAX_BROTLI_PROBES = 4096;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The most bytes one scan reads in search of Brotli decoders.
|
|
78
|
+
*
|
|
79
|
+
* @type {number}
|
|
80
|
+
*/
|
|
81
|
+
export const MAX_BROTLI_PROBE_BYTES = 16 * 1024 * 1024 * 1024;
|
|
82
|
+
|
|
83
|
+
function endsWith(buffer, suffix) {
|
|
84
|
+
return (
|
|
85
|
+
buffer.length >= suffix.length &&
|
|
86
|
+
buffer.subarray(buffer.length - suffix.length).equals(suffix)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Classify one hit of an error name by what it is part of.
|
|
92
|
+
*
|
|
93
|
+
* @param {{before: Buffer, after: Buffer}} hit marker hit
|
|
94
|
+
* @returns {string} `constant`, `c`, `rust` or `other`
|
|
95
|
+
*/
|
|
96
|
+
function classify(hit) {
|
|
97
|
+
if (endsWith(hit.before, CONSTANT_PREFIX)) {
|
|
98
|
+
return "constant";
|
|
99
|
+
}
|
|
100
|
+
if (endsWith(hit.before, C_PREFIX)) {
|
|
101
|
+
return "c";
|
|
102
|
+
}
|
|
103
|
+
if (endsWith(hit.before, RUST_PREFIX)) {
|
|
104
|
+
return "rust";
|
|
105
|
+
}
|
|
106
|
+
const previous = hit.before.length ? hit.before[hit.before.length - 1] : 0;
|
|
107
|
+
if (previous === 0 && hit.after.length && hit.after[0] === 0) {
|
|
108
|
+
return "c";
|
|
109
|
+
}
|
|
110
|
+
return "other";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read which Brotli decoders a native file embeds and whether each carries
|
|
115
|
+
* the CVE-2026-111497 fix.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} absPath absolute path of the file
|
|
118
|
+
* @param {string|undefined} rootDir extracted root the file must stay under
|
|
119
|
+
* @returns {{c?: {blockSwitchFix: string, evidence: string}, go?: {blockSwitchFix: string}, skipped?: string, size: number}|undefined}
|
|
120
|
+
* undefined when the file cannot be read or holds no decoder. `c` is
|
|
121
|
+
* Google's C decoder, with blockSwitchFix `true`, `false` or `unknown` and
|
|
122
|
+
* the evidence it rests on (`error-names`, `constant-names` or `tables`);
|
|
123
|
+
* `go` is the Go port. `skipped` is `too-large` for a file over the read cap.
|
|
124
|
+
*/
|
|
125
|
+
export function readBrotliDecoder(absPath, rootDir) {
|
|
126
|
+
const scan = scanFileMarkers(
|
|
127
|
+
absPath,
|
|
128
|
+
rootDir,
|
|
129
|
+
[
|
|
130
|
+
NAME,
|
|
131
|
+
FIX_NAME,
|
|
132
|
+
GO_BUILDINFO_MAGIC,
|
|
133
|
+
CODE_LENGTH_CODE_ORDER,
|
|
134
|
+
CODE_LENGTH_PREFIX_VALUE,
|
|
135
|
+
RUST_CRATE_SYMBOL,
|
|
136
|
+
RUST_CRATE_PATH,
|
|
137
|
+
],
|
|
138
|
+
{ contextBytes: 1, beforeBytes: CONSTANT_PREFIX.length, maxHits: 256 },
|
|
139
|
+
);
|
|
140
|
+
if (!scan) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (scan.skipped) {
|
|
144
|
+
return { skipped: scan.skipped, size: scan.size };
|
|
145
|
+
}
|
|
146
|
+
const [
|
|
147
|
+
names,
|
|
148
|
+
fixNames,
|
|
149
|
+
goMagic,
|
|
150
|
+
codeOrder,
|
|
151
|
+
prefixValue,
|
|
152
|
+
rustSymbol,
|
|
153
|
+
rustPath,
|
|
154
|
+
] = scan.hits;
|
|
155
|
+
const kinds = (hits) => new Set(hits.map(classify));
|
|
156
|
+
const nameKinds = kinds(names);
|
|
157
|
+
const fixKinds = kinds(fixNames);
|
|
158
|
+
const isGo = goMagic.length > 0;
|
|
159
|
+
const isRust = rustSymbol.length > 0 || rustPath.length > 0;
|
|
160
|
+
const hasTables = codeOrder.length > 0 && prefixValue.length > 0;
|
|
161
|
+
const result = { size: scan.size };
|
|
162
|
+
// Go strings are not delimited, so a name that is neither C-shaped nor a
|
|
163
|
+
// constant is the Go port's, in a Go binary.
|
|
164
|
+
const hasGoPort = isGo && nameKinds.has("other");
|
|
165
|
+
if (hasGoPort) {
|
|
166
|
+
result.go = { blockSwitchFix: fixKinds.has("other") ? "true" : "false" };
|
|
167
|
+
}
|
|
168
|
+
if (nameKinds.has("c")) {
|
|
169
|
+
result.c = {
|
|
170
|
+
blockSwitchFix: fixKinds.has("c") ? "true" : "false",
|
|
171
|
+
evidence: "error-names",
|
|
172
|
+
};
|
|
173
|
+
} else if (hasTables && !hasGoPort && !isRust) {
|
|
174
|
+
result.c = nameKinds.has("constant")
|
|
175
|
+
? {
|
|
176
|
+
blockSwitchFix: fixKinds.has("constant") ? "true" : "false",
|
|
177
|
+
evidence: "constant-names",
|
|
178
|
+
}
|
|
179
|
+
: { blockSwitchFix: "unknown", evidence: "tables" };
|
|
180
|
+
}
|
|
181
|
+
return result.c || result.go ? result : undefined;
|
|
182
|
+
}
|
package/lib/core/env.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, win32 } from "node:path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildReadCountSuffix,
|
|
@@ -301,6 +301,20 @@ export const SWIFT_CMD = readEnvironmentVariable("SWIFT_CMD") || "swift";
|
|
|
301
301
|
/** Resolved Ruby executable command (RUBY_CMD env var, or "ruby"). */
|
|
302
302
|
export const RUBY_CMD = readEnvironmentVariable("RUBY_CMD") || "ruby";
|
|
303
303
|
|
|
304
|
+
/** Resolved RubyGems executable command (CDXGEN_GEM_CMD env var, or "gem"). */
|
|
305
|
+
export const GEM_CMD = readEnvironmentVariable("CDXGEN_GEM_CMD") || "gem";
|
|
306
|
+
|
|
307
|
+
/** Resolved Bundler executable command (CDXGEN_BUNDLE_CMD env var, or "bundle"). */
|
|
308
|
+
export const BUNDLE_CMD =
|
|
309
|
+
readEnvironmentVariable("CDXGEN_BUNDLE_CMD") || "bundle";
|
|
310
|
+
|
|
311
|
+
/** Resolved PHP executable command (PHP_CMD env var, or "php"). */
|
|
312
|
+
export const PHP_CMD = readEnvironmentVariable("PHP_CMD") || "php";
|
|
313
|
+
|
|
314
|
+
/** Resolved Composer executable command (COMPOSER_CMD env var, or "composer"). */
|
|
315
|
+
export const COMPOSER_CMD =
|
|
316
|
+
readEnvironmentVariable("COMPOSER_CMD") || "composer";
|
|
317
|
+
|
|
304
318
|
/** Python package components that can be excluded from the generated BOM. */
|
|
305
319
|
export const PYTHON_EXCLUDED_COMPONENTS = [
|
|
306
320
|
"pip",
|
|
@@ -513,6 +527,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
513
527
|
"vscode",
|
|
514
528
|
"openvsx",
|
|
515
529
|
"vscode-extensions",
|
|
530
|
+
"ide",
|
|
516
531
|
"ide-extension",
|
|
517
532
|
"ide-extensions",
|
|
518
533
|
],
|
|
@@ -707,23 +722,38 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
|
|
|
707
722
|
return shouldInclude;
|
|
708
723
|
}
|
|
709
724
|
|
|
725
|
+
/**
|
|
726
|
+
* Extract the lowercase command name from an invoked command path, without
|
|
727
|
+
* directories or a trailing `.js`, `.cjs`, `.mjs` or `.exe` extension. Both
|
|
728
|
+
* `/` and `\\` separators are handled, so a Windows path is inspected the
|
|
729
|
+
* same way on any host.
|
|
730
|
+
*
|
|
731
|
+
* @param {string} [commandPath] Invoked command path or name
|
|
732
|
+
* @returns {string} Lowercase command name
|
|
733
|
+
*/
|
|
734
|
+
export function getCommandName(commandPath) {
|
|
735
|
+
return win32
|
|
736
|
+
.basename(`${commandPath || ""}`)
|
|
737
|
+
.toLowerCase()
|
|
738
|
+
.replace(/\.(?:[cm]?js|exe)$/u, "");
|
|
739
|
+
}
|
|
740
|
+
|
|
710
741
|
/**
|
|
711
742
|
* Determine whether the predictive dependency audit should run for the current
|
|
712
743
|
* CLI invocation.
|
|
713
744
|
*
|
|
714
|
-
* OBOM
|
|
715
|
-
*
|
|
745
|
+
* OBOM and HBOM runs (commands named `obom` or `hbom`, including `hbom-slim`)
|
|
746
|
+
* and explicit `-t os` / OS / hardware project types alone keep the direct BOM
|
|
747
|
+
* audit findings but skip the predictive dependency audit. Only the command
|
|
748
|
+
* name is tested, never the directories leading to it.
|
|
716
749
|
*
|
|
717
750
|
* @param {object} options CLI options
|
|
718
751
|
* @param {string} [commandPath] Invoked command path or name
|
|
719
752
|
* @returns {boolean} True when predictive dependency audit should run
|
|
720
753
|
*/
|
|
721
754
|
export function shouldRunPredictiveBomAudit(options, commandPath) {
|
|
722
|
-
const
|
|
723
|
-
if (
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
if (normalizedCommandPath.includes("hbom")) {
|
|
755
|
+
const commandName = getCommandName(commandPath);
|
|
756
|
+
if (commandName.startsWith("obom") || commandName.startsWith("hbom")) {
|
|
727
757
|
return false;
|
|
728
758
|
}
|
|
729
759
|
const projectTypes = Array.isArray(options?.projectType)
|
package/lib/core/fs.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "./activity.js";
|
|
44
44
|
import { thoughtLog, traceLog } from "./logger.js";
|
|
45
45
|
import { isWin } from "./paths.js";
|
|
46
|
+
import { recordExecutedTool } from "./toolRegistry.js";
|
|
46
47
|
|
|
47
48
|
function getArchiveSourceByteSize(sourcePath) {
|
|
48
49
|
if (!sourcePath || !safeExistsSync(sourcePath)) {
|
|
@@ -522,8 +523,13 @@ function isWindowsShellHijackRisk(command, options) {
|
|
|
522
523
|
const absCwd = resolve(cwd);
|
|
523
524
|
for (const candidate of candidates) {
|
|
524
525
|
const candidatePath = path.join(absCwd, candidate);
|
|
525
|
-
|
|
526
|
-
|
|
526
|
+
// cmd.exe only runs files, so a directory named like the tool is harmless.
|
|
527
|
+
try {
|
|
528
|
+
if (statSync(candidatePath).isFile()) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
} catch (_err) {
|
|
532
|
+
// Not present.
|
|
527
533
|
}
|
|
528
534
|
}
|
|
529
535
|
return false;
|
|
@@ -831,24 +837,28 @@ export function safeSpawnSync(command, args, options) {
|
|
|
831
837
|
error: new Error("No execute permission"),
|
|
832
838
|
};
|
|
833
839
|
}
|
|
840
|
+
// cmd.exe looks in the working directory before PATH, so a scanned project
|
|
841
|
+
// could otherwise replace the tool cdxgen means to run. This holds outside
|
|
842
|
+
// secure mode too, since --no-install-deps scans still run version probes
|
|
843
|
+
// and read-only tools with the project as the working directory.
|
|
844
|
+
if (isWindowsShellHijackRisk(command, options)) {
|
|
845
|
+
const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
|
|
846
|
+
console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
|
|
847
|
+
recordActivity({
|
|
848
|
+
kind: activityDescriptor.kind,
|
|
849
|
+
...activityDescriptor.metadata,
|
|
850
|
+
reason: blockedReason,
|
|
851
|
+
status: "blocked",
|
|
852
|
+
target: activityDescriptor.target,
|
|
853
|
+
});
|
|
854
|
+
return {
|
|
855
|
+
status: 1,
|
|
856
|
+
stdout: undefined,
|
|
857
|
+
stderr: undefined,
|
|
858
|
+
error: new Error(blockedReason),
|
|
859
|
+
};
|
|
860
|
+
}
|
|
834
861
|
if (isSecureMode) {
|
|
835
|
-
if (isWindowsShellHijackRisk(command, options)) {
|
|
836
|
-
const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
|
|
837
|
-
console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
|
|
838
|
-
recordActivity({
|
|
839
|
-
kind: activityDescriptor.kind,
|
|
840
|
-
...activityDescriptor.metadata,
|
|
841
|
-
reason: blockedReason,
|
|
842
|
-
status: "blocked",
|
|
843
|
-
target: activityDescriptor.target,
|
|
844
|
-
});
|
|
845
|
-
return {
|
|
846
|
-
status: 1,
|
|
847
|
-
stdout: undefined,
|
|
848
|
-
stderr: undefined,
|
|
849
|
-
error: new Error(blockedReason),
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
862
|
if (options?.cwd && options.cwd !== resolve(options.cwd)) {
|
|
853
863
|
if (DEBUG_MODE) {
|
|
854
864
|
console.log(
|
|
@@ -982,6 +992,8 @@ export function safeSpawnSync(command, args, options) {
|
|
|
982
992
|
}
|
|
983
993
|
traceLog("spawn", { command, args, ...options });
|
|
984
994
|
commandsExecuted.add(command);
|
|
995
|
+
// The tool as invoked, before a shell invocation folds the arguments in.
|
|
996
|
+
const executedCommand = command;
|
|
985
997
|
// Fix for DEP0190 warning
|
|
986
998
|
if (options?.shell === true) {
|
|
987
999
|
const joined = joinShellCommand(command, args, rawShellArgs);
|
|
@@ -991,6 +1003,15 @@ export function safeSpawnSync(command, args, options) {
|
|
|
991
1003
|
}
|
|
992
1004
|
}
|
|
993
1005
|
const result = spawnSync(command, args, options);
|
|
1006
|
+
// The run-scoped tool registry feeds metadata.tools (issue #598); every
|
|
1007
|
+
// external execution funnels through this wrapper. A spawn that failed to
|
|
1008
|
+
// start, such as a missing binary, ran nothing and is not recorded; through
|
|
1009
|
+
// a shell, a missing command shows as exit code 127, or 9009 on cmd.exe.
|
|
1010
|
+
const shellCommandNotFound =
|
|
1011
|
+
options?.shell === true && [127, 9009].includes(result.status);
|
|
1012
|
+
if (!result.error && !shellCommandNotFound) {
|
|
1013
|
+
recordExecutedTool(executedCommand);
|
|
1014
|
+
}
|
|
994
1015
|
recordActivity({
|
|
995
1016
|
kind: activityDescriptor.kind,
|
|
996
1017
|
...activityDescriptor.metadata,
|