@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
|
@@ -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
|
@@ -301,6 +301,13 @@ 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
|
+
|
|
304
311
|
/** Resolved PHP executable command (PHP_CMD env var, or "php"). */
|
|
305
312
|
export const PHP_CMD = readEnvironmentVariable("PHP_CMD") || "php";
|
|
306
313
|
|
|
@@ -520,6 +527,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
520
527
|
"vscode",
|
|
521
528
|
"openvsx",
|
|
522
529
|
"vscode-extensions",
|
|
530
|
+
"ide",
|
|
523
531
|
"ide-extension",
|
|
524
532
|
"ide-extensions",
|
|
525
533
|
],
|
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)) {
|
|
@@ -991,6 +992,8 @@ export function safeSpawnSync(command, args, options) {
|
|
|
991
992
|
}
|
|
992
993
|
traceLog("spawn", { command, args, ...options });
|
|
993
994
|
commandsExecuted.add(command);
|
|
995
|
+
// The tool as invoked, before a shell invocation folds the arguments in.
|
|
996
|
+
const executedCommand = command;
|
|
994
997
|
// Fix for DEP0190 warning
|
|
995
998
|
if (options?.shell === true) {
|
|
996
999
|
const joined = joinShellCommand(command, args, rawShellArgs);
|
|
@@ -1000,6 +1003,15 @@ export function safeSpawnSync(command, args, options) {
|
|
|
1000
1003
|
}
|
|
1001
1004
|
}
|
|
1002
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
|
+
}
|
|
1003
1015
|
recordActivity({
|
|
1004
1016
|
kind: activityDescriptor.kind,
|
|
1005
1017
|
...activityDescriptor.metadata,
|
package/lib/core/httpClient.js
CHANGED
|
@@ -5,6 +5,7 @@ import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
|
5
5
|
|
|
6
6
|
import { Agent, EnvHttpProxyAgent, request as undiciRequest } from "undici";
|
|
7
7
|
|
|
8
|
+
import { readBrotliDecoder } from "./brotliProbe.js";
|
|
8
9
|
import { registerRunReset } from "./runState.js";
|
|
9
10
|
|
|
10
11
|
// Environment variables here are read with raw `process.env` rather than
|
|
@@ -208,6 +209,75 @@ export function resolveTimeout(timeout) {
|
|
|
208
209
|
return undefined;
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
let runtimeBrotliVerdict;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set or clear the cached runtime Brotli verdict. Exported for unit tests only.
|
|
216
|
+
*
|
|
217
|
+
* @param {boolean|undefined} verdict verdict to use, or undefined to re-check
|
|
218
|
+
*/
|
|
219
|
+
export function _setRuntimeBrotliVerdict(verdict) {
|
|
220
|
+
runtimeBrotliVerdict = verdict;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether the Brotli decoder of the runtime cdxgen runs on carries the
|
|
225
|
+
* CVE-2026-111497 fix, read once from the decoder itself: the node binary, or
|
|
226
|
+
* the shared libbrotlidec a `--shared-brotli` build (Homebrew, distro
|
|
227
|
+
* packages) loads; under Bun, the bun binary, which carries its own copy.
|
|
228
|
+
* process.versions.brotli cannot answer this, since Node.js backports the fix
|
|
229
|
+
* without changing it. Deno decodes Brotli in Rust, which was never affected.
|
|
230
|
+
*
|
|
231
|
+
* On Node.js cdxgen never asks for `br`, so a Brotli response comes only from
|
|
232
|
+
* a server that sends it unasked. When the decoder cannot be shown to be
|
|
233
|
+
* patched, such a body is left undecoded rather than handed to a decoder an
|
|
234
|
+
* attacker can write past. Bun decodes in its transport, so there the client
|
|
235
|
+
* stops asking for `br` instead.
|
|
236
|
+
*
|
|
237
|
+
* @param {Object} [runtime] the runtime to judge, for tests
|
|
238
|
+
* @returns {boolean} true when Brotli bodies may be decoded
|
|
239
|
+
*/
|
|
240
|
+
export function runtimeBrotliDecoderIsPatched(runtime = undefined) {
|
|
241
|
+
if (!runtime && runtimeBrotliVerdict !== undefined) {
|
|
242
|
+
return runtimeBrotliVerdict;
|
|
243
|
+
}
|
|
244
|
+
const subject = runtime || {
|
|
245
|
+
isDeno: globalThis.Deno?.version?.deno !== undefined,
|
|
246
|
+
execPath: process.execPath,
|
|
247
|
+
sharedBrotli: [true, "true"].includes(
|
|
248
|
+
process.config?.variables?.node_shared_brotli,
|
|
249
|
+
),
|
|
250
|
+
sharedObjects: () => process.report?.getReport?.()?.sharedObjects || [],
|
|
251
|
+
};
|
|
252
|
+
let verdict = false;
|
|
253
|
+
if (subject.isDeno) {
|
|
254
|
+
verdict = true;
|
|
255
|
+
} else {
|
|
256
|
+
const decoderFile = subject.sharedBrotli
|
|
257
|
+
? subject
|
|
258
|
+
.sharedObjects()
|
|
259
|
+
.find((path) =>
|
|
260
|
+
`${path}`.split(/[\\/]/).pop().startsWith("libbrotlidec"),
|
|
261
|
+
)
|
|
262
|
+
: subject.execPath;
|
|
263
|
+
verdict = Boolean(
|
|
264
|
+
decoderFile &&
|
|
265
|
+
readBrotliDecoder(decoderFile)?.c?.blockSwitchFix === "true",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (!runtime) {
|
|
269
|
+
runtimeBrotliVerdict = verdict;
|
|
270
|
+
if (!verdict) {
|
|
271
|
+
console.warn(
|
|
272
|
+
isBun
|
|
273
|
+
? "The Brotli decoder in this Bun is not patched for CVE-2026-111497, or could not be checked, so cdxgen no longer asks servers for Brotli-encoded responses. Bun still decodes one a server sends unasked; upgrade Bun to a release with a patched Brotli."
|
|
274
|
+
: "The Brotli decoder of this runtime is not patched for CVE-2026-111497, or could not be checked, so Brotli-encoded responses are left undecoded. Upgrade Node.js to 22.23.4, 24.22.1 or 26.12.1 or later (and the shared brotli library on Homebrew or distro builds).",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return verdict;
|
|
279
|
+
}
|
|
280
|
+
|
|
211
281
|
/**
|
|
212
282
|
* Decompress a response body buffer according to its `Content-Encoding`. undici
|
|
213
283
|
* performs no automatic decompression, so we replicate the behaviour `got` gave
|
|
@@ -235,6 +305,9 @@ function decompressBody(rawBody, contentEncoding) {
|
|
|
235
305
|
} else if (encoding === "deflate") {
|
|
236
306
|
buffer = inflateSync(buffer);
|
|
237
307
|
} else if (encoding === "br") {
|
|
308
|
+
if (!runtimeBrotliDecoderIsPatched()) {
|
|
309
|
+
return buffer;
|
|
310
|
+
}
|
|
238
311
|
buffer = brotliDecompressSync(buffer);
|
|
239
312
|
} else if (encoding === "identity") {
|
|
240
313
|
// no-op
|
|
@@ -296,6 +369,30 @@ async function readBody(undiciResponse, responseType) {
|
|
|
296
369
|
return { body, rawBody };
|
|
297
370
|
}
|
|
298
371
|
|
|
372
|
+
/**
|
|
373
|
+
* The headers to put on the wire. Bun's bundled undici asks for br by default
|
|
374
|
+
* and decodes it with Bun's own decoder before this client sees the body, so
|
|
375
|
+
* the check in decompressBody never runs there. Where that decoder is not
|
|
376
|
+
* patched, ask only for the encodings this client decodes itself. A server
|
|
377
|
+
* that sends br unasked is still decoded by Bun; only upgrading Bun closes
|
|
378
|
+
* that. The cache key is built from the caller's headers, not these.
|
|
379
|
+
*
|
|
380
|
+
* @param {Object} headers Request headers.
|
|
381
|
+
* @returns {Object} Headers to send.
|
|
382
|
+
*/
|
|
383
|
+
function transportHeaders(headers) {
|
|
384
|
+
if (
|
|
385
|
+
!isBun ||
|
|
386
|
+
Object.keys(headers).some(
|
|
387
|
+
(key) => key.toLowerCase() === "accept-encoding",
|
|
388
|
+
) ||
|
|
389
|
+
runtimeBrotliDecoderIsPatched()
|
|
390
|
+
) {
|
|
391
|
+
return headers;
|
|
392
|
+
}
|
|
393
|
+
return { ...headers, "accept-encoding": "gzip, deflate" };
|
|
394
|
+
}
|
|
395
|
+
|
|
299
396
|
/**
|
|
300
397
|
* Convert a body into the `[body, headers]` pair to hand to undici. When the
|
|
301
398
|
* `json` option is present it is serialized and a JSON `content-type` is added
|
|
@@ -752,7 +849,7 @@ async function doRequest(mergedOptions) {
|
|
|
752
849
|
const insecure = options.https?.rejectUnauthorized === false;
|
|
753
850
|
const requestOptions = {
|
|
754
851
|
method,
|
|
755
|
-
headers,
|
|
852
|
+
headers: transportHeaders(headers),
|
|
756
853
|
body: method === "GET" || method === "HEAD" ? undefined : body,
|
|
757
854
|
dispatcher: selectDispatcher(insecure),
|
|
758
855
|
signal,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, win32 } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Containment check for a path taken from a lockfile (issue #3816). A crafted
|
|
5
|
+
* npm lockfile can name `file:`/`link:` specifiers or link targets built from
|
|
6
|
+
* `..` chains or absolute paths, so a parser that is about to list such an
|
|
7
|
+
* entry checks the resolved path against the project root first.
|
|
8
|
+
*
|
|
9
|
+
* The candidate is resolved against `projectRoot`, so both relative values
|
|
10
|
+
* and the absolute paths arborist already normalised are accepted. The root
|
|
11
|
+
* itself counts as inside: a lockfile may legitimately link the project to
|
|
12
|
+
* its own directory.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} candidate Path to check, absolute or relative to projectRoot
|
|
15
|
+
* @param {string} projectRoot Project root the path must stay inside; a falsy
|
|
16
|
+
* root means the current working directory, mirroring `node:path`
|
|
17
|
+
*
|
|
18
|
+
* @returns {Boolean} True when the path is inside projectRoot
|
|
19
|
+
*/
|
|
20
|
+
export function isPathInsideProject(candidate, projectRoot) {
|
|
21
|
+
if (typeof candidate !== "string" || !candidate) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (candidate.includes("\0")) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const rootAbs = resolve(projectRoot || process.cwd());
|
|
28
|
+
const rel = relative(rootAbs, resolve(rootAbs, candidate));
|
|
29
|
+
if (!rel) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
// relative() returns an absolute path when the two sit on different
|
|
33
|
+
// Windows drives.
|
|
34
|
+
if (isAbsolute(rel) || win32.isAbsolute(rel)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\");
|
|
38
|
+
}
|
package/lib/core/paths.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `https://` URL, and `fileURLToPath` throws `ERR_INVALID_URL_SCHEME` on those.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { platform } from "node:os";
|
|
17
|
+
import { homedir, platform } from "node:os";
|
|
18
18
|
import { dirname } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
@@ -68,3 +68,26 @@ export function isValidDriveRoot(root) {
|
|
|
68
68
|
export function toCamel(str) {
|
|
69
69
|
return str.replace(/-([a-z])/g, (_, g) => g.toUpperCase());
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Rewrite an absolute path under the user's home directory to a `~`-relative
|
|
74
|
+
* form so the emitted BOM does not carry the local username.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} filePath Absolute path
|
|
77
|
+
* @param {string} [home] Home directory, defaulting to the current user's
|
|
78
|
+
* @returns {string} Home-relative path when applicable, else the input
|
|
79
|
+
*/
|
|
80
|
+
export function redactHomePath(filePath, home = homedir()) {
|
|
81
|
+
const value = String(filePath || "");
|
|
82
|
+
if (!home || !value.startsWith(home)) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
// Require a path boundary so a sibling home such as /Users/alice-backup is
|
|
86
|
+
// not rewritten as if it sat under /Users/alice.
|
|
87
|
+
const remainder = value.slice(home.length);
|
|
88
|
+
if (remainder && !/^[/\\]/u.test(remainder)) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
const rest = remainder.replace(/^[/\\]/u, "");
|
|
92
|
+
return rest ? `~/${rest.replaceAll("\\", "/")}` : "~";
|
|
93
|
+
}
|
package/lib/core/processTree.js
CHANGED
|
@@ -37,6 +37,8 @@ export function formatDuration(ms) {
|
|
|
37
37
|
* its end: once the root is gone its pid can be handed to an unrelated
|
|
38
38
|
* process, whose children would otherwise look like the run's. Elsewhere this
|
|
39
39
|
* does nothing: an orphan is re-parented and can no longer be told apart.
|
|
40
|
+
* It returns once the stopped processes have exited, or after ten seconds,
|
|
41
|
+
* so the caller can remove files the run left half-written.
|
|
40
42
|
*
|
|
41
43
|
* @param {number|undefined} rootPid pid of the process cdxgen spawned
|
|
42
44
|
* @param {number} startedAt Epoch milliseconds at which the run started
|
|
@@ -77,6 +79,9 @@ export function reapProcessTree(
|
|
|
77
79
|
" }",
|
|
78
80
|
"}",
|
|
79
81
|
"foreach ($child in $found) { Stop-Process -Id $child -Force -ErrorAction SilentlyContinue }",
|
|
82
|
+
// Stop-Process returns before the process exits, and a process that is
|
|
83
|
+
// still exiting keeps its files open, so wait for the exits.
|
|
84
|
+
"if ($found.Count) { Wait-Process -Id $found.ToArray() -Timeout 10 -ErrorAction SilentlyContinue }",
|
|
80
85
|
"$found -join ','",
|
|
81
86
|
].join("\n");
|
|
82
87
|
const result = safeSpawnSync(
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { registerRunReset } from "./runState.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Run-scoped registry of the external tools cdxgen actually executed, feeding
|
|
7
|
+
* the `metadata.tools` section (issue #598). `safeSpawnSync` records a command
|
|
8
|
+
* once the process has started, so a binary that is missing or a spawn that
|
|
9
|
+
* was blocked never appears, and the stages that already know a tool's
|
|
10
|
+
* version - such as the JVM tool resolution in pregen - enrich the entry; no
|
|
11
|
+
* process is ever spawned just to learn a version.
|
|
12
|
+
*
|
|
13
|
+
* The registry is filled in as the run progresses and read once per BOM when
|
|
14
|
+
* the metadata section is assembled. A run that spawns nothing external keeps
|
|
15
|
+
* it empty, which is what keeps such BOMs byte-identical to the previous
|
|
16
|
+
* behaviour. It is per-run state: a long-lived process such as the server
|
|
17
|
+
* empties it between scans through {@link resetRunState}.
|
|
18
|
+
*
|
|
19
|
+
* @type {Map<string, {name: string, version: (string|undefined)}>}
|
|
20
|
+
*/
|
|
21
|
+
const EXECUTED_TOOLS = new Map();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Versions stages already resolved for a tool, keyed like EXECUTED_TOOLS. They
|
|
25
|
+
* are applied only to tools that actually started, so provisioning a tool the
|
|
26
|
+
* run then never invokes adds nothing to `metadata.tools`.
|
|
27
|
+
*
|
|
28
|
+
* @type {Map<string, string>}
|
|
29
|
+
*/
|
|
30
|
+
const KNOWN_VERSIONS = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Interpreter and shell binaries that wrap the real work. They appear on
|
|
34
|
+
* nearly every spawn, so recording them would drown the tools that actually
|
|
35
|
+
* generated the BOM.
|
|
36
|
+
*
|
|
37
|
+
* @type {Set<string>}
|
|
38
|
+
*/
|
|
39
|
+
const WRAPPER_BASENAMES = new Set([
|
|
40
|
+
"bash",
|
|
41
|
+
"cmd",
|
|
42
|
+
"cmd.exe",
|
|
43
|
+
"node",
|
|
44
|
+
"powershell",
|
|
45
|
+
"powershell.exe",
|
|
46
|
+
"pwsh",
|
|
47
|
+
"sh",
|
|
48
|
+
"zsh",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Strip a trailing Windows executable suffix from a binary name.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} name Binary name
|
|
55
|
+
* @returns {string} Name without a trailing `.exe`, `.bat`, or `.cmd`
|
|
56
|
+
*/
|
|
57
|
+
function trimWindowsSuffix(name) {
|
|
58
|
+
for (const suffix of [".exe", ".bat", ".cmd"]) {
|
|
59
|
+
if (name.endsWith(suffix)) {
|
|
60
|
+
return name.slice(0, -suffix.length);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Name a tool the way the registry keys it: the basename without a Windows
|
|
68
|
+
* executable suffix, and without the `-cdxgen-<os>-<arch>` suffix of a
|
|
69
|
+
* bundled cdxgen-plugins-bin helper, so `trivy-cdxgen-linux-amd64` is `trivy`
|
|
70
|
+
* on every platform and matches its plugin manifest entry.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} name Tool name as invoked (a bare name or a path)
|
|
73
|
+
* @returns {string} Tool name
|
|
74
|
+
*/
|
|
75
|
+
function toolNameOf(name) {
|
|
76
|
+
const toolName = trimWindowsSuffix(basename(`${name}`.split("\\").join("/")));
|
|
77
|
+
const pluginSuffixAt = toolName.lastIndexOf("-cdxgen-");
|
|
78
|
+
if (
|
|
79
|
+
pluginSuffixAt > 0 &&
|
|
80
|
+
toolName.slice(pluginSuffixAt + "-cdxgen-".length).split("-").length === 2
|
|
81
|
+
) {
|
|
82
|
+
return toolName.slice(0, pluginSuffixAt);
|
|
83
|
+
}
|
|
84
|
+
return toolName;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record an external tool cdxgen executed. The first version seen wins unless
|
|
89
|
+
* it was unknown, so an enriched probe never overwrites a concrete version
|
|
90
|
+
* with `undefined` and a later concrete version fills an earlier gap.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} name Tool name as invoked (a bare name or a path)
|
|
93
|
+
* @param {string|undefined} version Version when cheaply known
|
|
94
|
+
*/
|
|
95
|
+
export function recordExecutedTool(name, version) {
|
|
96
|
+
if (typeof name !== "string" || !name) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const toolName = toolNameOf(name);
|
|
100
|
+
if (!toolName || WRAPPER_BASENAMES.has(toolName)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const key = toolName.toLowerCase();
|
|
104
|
+
const existing = EXECUTED_TOOLS.get(key);
|
|
105
|
+
if (!existing) {
|
|
106
|
+
EXECUTED_TOOLS.set(key, {
|
|
107
|
+
name: toolName,
|
|
108
|
+
version: typeof version === "string" && version ? version : undefined,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (typeof version === "string" && version && !existing.version) {
|
|
113
|
+
existing.version = version;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remember the version a stage already resolved for a tool, such as the JVM
|
|
119
|
+
* build tool pregen provisioned. The version shows in `metadata.tools` only if
|
|
120
|
+
* the tool is executed; the first version recorded wins.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} name Tool name as it will be invoked (a bare name or a path)
|
|
123
|
+
* @param {string} version Resolved version
|
|
124
|
+
*/
|
|
125
|
+
export function recordToolVersion(name, version) {
|
|
126
|
+
if (typeof name !== "string" || !name) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (typeof version !== "string" || !version) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const key = toolNameOf(name).toLowerCase();
|
|
133
|
+
if (key && !KNOWN_VERSIONS.has(key)) {
|
|
134
|
+
KNOWN_VERSIONS.set(key, version);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Snapshot of the tools recorded so far, sorted by name so the assembled
|
|
140
|
+
* `metadata.tools` diff stays stable between runs.
|
|
141
|
+
*
|
|
142
|
+
* @returns {Array<{name: string, version: (string|undefined)}>} Sorted tool records
|
|
143
|
+
*/
|
|
144
|
+
export function getExecutedTools() {
|
|
145
|
+
return [...EXECUTED_TOOLS.entries()]
|
|
146
|
+
.map(([key, tool]) => ({
|
|
147
|
+
name: tool.name,
|
|
148
|
+
version: tool.version || KNOWN_VERSIONS.get(key),
|
|
149
|
+
}))
|
|
150
|
+
.sort((a, b) =>
|
|
151
|
+
a.name === b.name
|
|
152
|
+
? 0
|
|
153
|
+
: a.name.toLowerCase() < b.name.toLowerCase()
|
|
154
|
+
? -1
|
|
155
|
+
: 1,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clear the registry. The server calls it between scans through the run-state
|
|
161
|
+
* reset; tests call it directly.
|
|
162
|
+
*
|
|
163
|
+
* @returns {void}
|
|
164
|
+
*/
|
|
165
|
+
export function resetExecutedTools() {
|
|
166
|
+
EXECUTED_TOOLS.clear();
|
|
167
|
+
KNOWN_VERSIONS.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
registerRunReset("toolRegistry.executedTools", resetExecutedTools);
|