@cdxgen/cdxgen 13.3.0 → 13.4.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 -6
- package/bin/cdxgen.js +90 -26
- package/bin/evinse.js +18 -1
- package/bin/repl.js +86 -0
- package/bin/sign.js +45 -15
- package/bin/tracebom.js +5 -1
- package/bin/validate.js +84 -18
- package/bin/verify.js +155 -86
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +61 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/index.js +113 -54
- package/lib/cli/jsBom.js +50 -29
- package/lib/cli/jvmBom.js +665 -138
- package/lib/cli/managedBom.js +320 -200
- package/lib/cli/nativeBom.js +139 -30
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +17 -9
- package/lib/core/fs.js +3 -2
- package/lib/core/httpClient.js +159 -24
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +5 -7
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/cmakeResolver.js +10 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +337 -22
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1967 -788
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +314 -30
- package/lib/ecosystems/jsEvidence.js +85 -24
- package/lib/ecosystems/kosi.js +21 -4
- package/lib/ecosystems/npmutils.js +64 -0
- package/lib/ecosystems/parsers-dotnet.js +567 -151
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +324 -61
- package/lib/ecosystems/parsers-js.js +92 -26
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +182 -55
- package/lib/ecosystems/parsers-python.js +141 -10
- package/lib/ecosystems/parsers-rust.js +4 -4
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +67 -17
- package/lib/ecosystems/rusi.js +22 -9
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +578 -69
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +576 -128
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +171 -48
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/dosai.js +248 -44
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +52 -6
- package/lib/inventory/evidenceUtils.js +154 -109
- package/lib/inventory/fetchBatch.js +574 -38
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +8 -8
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +88 -7
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/docker.js +33 -32
- package/lib/managers/oci.js +24 -2
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +16 -2
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +15 -1
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/index.js +25 -10
- package/lib/validator/reporters/json.js +1 -0
- package/package.json +21 -21
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +9 -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 +13 -8
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.d.ts.map +1 -0
- package/types/lib/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +50 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +15 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +44 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +6 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +21 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +75 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +80 -4
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +82 -10
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
- package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +57 -8
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +12 -0
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +3 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +2 -1
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- package/types/lib/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +8 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
package/lib/cli/nativeBom.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
|
|
7
7
|
import { build } from "@cdxgen/cdx-purl";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "../core/fs.js";
|
|
29
29
|
import { thoughtLog } from "../core/logger.js";
|
|
30
30
|
import { dirNameStr, isMac } from "../core/paths.js";
|
|
31
|
+
import { readCrateArchiveValues } from "../ecosystems/cargoCache.js";
|
|
31
32
|
import {
|
|
32
33
|
buildDependentPurl,
|
|
33
34
|
collapseCmakeVersions,
|
|
@@ -35,7 +36,11 @@ import {
|
|
|
35
36
|
resolveCmakeContext,
|
|
36
37
|
withoutScrapedFetchDependency,
|
|
37
38
|
} from "../ecosystems/cmakeResolver.js";
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
emptyCppBuildContext,
|
|
41
|
+
resolveCppBuildContext,
|
|
42
|
+
} from "../ecosystems/cppBuildContext.js";
|
|
43
|
+
import { getCppModules, hasCppSources } from "../ecosystems/cppEvidence.js";
|
|
39
44
|
import {
|
|
40
45
|
getCratesMetadata,
|
|
41
46
|
getSwiftPackageMetadata,
|
|
@@ -58,6 +63,7 @@ import {
|
|
|
58
63
|
parseGoModWhy,
|
|
59
64
|
parseGopkgData,
|
|
60
65
|
parseGosumData,
|
|
66
|
+
parseGosumHashes,
|
|
61
67
|
} from "../ecosystems/parsers-go.js";
|
|
62
68
|
import { parseJuliaProject } from "../ecosystems/parsers-julia.js";
|
|
63
69
|
import {
|
|
@@ -115,7 +121,9 @@ import {
|
|
|
115
121
|
} from "../ecosystems/parsers-rust.js";
|
|
116
122
|
import { parseSpackLock } from "../ecosystems/parsers-spack.js";
|
|
117
123
|
import { parseStackProject } from "../ecosystems/parsers-stack.js";
|
|
124
|
+
import { findVendoredCode } from "../ecosystems/vendoredCode.js";
|
|
118
125
|
import { resolveZigGraph } from "../ecosystems/zigResolver.js";
|
|
126
|
+
import { cmakeBuildDirCandidates } from "../inventory/cmakeBuildDirs.js";
|
|
119
127
|
import { mergeDependencies, trimComponents } from "../inventory/depsUtils.js";
|
|
120
128
|
import { convertOSQueryResults } from "../inventory/evidenceUtils.js";
|
|
121
129
|
import { applyPurl, nixBomRef, tryBuildPurl } from "../inventory/purl.js";
|
|
@@ -339,12 +347,9 @@ export async function createGoBom(path, options) {
|
|
|
339
347
|
console.log(`Parsing ${f}`);
|
|
340
348
|
}
|
|
341
349
|
const gosumData = readFileSync(f, { encoding: "utf-8" });
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
gosumMap[`${pkg.name}@${pkg.version}`] = pkg._integrity;
|
|
346
|
-
});
|
|
347
|
-
}
|
|
350
|
+
// Only the hashes are needed here; reading them costs no registry
|
|
351
|
+
// round trip. The components come from go.mod and the go list output.
|
|
352
|
+
Object.assign(gosumMap, parseGosumHashes(gosumData));
|
|
348
353
|
}
|
|
349
354
|
}
|
|
350
355
|
|
|
@@ -1163,7 +1168,9 @@ export function buildCargoCacheComponent(crateFile) {
|
|
|
1163
1168
|
|
|
1164
1169
|
/**
|
|
1165
1170
|
* Enriches a cargo cache component with a SHA-256 hash and filename-based
|
|
1166
|
-
* identity evidence (confidence 0.5)
|
|
1171
|
+
* identity evidence (confidence 0.5), and with what the archive's own manifest
|
|
1172
|
+
* says: its licence, description and repository, read from the archive without
|
|
1173
|
+
* extracting it anywhere. Hash computation failures are ignored.
|
|
1167
1174
|
*
|
|
1168
1175
|
* @param {string} crateFile Absolute path to the `.crate` archive
|
|
1169
1176
|
* @param {object} [component] Component produced by `buildCargoCacheComponent`
|
|
@@ -1183,6 +1190,33 @@ export async function enrichCargoCacheComponent(crateFile, component) {
|
|
|
1183
1190
|
} catch {
|
|
1184
1191
|
// continue without hashes
|
|
1185
1192
|
}
|
|
1193
|
+
const archive = await readCrateArchiveValues(
|
|
1194
|
+
crateFile,
|
|
1195
|
+
component.name,
|
|
1196
|
+
component.version,
|
|
1197
|
+
);
|
|
1198
|
+
if (archive) {
|
|
1199
|
+
if (archive.values.license && !component.license) {
|
|
1200
|
+
component.license = archive.values.license;
|
|
1201
|
+
}
|
|
1202
|
+
if (archive.values.licenses && !component.licenses) {
|
|
1203
|
+
component.licenses = archive.values.licenses;
|
|
1204
|
+
}
|
|
1205
|
+
if (archive.values.description && !component.description) {
|
|
1206
|
+
component.description = archive.values.description;
|
|
1207
|
+
}
|
|
1208
|
+
if (archive.values.repository && !component.repository) {
|
|
1209
|
+
component.repository = { url: archive.values.repository };
|
|
1210
|
+
}
|
|
1211
|
+
if (archive.values.homepage && !component.homepage) {
|
|
1212
|
+
component.homepage = { url: archive.values.homepage };
|
|
1213
|
+
}
|
|
1214
|
+
component.properties = component.properties || [];
|
|
1215
|
+
component.properties.push({
|
|
1216
|
+
name: "cdx:cargo:metadataSource",
|
|
1217
|
+
value: "local-registry",
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1186
1220
|
component.evidence = {
|
|
1187
1221
|
identity: {
|
|
1188
1222
|
field: "purl",
|
|
@@ -1365,6 +1399,7 @@ export function createCppBom(path, options) {
|
|
|
1365
1399
|
}
|
|
1366
1400
|
let pkgList = [];
|
|
1367
1401
|
let parentComponentDependencies = [];
|
|
1402
|
+
const subProjects = [];
|
|
1368
1403
|
if (conanLockFiles.length) {
|
|
1369
1404
|
for (const f of conanLockFiles) {
|
|
1370
1405
|
if (DEBUG_MODE) {
|
|
@@ -1477,8 +1512,17 @@ export function createCppBom(path, options) {
|
|
|
1477
1512
|
Object.keys(retMap.parentComponent).length &&
|
|
1478
1513
|
!addedParentComponentsMap[retMap.parentComponent.name]
|
|
1479
1514
|
) {
|
|
1515
|
+
// a project() below the scan root is a part of this project, not a
|
|
1516
|
+
// dependency of it
|
|
1480
1517
|
retMap.parentComponent.type = "library";
|
|
1481
|
-
|
|
1518
|
+
const subprojectDir = relative(scanRoot, resolve(dirname(f)))
|
|
1519
|
+
.split(sep)
|
|
1520
|
+
.join("/");
|
|
1521
|
+
retMap.parentComponent.properties = [
|
|
1522
|
+
...(retMap.parentComponent.properties || []),
|
|
1523
|
+
{ name: "cdx:cmake:subprojectDir", value: subprojectDir || "." },
|
|
1524
|
+
];
|
|
1525
|
+
subProjects.push(retMap.parentComponent);
|
|
1482
1526
|
addedParentComponentsMap[retMap.parentComponent.name] = true;
|
|
1483
1527
|
}
|
|
1484
1528
|
// Retain the dependency tree from cmake
|
|
@@ -1522,6 +1566,29 @@ export function createCppBom(path, options) {
|
|
|
1522
1566
|
boundaries: new Map(),
|
|
1523
1567
|
}
|
|
1524
1568
|
: resolveCmakeContext(path, options);
|
|
1569
|
+
// How the project is built: its presets and compilers (formulation), its
|
|
1570
|
+
// hardening options (project properties) and its own include directories.
|
|
1571
|
+
const isImageScan = hasAnyProjectType(
|
|
1572
|
+
["docker", "oci", "container", "os"],
|
|
1573
|
+
options,
|
|
1574
|
+
false,
|
|
1575
|
+
);
|
|
1576
|
+
// Code carried in the tree under a license of its own, outside the sources
|
|
1577
|
+
// of the dependencies CMake already knows and the build trees
|
|
1578
|
+
const vendored = isImageScan
|
|
1579
|
+
? { components: [], dirs: [] }
|
|
1580
|
+
: findVendoredCode(path, options, [
|
|
1581
|
+
...[...(cmakeContext.boundaries || new Map())]
|
|
1582
|
+
.filter(([, e]) => e?.kind === "fetch" || e?.kind === "submodule")
|
|
1583
|
+
.map(([dir]) => resolve(path, dir)),
|
|
1584
|
+
...cmakeBuildDirCandidates(path, options),
|
|
1585
|
+
]);
|
|
1586
|
+
if (vendored.components.length) {
|
|
1587
|
+
pkgList = pkgList.concat(vendored.components);
|
|
1588
|
+
}
|
|
1589
|
+
const buildContext = isImageScan
|
|
1590
|
+
? emptyCppBuildContext()
|
|
1591
|
+
: resolveCppBuildContext(path, options, cmakeContext, vendored.dirs);
|
|
1525
1592
|
if (cmakeContext.rootProject?.version && parentComponent) {
|
|
1526
1593
|
const resolvedVersion = cmakeContext.rootProject.version;
|
|
1527
1594
|
const scrapedVersion = parentComponent.version;
|
|
@@ -1651,30 +1718,44 @@ export function createCppBom(path, options) {
|
|
|
1651
1718
|
// We collect OS packages with the word dev in the name using osquery here
|
|
1652
1719
|
// rpm, deb and ebuild are supported
|
|
1653
1720
|
// TODO: For archlinux and alpine users we need a different mechanism to collect this information
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1721
|
+
// The packages only resolve the headers named by the usages slice, so
|
|
1722
|
+
// they are not queried when there is neither a slice to reuse nor a C or
|
|
1723
|
+
// C++ source for atom to read.
|
|
1724
|
+
if (
|
|
1725
|
+
(options.usagesSlicesFile && safeExistsSync(options.usagesSlicesFile)) ||
|
|
1726
|
+
hasCppSources(path, options)
|
|
1727
|
+
) {
|
|
1728
|
+
for (const queryCategory of Object.keys(cosDbQueries)) {
|
|
1729
|
+
const queryObj = cosDbQueries[queryCategory];
|
|
1730
|
+
const results = executeOsQuery(queryObj.query);
|
|
1731
|
+
const dlist = convertOSQueryResults(
|
|
1732
|
+
queryCategory,
|
|
1733
|
+
queryObj,
|
|
1734
|
+
results,
|
|
1735
|
+
true,
|
|
1736
|
+
{
|
|
1737
|
+
deb: executeDpkgList,
|
|
1738
|
+
rpm: executeRpmList,
|
|
1739
|
+
apk: executeApkList,
|
|
1740
|
+
ebuild: executeEqueryList,
|
|
1741
|
+
alpm: executeAlpmList,
|
|
1742
|
+
},
|
|
1743
|
+
);
|
|
1744
|
+
if (dlist?.length) {
|
|
1745
|
+
osPkgsList = osPkgsList.concat(dlist);
|
|
1746
|
+
}
|
|
1672
1747
|
}
|
|
1673
1748
|
}
|
|
1674
1749
|
// Now we check with atom and attempt to detect all external modules via usages
|
|
1675
1750
|
// We pass the current list of packages so that we enhance the current list and replace
|
|
1676
1751
|
// components inadvertently. For example, we might resolved a name, version and url information already via cmake
|
|
1677
|
-
const retMap = getCppModules(
|
|
1752
|
+
const retMap = getCppModules(
|
|
1753
|
+
path,
|
|
1754
|
+
options,
|
|
1755
|
+
osPkgsList,
|
|
1756
|
+
pkgList,
|
|
1757
|
+
buildContext.isFirstPartyHeader,
|
|
1758
|
+
);
|
|
1678
1759
|
if (retMap.pkgList?.length) {
|
|
1679
1760
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
1680
1761
|
}
|
|
@@ -1718,6 +1799,33 @@ export function createCppBom(path, options) {
|
|
|
1718
1799
|
]);
|
|
1719
1800
|
}
|
|
1720
1801
|
|
|
1802
|
+
if (subProjects.length) {
|
|
1803
|
+
if (parentComponent) {
|
|
1804
|
+
parentComponent.components = parentComponent.components || [];
|
|
1805
|
+
for (const sub of subProjects) {
|
|
1806
|
+
if (
|
|
1807
|
+
!parentComponent.components.some(
|
|
1808
|
+
(c) => c["bom-ref"] === sub["bom-ref"],
|
|
1809
|
+
) &&
|
|
1810
|
+
sub["bom-ref"] !== parentComponent["bom-ref"]
|
|
1811
|
+
) {
|
|
1812
|
+
parentComponent.components.push(sub);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
// with no project at the scan root there is nothing to nest them under
|
|
1817
|
+
pkgList = pkgList.concat(subProjects);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (parentComponent && buildContext.parentProperties.length) {
|
|
1821
|
+
parentComponent.properties = (parentComponent.properties || []).filter(
|
|
1822
|
+
(p) =>
|
|
1823
|
+
!buildContext.parentProperties.some(
|
|
1824
|
+
(bp) => bp.name === p.name && bp.value === p.value,
|
|
1825
|
+
),
|
|
1826
|
+
);
|
|
1827
|
+
parentComponent.properties.push(...buildContext.parentProperties);
|
|
1828
|
+
}
|
|
1721
1829
|
if (parentComponent) {
|
|
1722
1830
|
sanitizeUnresolvedPurl(parentComponent);
|
|
1723
1831
|
if (parentComponent.components) {
|
|
@@ -1734,6 +1842,7 @@ export function createCppBom(path, options) {
|
|
|
1734
1842
|
src: path,
|
|
1735
1843
|
parentComponent,
|
|
1736
1844
|
dependencies,
|
|
1845
|
+
formulationList: buildContext.formulationComponents,
|
|
1737
1846
|
});
|
|
1738
1847
|
}
|
|
1739
1848
|
|
|
@@ -2328,7 +2437,7 @@ export async function createCocoaBom(path, options) {
|
|
|
2328
2437
|
"application",
|
|
2329
2438
|
);
|
|
2330
2439
|
const podfileLock = loadYaml(readFileSync(lockFile, "utf-8"));
|
|
2331
|
-
const pods = await parsePodfileLock(podfileLock, projectPath);
|
|
2440
|
+
const pods = await parsePodfileLock(podfileLock, projectPath, path);
|
|
2332
2441
|
const allObjects = new Map();
|
|
2333
2442
|
if (options) {
|
|
2334
2443
|
// Every remote podspec the scan below reads is known from the lockfile,
|
package/lib/core/activity.js
CHANGED
|
@@ -285,6 +285,18 @@ const MANIFEST_ACTIVITY_HINTS = new Map([
|
|
|
285
285
|
],
|
|
286
286
|
]);
|
|
287
287
|
const SENSITIVE_CONFIG_ACTIVITY_HINTS = [
|
|
288
|
+
{
|
|
289
|
+
matcher: (_lowerPath, baseName) =>
|
|
290
|
+
baseName === ".terraformrc" ||
|
|
291
|
+
baseName === "terraform.rc" ||
|
|
292
|
+
baseName === "credentials.tfrc.json",
|
|
293
|
+
metadata: {
|
|
294
|
+
classification: "credential",
|
|
295
|
+
ecosystem: "terraform",
|
|
296
|
+
label: "Terraform CLI configuration",
|
|
297
|
+
sensitive: true,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
288
300
|
{
|
|
289
301
|
matcher: (lowerPath, _baseName) =>
|
|
290
302
|
lowerPath.includes("/.cargo/config.toml") ||
|
package/lib/core/env.js
CHANGED
|
@@ -111,19 +111,27 @@ export function shouldFetchLicense() {
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Determines whether registry provenance, such as publish times, publishers
|
|
116
|
+
* and trusted-publishing signals, is wanted. `--bom-audit` turns it on through
|
|
117
|
+
* CDXGEN_FETCH_PKG_METADATA. Local data cannot answer these questions, so a
|
|
118
|
+
* package whose licence and description are known locally is still looked up.
|
|
119
|
+
*
|
|
120
|
+
* @returns {boolean} True if CDXGEN_FETCH_PKG_METADATA is "true" or "1"
|
|
121
|
+
*/
|
|
122
|
+
export function shouldFetchRegistryProvenance() {
|
|
123
|
+
return ["true", "1"].includes(
|
|
124
|
+
readEnvironmentVariable("CDXGEN_FETCH_PKG_METADATA"),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
/**
|
|
115
129
|
* Determines whether remote package metadata should be fetched for enrichment.
|
|
116
130
|
*
|
|
117
131
|
* @returns {boolean} True when registry metadata enrichment is enabled.
|
|
118
132
|
*/
|
|
119
133
|
export function shouldFetchPackageMetadata() {
|
|
120
|
-
return (
|
|
121
|
-
shouldFetchLicense() ||
|
|
122
|
-
(readEnvironmentVariable("CDXGEN_FETCH_PKG_METADATA") &&
|
|
123
|
-
["true", "1"].includes(
|
|
124
|
-
readEnvironmentVariable("CDXGEN_FETCH_PKG_METADATA"),
|
|
125
|
-
))
|
|
126
|
-
);
|
|
134
|
+
return shouldFetchLicense() || shouldFetchRegistryProvenance();
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
/**
|
|
@@ -457,7 +465,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
457
465
|
perl: ["perl", "cpan", "carton", "cpanfile"],
|
|
458
466
|
spack: ["spack"],
|
|
459
467
|
terraform: ["terraform", "opentofu", "tf"],
|
|
460
|
-
c: ["c", "cpp", "c++", "conan", "collider"],
|
|
468
|
+
c: ["c", "cpp", "c++", "conan", "collider", "cmake", "meson", "vcpkg"],
|
|
461
469
|
clojure: ["clojure", "edn", "clj", "leiningen"],
|
|
462
470
|
github: ["github", "actions"],
|
|
463
471
|
hbom: ["hbom", "hardware"],
|
|
@@ -493,7 +501,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
493
501
|
binary: ["binary", "blint"],
|
|
494
502
|
oci: ["docker", "image", "oci", "container", "podman", "rootfs", "oci-dir"],
|
|
495
503
|
cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
|
|
496
|
-
scala: ["scala", "scala3", "sbt", "mill"],
|
|
504
|
+
scala: ["scala", "scala3", "sbt", "mill", "scala-cli"],
|
|
497
505
|
nix: ["nix", "nixos", "flake"],
|
|
498
506
|
zig: ["zig", "zon"],
|
|
499
507
|
gleam: ["gleam"],
|
package/lib/core/fs.js
CHANGED
|
@@ -577,7 +577,7 @@ export function shellQuoteArgument(arg, forWindows = isWin) {
|
|
|
577
577
|
backslashes++;
|
|
578
578
|
quoted += ch;
|
|
579
579
|
} else if (ch === '"') {
|
|
580
|
-
quoted += "\\".repeat(backslashes)
|
|
580
|
+
quoted += `${"\\".repeat(backslashes)}\\"`;
|
|
581
581
|
backslashes = 0;
|
|
582
582
|
} else {
|
|
583
583
|
quoted += ch;
|
|
@@ -1025,7 +1025,8 @@ export const MAX_BUFFER =
|
|
|
1025
1025
|
export function getAllFiles(dirPath, pattern, options = {}) {
|
|
1026
1026
|
let ignoreList = [];
|
|
1027
1027
|
if (!options.noIgnore) {
|
|
1028
|
-
|
|
1028
|
+
// .chen holds chen's AST cache, which atom writes into a C/C++ source tree
|
|
1029
|
+
ignoreList.push("**/.hg/**", "**/.git/**", "**/.chen/**");
|
|
1029
1030
|
// Only ignore node_modules if the caller is not looking for package.json
|
|
1030
1031
|
if (!pattern.includes("package.json") && !options.includeNodeModulesDir) {
|
|
1031
1032
|
ignoreList.push("**/node_modules/**");
|
package/lib/core/httpClient.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
|
|
4
5
|
|
|
@@ -9,6 +10,8 @@ import {
|
|
|
9
10
|
request as undiciRequest,
|
|
10
11
|
} from "undici";
|
|
11
12
|
|
|
13
|
+
import { registerRunReset } from "./runState.js";
|
|
14
|
+
|
|
12
15
|
// Environment variables here are read with raw `process.env` rather than
|
|
13
16
|
// `readEnvironmentVariable`: that helper lives in activity.js, which imports
|
|
14
17
|
// this module, so routing these through it would form an import cycle.
|
|
@@ -41,9 +44,8 @@ import {
|
|
|
41
44
|
* `Content-Encoding` header, matching `got`'s transparent decoding.
|
|
42
45
|
* - Proxy support via the standard `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`
|
|
43
46
|
* environment variables (through undici's `EnvHttpProxyAgent`).
|
|
44
|
-
* - An in-memory GET response cache
|
|
45
|
-
*
|
|
46
|
-
* cache cdxgen previously relied on.
|
|
47
|
+
* - An in-memory GET response cache that replaces the got + Keyv cache cdxgen
|
|
48
|
+
* previously relied on. A request opts out with `cache: false`.
|
|
47
49
|
* - Response objects exposing `statusCode`, `headers`, `body`, `rawBody`,
|
|
48
50
|
* `url` and `request.options`.
|
|
49
51
|
* - A lazily-resolved `.json()` method on the returned promise, mirroring
|
|
@@ -137,7 +139,12 @@ function mergeHeaders(base = {}, override = {}) {
|
|
|
137
139
|
* `beforeError` arrays.
|
|
138
140
|
*/
|
|
139
141
|
function mergeHooks(base = {}, override = {}) {
|
|
140
|
-
const hookNames = [
|
|
142
|
+
const hookNames = [
|
|
143
|
+
"beforeRequest",
|
|
144
|
+
"beforeNetwork",
|
|
145
|
+
"afterResponse",
|
|
146
|
+
"beforeError",
|
|
147
|
+
];
|
|
141
148
|
const merged = {};
|
|
142
149
|
for (const name of hookNames) {
|
|
143
150
|
merged[name] = [...(base?.[name] || []), ...(override?.[name] || [])];
|
|
@@ -253,6 +260,11 @@ function decompressBody(rawBody, contentEncoding) {
|
|
|
253
260
|
* Read and decode an undici response body according to the requested
|
|
254
261
|
* `responseType`.
|
|
255
262
|
*
|
|
263
|
+
* An error response often carries a body in another format than the one asked
|
|
264
|
+
* for, such as the HTML page a CDN sends with a 429. Its JSON body is decoded
|
|
265
|
+
* when it parses and kept as text otherwise, so the caller sees the status
|
|
266
|
+
* rather than a parse error.
|
|
267
|
+
*
|
|
256
268
|
* @param {Object} undiciResponse Response returned by `undici.request`.
|
|
257
269
|
* @param {string} [responseType] One of `"json"`, `"buffer"` or `"text"`.
|
|
258
270
|
* @returns {Promise<{body: any, rawBody: Buffer}>} Decoded body and raw bytes.
|
|
@@ -272,7 +284,17 @@ async function readBody(undiciResponse, responseType) {
|
|
|
272
284
|
body = rawBody;
|
|
273
285
|
} else if (responseType === "json") {
|
|
274
286
|
const text = rawBody.toString("utf-8");
|
|
275
|
-
|
|
287
|
+
if (!text.length) {
|
|
288
|
+
body = undefined;
|
|
289
|
+
} else if (undiciResponse.statusCode >= 400) {
|
|
290
|
+
try {
|
|
291
|
+
body = JSON.parse(text);
|
|
292
|
+
} catch (_err) {
|
|
293
|
+
body = text;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
body = JSON.parse(text);
|
|
297
|
+
}
|
|
276
298
|
} else {
|
|
277
299
|
body = rawBody.toString("utf-8");
|
|
278
300
|
}
|
|
@@ -304,20 +326,100 @@ function resolveRequestBody(options) {
|
|
|
304
326
|
|
|
305
327
|
// In-memory cache of successful GET responses, mirroring the HTTP cache cdxgen
|
|
306
328
|
// historically kept via got + Keyv. Repeated metadata/license lookups during a
|
|
307
|
-
// scan are served from here instead of hitting the network again.
|
|
308
|
-
// CDXGEN_NO_CACHE
|
|
329
|
+
// scan are served from here instead of hitting the network again. It lasts for
|
|
330
|
+
// one run; `CDXGEN_NO_CACHE` and `--no-cache` turn off only the on-disk caches,
|
|
331
|
+
// because nothing gains from asking a registry the same question twice in one
|
|
332
|
+
// scan.
|
|
333
|
+
//
|
|
334
|
+
// Entries expire after an hour and the cache is bounded in size, least
|
|
335
|
+
// recently used first, so a long-running server neither serves stale registry
|
|
336
|
+
// documents nor grows without limit.
|
|
309
337
|
const responseCache = new Map();
|
|
338
|
+
const RESPONSE_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
339
|
+
const RESPONSE_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
340
|
+
let responseCacheLimits = {
|
|
341
|
+
ttlMs: RESPONSE_CACHE_TTL_MS,
|
|
342
|
+
maxBytes: RESPONSE_CACHE_MAX_BYTES,
|
|
343
|
+
};
|
|
344
|
+
let responseCacheBytes = 0;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Approximate the memory a cached response holds. The raw bytes are kept,
|
|
348
|
+
* plus the decoded body for text and JSON responses.
|
|
349
|
+
*
|
|
350
|
+
* @param {Object} response Response object.
|
|
351
|
+
* @returns {number} Size in bytes.
|
|
352
|
+
*/
|
|
353
|
+
function cachedResponseBytes(response) {
|
|
354
|
+
const raw = response?.rawBody?.length || 0;
|
|
355
|
+
return response?.body === response?.rawBody ? raw : raw * 3;
|
|
356
|
+
}
|
|
310
357
|
|
|
311
358
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* callers can toggle it at runtime.
|
|
359
|
+
* Read a cached response, dropping it when it has expired and marking it as
|
|
360
|
+
* recently used otherwise.
|
|
315
361
|
*
|
|
316
|
-
* @
|
|
362
|
+
* @param {string} key Cache key.
|
|
363
|
+
* @returns {Object|undefined} The cached response.
|
|
317
364
|
*/
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
365
|
+
function cacheGet(key) {
|
|
366
|
+
const entry = responseCache.get(key);
|
|
367
|
+
if (!entry) {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
responseCache.delete(key);
|
|
371
|
+
if (entry.expiresAt <= Date.now()) {
|
|
372
|
+
responseCacheBytes -= entry.bytes;
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
responseCache.set(key, entry);
|
|
376
|
+
return entry.response;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Store a response, evicting the least recently used entries beyond the size
|
|
381
|
+
* bound.
|
|
382
|
+
*
|
|
383
|
+
* @param {string} key Cache key.
|
|
384
|
+
* @param {Object} response Response object.
|
|
385
|
+
* @returns {void}
|
|
386
|
+
*/
|
|
387
|
+
function cacheSet(key, response) {
|
|
388
|
+
const bytes = cachedResponseBytes(response);
|
|
389
|
+
if (bytes > responseCacheLimits.maxBytes) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const previous = responseCache.get(key);
|
|
393
|
+
if (previous) {
|
|
394
|
+
responseCache.delete(key);
|
|
395
|
+
responseCacheBytes -= previous.bytes;
|
|
396
|
+
}
|
|
397
|
+
responseCache.set(key, {
|
|
398
|
+
response,
|
|
399
|
+
bytes,
|
|
400
|
+
expiresAt: Date.now() + responseCacheLimits.ttlMs,
|
|
401
|
+
});
|
|
402
|
+
responseCacheBytes += bytes;
|
|
403
|
+
for (const [oldestKey, oldest] of responseCache) {
|
|
404
|
+
if (responseCacheBytes <= responseCacheLimits.maxBytes) {
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
responseCache.delete(oldestKey);
|
|
408
|
+
responseCacheBytes -= oldest.bytes;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Override the response cache bounds. Returns the previous bounds so tests can
|
|
414
|
+
* restore them. Not part of the public API.
|
|
415
|
+
*
|
|
416
|
+
* @param {{ttlMs?: number, maxBytes?: number}} limits New bounds.
|
|
417
|
+
* @returns {{ttlMs: number, maxBytes: number}} Previous bounds.
|
|
418
|
+
*/
|
|
419
|
+
export function _setHttpCacheLimits(limits) {
|
|
420
|
+
const previous = responseCacheLimits;
|
|
421
|
+
responseCacheLimits = { ...responseCacheLimits, ...limits };
|
|
422
|
+
return previous;
|
|
321
423
|
}
|
|
322
424
|
|
|
323
425
|
/**
|
|
@@ -327,8 +429,11 @@ export function isCacheDisabled() {
|
|
|
327
429
|
*/
|
|
328
430
|
export function clearHttpCache() {
|
|
329
431
|
responseCache.clear();
|
|
432
|
+
responseCacheBytes = 0;
|
|
330
433
|
}
|
|
331
434
|
|
|
435
|
+
registerRunReset("http:responses", clearHttpCache);
|
|
436
|
+
|
|
332
437
|
// ---------------------------------------------------------------------------
|
|
333
438
|
// Test cassette interceptor — the single network seam.
|
|
334
439
|
//
|
|
@@ -397,15 +502,33 @@ export function clearHttpInterceptor() {
|
|
|
397
502
|
}
|
|
398
503
|
|
|
399
504
|
/**
|
|
400
|
-
* Build the cache key for a request.
|
|
401
|
-
*
|
|
505
|
+
* Build the cache key for a request.
|
|
506
|
+
*
|
|
507
|
+
* Besides the method and URL, the key carries the response type, which
|
|
508
|
+
* changes how the same bytes are decoded, and a digest of every request
|
|
509
|
+
* header. A response fetched with one credential is therefore never served to
|
|
510
|
+
* a request made with another credential or with none, and a JSON request
|
|
511
|
+
* never receives a text body. Header values only enter the key as a SHA-256
|
|
512
|
+
* digest, so no credential is held in it.
|
|
402
513
|
*
|
|
403
514
|
* @param {string} method Uppercased HTTP method.
|
|
404
515
|
* @param {URL} url Request URL.
|
|
516
|
+
* @param {Object} [headers] Request headers as sent.
|
|
517
|
+
* @param {string} [responseType] Requested body decoding.
|
|
405
518
|
* @returns {string} Cache key.
|
|
406
519
|
*/
|
|
407
|
-
function cacheKeyFor(method, url) {
|
|
408
|
-
|
|
520
|
+
function cacheKeyFor(method, url, headers = {}, responseType = undefined) {
|
|
521
|
+
const headerEntries = Object.entries(headers || {})
|
|
522
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
523
|
+
.map(([name, value]) => [
|
|
524
|
+
name.toLowerCase(),
|
|
525
|
+
Array.isArray(value) ? value.join(", ") : String(value),
|
|
526
|
+
])
|
|
527
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
528
|
+
const headerDigest = headerEntries.length
|
|
529
|
+
? createHash("sha256").update(JSON.stringify(headerEntries)).digest("hex")
|
|
530
|
+
: "";
|
|
531
|
+
return `${method}:${responseType || "text"}:${url.toString()}:${headerDigest}`;
|
|
409
532
|
}
|
|
410
533
|
|
|
411
534
|
// The maximum number of redirects to follow when `followRedirect` is enabled.
|
|
@@ -587,11 +710,23 @@ async function doRequest(mergedOptions) {
|
|
|
587
710
|
const { body, headers } = resolveRequestBody(options);
|
|
588
711
|
const method = (options.method || "GET").toUpperCase();
|
|
589
712
|
|
|
590
|
-
// Serve cacheable GET requests from the in-memory cache when enabled.
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
713
|
+
// Serve cacheable GET requests from the in-memory cache when enabled. A
|
|
714
|
+
// request opts out with `cache: false`, as token exchanges do.
|
|
715
|
+
const cacheable = method === "GET" && options.cache !== false;
|
|
716
|
+
const cacheKey = cacheable
|
|
717
|
+
? cacheKeyFor(method, url, headers, options.responseType)
|
|
718
|
+
: undefined;
|
|
719
|
+
if (cacheable) {
|
|
720
|
+
const cached = cacheGet(cacheKey);
|
|
721
|
+
if (cached) {
|
|
722
|
+
return cached;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Runs only when the request is about to reach the network, so a caller
|
|
727
|
+
// that rate-limits a host does not wait for a response already cached.
|
|
728
|
+
for (const hook of options.hooks?.beforeNetwork || []) {
|
|
729
|
+
await hook(options);
|
|
595
730
|
}
|
|
596
731
|
|
|
597
732
|
const timeoutMs = resolveTimeout(options.timeout);
|
|
@@ -652,7 +787,7 @@ async function doRequest(mergedOptions) {
|
|
|
652
787
|
|
|
653
788
|
// Only successful responses are cached, matching HTTP cache semantics.
|
|
654
789
|
if (cacheable && finalResponse.statusCode < 400) {
|
|
655
|
-
|
|
790
|
+
cacheSet(cacheKey, finalResponse);
|
|
656
791
|
}
|
|
657
792
|
return finalResponse;
|
|
658
793
|
}
|