@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
package/lib/core/httpClient.js
CHANGED
|
@@ -3,13 +3,9 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
Agent,
|
|
8
|
-
EnvHttpProxyAgent,
|
|
9
|
-
interceptors,
|
|
10
|
-
request as undiciRequest,
|
|
11
|
-
} from "undici";
|
|
6
|
+
import { Agent, EnvHttpProxyAgent, request as undiciRequest } from "undici";
|
|
12
7
|
|
|
8
|
+
import { readBrotliDecoder } from "./brotliProbe.js";
|
|
13
9
|
import { registerRunReset } from "./runState.js";
|
|
14
10
|
|
|
15
11
|
// Environment variables here are read with raw `process.env` rather than
|
|
@@ -213,6 +209,75 @@ export function resolveTimeout(timeout) {
|
|
|
213
209
|
return undefined;
|
|
214
210
|
}
|
|
215
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
|
+
|
|
216
281
|
/**
|
|
217
282
|
* Decompress a response body buffer according to its `Content-Encoding`. undici
|
|
218
283
|
* performs no automatic decompression, so we replicate the behaviour `got` gave
|
|
@@ -240,6 +305,9 @@ function decompressBody(rawBody, contentEncoding) {
|
|
|
240
305
|
} else if (encoding === "deflate") {
|
|
241
306
|
buffer = inflateSync(buffer);
|
|
242
307
|
} else if (encoding === "br") {
|
|
308
|
+
if (!runtimeBrotliDecoderIsPatched()) {
|
|
309
|
+
return buffer;
|
|
310
|
+
}
|
|
243
311
|
buffer = brotliDecompressSync(buffer);
|
|
244
312
|
} else if (encoding === "identity") {
|
|
245
313
|
// no-op
|
|
@@ -301,6 +369,30 @@ async function readBody(undiciResponse, responseType) {
|
|
|
301
369
|
return { body, rawBody };
|
|
302
370
|
}
|
|
303
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
|
+
|
|
304
396
|
/**
|
|
305
397
|
* Convert a body into the `[body, headers]` pair to hand to undici. When the
|
|
306
398
|
* `json` option is present it is serialized and a JSON `content-type` is added
|
|
@@ -534,20 +626,13 @@ function cacheKeyFor(method, url, headers = {}, responseType = undefined) {
|
|
|
534
626
|
// The maximum number of redirects to follow when `followRedirect` is enabled.
|
|
535
627
|
const MAX_REDIRECTIONS = 10;
|
|
536
628
|
|
|
537
|
-
// Bun ships its own bundled implementation of the `undici` module which
|
|
538
|
-
//
|
|
539
|
-
// `interceptors.redirect` interceptor that undici v7 uses for redirect handling
|
|
540
|
-
// (see cdxgen issue #4245). Detect the Bun runtime so we can fall back to plain
|
|
541
|
-
// dispatchers and follow redirects manually instead of crashing at module load.
|
|
629
|
+
// Bun ships its own bundled implementation of the `undici` module, which lacks
|
|
630
|
+
// `EnvHttpProxyAgent` (see cdxgen issue #4245).
|
|
542
631
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
543
632
|
|
|
544
|
-
// Reusable pooled dispatchers
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
// allocating a fresh Agent per request (which would defeat connection pooling).
|
|
548
|
-
// On Bun, `compose`/`interceptors.redirect` are unavailable, so the redirect
|
|
549
|
-
// dispatchers are just the plain agents and redirects are handled in
|
|
550
|
-
// `requestWithRedirects` below.
|
|
633
|
+
// Reusable pooled dispatchers, one per TLS verification setting, so requests
|
|
634
|
+
// share connections instead of allocating a fresh Agent each time. Redirects
|
|
635
|
+
// are followed in `requestWithRedirects` below on every runtime.
|
|
551
636
|
// Whether standard proxy environment variables are set. When they are, requests
|
|
552
637
|
// must be routed through the proxy. Because this client always passes an
|
|
553
638
|
// explicit per-request `dispatcher`, it overrides Node's global (potentially
|
|
@@ -584,51 +669,72 @@ const defaultAgent = createBaseAgent();
|
|
|
584
669
|
const insecureAgent = createBaseAgent({
|
|
585
670
|
connect: { rejectUnauthorized: false },
|
|
586
671
|
});
|
|
587
|
-
const redirectDispatcher = isBun
|
|
588
|
-
? defaultAgent
|
|
589
|
-
: defaultAgent.compose(
|
|
590
|
-
interceptors.redirect({ maxRedirections: MAX_REDIRECTIONS }),
|
|
591
|
-
);
|
|
592
|
-
const insecureRedirectDispatcher = isBun
|
|
593
|
-
? insecureAgent
|
|
594
|
-
: insecureAgent.compose(
|
|
595
|
-
interceptors.redirect({ maxRedirections: MAX_REDIRECTIONS }),
|
|
596
|
-
);
|
|
597
672
|
|
|
598
673
|
/**
|
|
599
|
-
* Pick the undici dispatcher matching the request's
|
|
600
|
-
*
|
|
674
|
+
* Pick the undici dispatcher matching the request's TLS verification
|
|
675
|
+
* preference. Redirects are followed by `requestWithRedirects`, not by the
|
|
676
|
+
* dispatcher.
|
|
601
677
|
*
|
|
602
|
-
* @param {boolean} followRedirect Whether HTTP redirects should be followed.
|
|
603
678
|
* @param {boolean} insecure Whether TLS certificate verification is disabled.
|
|
604
679
|
* @returns {import("undici").Dispatcher} The dispatcher to use for the request.
|
|
605
680
|
*/
|
|
606
|
-
function selectDispatcher(
|
|
607
|
-
|
|
608
|
-
return followRedirect ? insecureRedirectDispatcher : insecureAgent;
|
|
609
|
-
}
|
|
610
|
-
return followRedirect ? redirectDispatcher : defaultAgent;
|
|
681
|
+
function selectDispatcher(insecure) {
|
|
682
|
+
return insecure ? insecureAgent : defaultAgent;
|
|
611
683
|
}
|
|
612
684
|
|
|
613
685
|
// HTTP status codes that represent a redirect with a `location` header.
|
|
614
686
|
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
615
687
|
|
|
688
|
+
// Request headers that may follow a redirect to a different origin. Everything
|
|
689
|
+
// else, such as Authorization, cookies and registry API keys like X-Api-Key or
|
|
690
|
+
// PRIVATE-TOKEN, stays with the origin the caller addressed.
|
|
691
|
+
const CROSS_ORIGIN_REDIRECT_HEADERS = new Set([
|
|
692
|
+
"accept",
|
|
693
|
+
"accept-encoding",
|
|
694
|
+
"accept-language",
|
|
695
|
+
"user-agent",
|
|
696
|
+
]);
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Keep only the request headers that may be sent to a different origin.
|
|
700
|
+
*
|
|
701
|
+
* @param {Object} headers Request headers.
|
|
702
|
+
* @returns {Object} Headers safe to send cross-origin.
|
|
703
|
+
*/
|
|
704
|
+
function crossOriginRedirectHeaders(headers = {}) {
|
|
705
|
+
const kept = {};
|
|
706
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
707
|
+
if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name.toLowerCase())) {
|
|
708
|
+
kept[name] = value;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return kept;
|
|
712
|
+
}
|
|
713
|
+
|
|
616
714
|
/**
|
|
617
|
-
* Issue an undici request
|
|
715
|
+
* Issue an undici request and follow redirects one hop at a time.
|
|
618
716
|
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
717
|
+
* Redirects are followed here rather than by undici's redirect interceptor so
|
|
718
|
+
* that every hop goes through `beforeRedirect`, which runs the same host
|
|
719
|
+
* allowlist, protocol and dry-run checks as the first request, and so that
|
|
720
|
+
* credentials are not forwarded to another origin. Each intermediate body is
|
|
721
|
+
* drained and relative `location` headers are resolved, up to
|
|
623
722
|
* `MAX_REDIRECTIONS` hops.
|
|
624
723
|
*
|
|
625
724
|
* @param {URL} url Request URL.
|
|
626
725
|
* @param {Object} requestOptions Options passed to `undiciRequest`.
|
|
627
726
|
* @param {boolean} followRedirect Whether redirects should be followed.
|
|
727
|
+
* @param {function(URL): Promise<void>} beforeRedirect Called with the next URL
|
|
728
|
+
* before each hop; throws to stop the request.
|
|
628
729
|
* @returns {Promise<Object>} The final undici response.
|
|
629
730
|
*/
|
|
630
|
-
async function requestWithRedirects(
|
|
631
|
-
|
|
731
|
+
async function requestWithRedirects(
|
|
732
|
+
url,
|
|
733
|
+
requestOptions,
|
|
734
|
+
followRedirect,
|
|
735
|
+
beforeRedirect,
|
|
736
|
+
) {
|
|
737
|
+
if (!followRedirect) {
|
|
632
738
|
return await undiciRequest(url, requestOptions);
|
|
633
739
|
}
|
|
634
740
|
let currentUrl = url;
|
|
@@ -649,7 +755,14 @@ async function requestWithRedirects(url, requestOptions, followRedirect) {
|
|
|
649
755
|
} catch {
|
|
650
756
|
// Ignore drain failures; the socket will be discarded.
|
|
651
757
|
}
|
|
652
|
-
|
|
758
|
+
const nextUrl = new URL(location, currentUrl);
|
|
759
|
+
await beforeRedirect(nextUrl);
|
|
760
|
+
if (nextUrl.origin !== currentUrl.origin) {
|
|
761
|
+
requestOptions.headers = crossOriginRedirectHeaders(
|
|
762
|
+
requestOptions.headers,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
currentUrl = nextUrl;
|
|
653
766
|
redirections += 1;
|
|
654
767
|
// 303, and 301/302 for anything other than GET/HEAD, must switch to GET
|
|
655
768
|
// without a body, matching how undici's redirect interceptor behaves.
|
|
@@ -736,11 +849,23 @@ async function doRequest(mergedOptions) {
|
|
|
736
849
|
const insecure = options.https?.rejectUnauthorized === false;
|
|
737
850
|
const requestOptions = {
|
|
738
851
|
method,
|
|
739
|
-
headers,
|
|
852
|
+
headers: transportHeaders(headers),
|
|
740
853
|
body: method === "GET" || method === "HEAD" ? undefined : body,
|
|
741
|
-
dispatcher: selectDispatcher(
|
|
854
|
+
dispatcher: selectDispatcher(insecure),
|
|
742
855
|
signal,
|
|
743
856
|
};
|
|
857
|
+
// A redirect target must pass the same beforeRequest checks, such as the
|
|
858
|
+
// host allowlist, as the URL the caller asked for.
|
|
859
|
+
const beforeRedirect = async (nextUrl) => {
|
|
860
|
+
const hopOptions = {
|
|
861
|
+
...options,
|
|
862
|
+
url: nextUrl,
|
|
863
|
+
context: { ...options.context },
|
|
864
|
+
};
|
|
865
|
+
for (const hook of options.hooks?.beforeRequest || []) {
|
|
866
|
+
await hook(hopOptions);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
744
869
|
|
|
745
870
|
let undiciResponse;
|
|
746
871
|
try {
|
|
@@ -748,6 +873,7 @@ async function doRequest(mergedOptions) {
|
|
|
748
873
|
url,
|
|
749
874
|
requestOptions,
|
|
750
875
|
followRedirect,
|
|
876
|
+
beforeRedirect,
|
|
751
877
|
);
|
|
752
878
|
} catch (err) {
|
|
753
879
|
const requestError = new RequestError(err, options);
|
|
@@ -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,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy for running the scanned project's own build tooling.
|
|
3
|
+
*
|
|
4
|
+
* Build tools such as Maven, Gradle, Poetry, MSBuild, SwiftPM, Leiningen and
|
|
5
|
+
* CocoaPods read configuration or run code that ships with the project
|
|
6
|
+
* (`.mvn/jvm.config`, `.poetry/plugins`, `Directory.Build.rsp`, `Package.swift`,
|
|
7
|
+
* `project.clj`, podspecs). Running them on an untrusted project runs that
|
|
8
|
+
* project's code, so they belong to the same surface as dependency
|
|
9
|
+
* installation and are skipped whenever installation is disabled.
|
|
10
|
+
*/
|
|
11
|
+
import { isSecureMode, readEnvironmentVariable } from "./activity.js";
|
|
12
|
+
import { recordDegradation } from "./buildLedger.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether secure mode is on right now. `isSecureMode` is read once when the
|
|
16
|
+
* modules load, but the `cdxgen-secure` command turns secure mode on later,
|
|
17
|
+
* by setting CDXGEN_SECURE_MODE before the scan starts.
|
|
18
|
+
*
|
|
19
|
+
* @returns {boolean} true when secure mode is on
|
|
20
|
+
*/
|
|
21
|
+
export function isSecureModeActive() {
|
|
22
|
+
return (
|
|
23
|
+
isSecureMode ||
|
|
24
|
+
["true", "1"].includes(readEnvironmentVariable("CDXGEN_SECURE_MODE"))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the scan may run the scanned project's own build tooling, such as
|
|
30
|
+
* wrapper scripts or build definitions that Maven, Gradle, sbt, Mill, Poetry,
|
|
31
|
+
* PDM, MSBuild, SwiftPM, Leiningen and CocoaPods evaluate.
|
|
32
|
+
*
|
|
33
|
+
* Tooling follows `options.installDeps`, which defaults to off in secure mode
|
|
34
|
+
* when a caller does not set it. Introspection mode exists to observe the
|
|
35
|
+
* project's own toolchain, so it keeps that tooling on even when dependency
|
|
36
|
+
* installation is disabled, as long as the operator asked for it rather than
|
|
37
|
+
* a config file in the scanned project. Secure mode still wins:
|
|
38
|
+
* CDXGEN_INTROSPECT alone must not make a secure-mode scan run the scanned
|
|
39
|
+
* project's code.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} [options] CLI options
|
|
42
|
+
* @returns {boolean} true when project tooling may run
|
|
43
|
+
*/
|
|
44
|
+
export function isProjectToolingAllowed(options) {
|
|
45
|
+
const secureMode = isSecureModeActive();
|
|
46
|
+
const installDeps = options?.installDeps ?? !secureMode;
|
|
47
|
+
if (installDeps !== false) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return (
|
|
51
|
+
options?.introspect === true &&
|
|
52
|
+
options?.introspectFromConfigFile !== true &&
|
|
53
|
+
!secureMode
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The skips already reported for each scan, keyed by the scan's options.
|
|
58
|
+
// Callers without options share one key.
|
|
59
|
+
const reportedSkips = new WeakMap();
|
|
60
|
+
const noOptionsKey = {};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Tell the user that a tool was skipped because project tooling is not
|
|
64
|
+
* allowed, and record the matching degradation for the fidelity report.
|
|
65
|
+
* Each skip is reported once per scan, however many manifests it applies to.
|
|
66
|
+
* A skip that only costs evidence, not BOM content, passes no remediation id
|
|
67
|
+
* and is only shown.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} options CLI options of the scan
|
|
70
|
+
* @param {string|null} remediationId Remediation id from
|
|
71
|
+
* data/remediations.json, or null for a message only
|
|
72
|
+
* @param {string} message Console message shown to the user
|
|
73
|
+
* @param {Object} [fields] Degradation fields (ecosystem, tool, impact, detail)
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
export function reportProjectToolingSkipped(
|
|
77
|
+
options,
|
|
78
|
+
remediationId,
|
|
79
|
+
message,
|
|
80
|
+
fields,
|
|
81
|
+
) {
|
|
82
|
+
const key = options && typeof options === "object" ? options : noOptionsKey;
|
|
83
|
+
let reported = reportedSkips.get(key);
|
|
84
|
+
if (!reported) {
|
|
85
|
+
reported = new Set();
|
|
86
|
+
reportedSkips.set(key, reported);
|
|
87
|
+
}
|
|
88
|
+
const skipKey = remediationId || message;
|
|
89
|
+
if (reported.has(skipKey)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
reported.add(skipKey);
|
|
93
|
+
console.log(message);
|
|
94
|
+
if (remediationId) {
|
|
95
|
+
recordDegradation(remediationId, fields);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -26,6 +26,15 @@ const JSON_PROPERTY_NAMES = new Set([
|
|
|
26
26
|
]);
|
|
27
27
|
const URL_PATTERN = /https?:\/\/[^\s<>"'),\]}]+/giu;
|
|
28
28
|
const MAX_STRUCTURE_DEPTH = 6;
|
|
29
|
+
// What an RFC 3986 path may hold as it is: unreserved characters, sub-delims,
|
|
30
|
+
// ":", "@", "/" and well-formed percent-escapes. A query or fragment may also
|
|
31
|
+
// hold "?". Everything else is percent-encoded.
|
|
32
|
+
const URI_PATH_UNSAFE =
|
|
33
|
+
/%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/%]/gu;
|
|
34
|
+
const URI_QUERY_UNSAFE =
|
|
35
|
+
/%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/?%]/gu;
|
|
36
|
+
const URI_SCHEME_AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*/u;
|
|
37
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
29
38
|
|
|
30
39
|
function sanitizeUrlForBom(value) {
|
|
31
40
|
const input = String(value || "").trim();
|
|
@@ -146,6 +155,69 @@ export function sanitizeBomUrl(value) {
|
|
|
146
155
|
return sanitizeUrlForBom(value);
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
function percentEncodeUnsafe(text, unsafe) {
|
|
159
|
+
return text.replace(unsafe, (character) =>
|
|
160
|
+
Array.from(
|
|
161
|
+
UTF8_ENCODER.encode(character),
|
|
162
|
+
(byte) => `%${byte.toString(16).toUpperCase().padStart(2, "0")}`,
|
|
163
|
+
).join(""),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Percent-encode what an RFC 3986 URI reference cannot hold as it is, so a
|
|
169
|
+
* route template or URL can be emitted where CycloneDX expects an
|
|
170
|
+
* iri-reference, such as `services[].endpoints[]`. The braces of a route
|
|
171
|
+
* template, spaces, `<>`, `[]` outside an IPv6 host, `|`, `^`, non-ASCII
|
|
172
|
+
* characters and a `%` that starts no escape are encoded:
|
|
173
|
+
* `/users/{id}` becomes `/users/%7Bid%7D`. The scheme and authority of an
|
|
174
|
+
* absolute URL are kept as written, the query and fragment stay a query and
|
|
175
|
+
* fragment, and existing escapes are kept, so encoding twice changes
|
|
176
|
+
* nothing. Nothing is removed; strip credentials with {@link sanitizeBomUrl}
|
|
177
|
+
* first.
|
|
178
|
+
*
|
|
179
|
+
* @param {unknown} value Route template, path or URL
|
|
180
|
+
* @returns {string} The encoded reference
|
|
181
|
+
*/
|
|
182
|
+
export function encodeUriReference(value) {
|
|
183
|
+
const text = String(value ?? "");
|
|
184
|
+
const authority = URI_SCHEME_AUTHORITY.exec(text)?.[0] ?? "";
|
|
185
|
+
const rest = text.slice(authority.length);
|
|
186
|
+
const queryStart = rest.indexOf("?");
|
|
187
|
+
const fragmentStart = rest.indexOf("#");
|
|
188
|
+
const pathEnd = Math.min(
|
|
189
|
+
queryStart < 0 ? rest.length : queryStart,
|
|
190
|
+
fragmentStart < 0 ? rest.length : fragmentStart,
|
|
191
|
+
);
|
|
192
|
+
let encoded =
|
|
193
|
+
authority + percentEncodeUnsafe(rest.slice(0, pathEnd), URI_PATH_UNSAFE);
|
|
194
|
+
if (pathEnd === queryStart) {
|
|
195
|
+
const queryEnd = fragmentStart > queryStart ? fragmentStart : rest.length;
|
|
196
|
+
encoded += `?${percentEncodeUnsafe(rest.slice(queryStart + 1, queryEnd), URI_QUERY_UNSAFE)}`;
|
|
197
|
+
}
|
|
198
|
+
if (fragmentStart >= 0) {
|
|
199
|
+
encoded += `#${percentEncodeUnsafe(rest.slice(fragmentStart + 1), URI_QUERY_UNSAFE)}`;
|
|
200
|
+
}
|
|
201
|
+
return encoded;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether a value carries a token shape the BOM sanitizer redacts: cloud
|
|
206
|
+
* access keys, bearer tokens, GitHub, Slack and npm tokens, JWTs and the
|
|
207
|
+
* like. Callers that cannot splice a `[redacted]` marker into the value, such
|
|
208
|
+
* as a URL path segment, use it to drop the value instead.
|
|
209
|
+
*
|
|
210
|
+
* @param {unknown} value Value to check
|
|
211
|
+
* @returns {boolean} true when the value holds an inline credential
|
|
212
|
+
*/
|
|
213
|
+
export function hasInlineCredential(value) {
|
|
214
|
+
const text = String(value ?? "");
|
|
215
|
+
return INLINE_CREDENTIAL_PATTERNS.some((pattern) => {
|
|
216
|
+
pattern.lastIndex = 0;
|
|
217
|
+
return pattern.test(text);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
149
221
|
/**
|
|
150
222
|
* Sanitize a property value before serializing it into BOM properties.
|
|
151
223
|
*
|