@cdxgen/cdxgen 13.0.1 → 13.2.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 +30 -16
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +141 -7
- package/bin/evinse.js +3 -1
- package/data/component-tags.json +8 -1
- package/data/crypto-oid.json +88 -12
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1167 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +32 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +268 -49
- package/lib/cli/jsBom.js +91 -5
- package/lib/cli/jvmBom.js +1515 -1314
- package/lib/cli/managedBom.js +249 -28
- package/lib/cli/nativeBom.js +986 -10
- package/lib/core/activity.js +120 -2
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +108 -2
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +250 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +498 -17
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +188 -2
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +172 -36
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +944 -19
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +141 -13
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +17 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +508 -22
- package/lib/inventory/envcontext.js +215 -24
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +131 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +594 -0
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +250 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +284 -3
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +26 -26
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +10 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- 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 +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +43 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +53 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +21 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +34 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +12 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +43 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +43 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +35 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build ledger: an opt-in, worker-safe recorder of structured build-adequacy
|
|
3
|
+
* events.
|
|
4
|
+
*
|
|
5
|
+
* Producers record what a run did to its build environment — the tool versions
|
|
6
|
+
* a project expected, the tools that were actually found, the commands that
|
|
7
|
+
* failed, the fallback code paths that were taken — and the post-generation
|
|
8
|
+
* reflection turns those events into a fidelity verdict. The recorder follows
|
|
9
|
+
* the shape of `activity.js` but stays a separate stream: activity records
|
|
10
|
+
* describe security-relevant side effects consumed by the secure-mode
|
|
11
|
+
* machinery, while ledger events describe environment adequacy consumed by the
|
|
12
|
+
* introspection report.
|
|
13
|
+
*
|
|
14
|
+
* Two facts about workers shape the design:
|
|
15
|
+
*
|
|
16
|
+
* 1. A worker thread gets its own instance of this module, so events recorded
|
|
17
|
+
* inside a worker are invisible to the main thread's in-memory buffer.
|
|
18
|
+
* When `CDXGEN_INTROSPECT_LEDGER` is set, every instance appends its events
|
|
19
|
+
* to the same JSONL sidecar through an `O_APPEND` descriptor (only the main
|
|
20
|
+
* thread truncates, at open), which makes the sidecar the only complete
|
|
21
|
+
* record of a multi-worker run. Consumers must therefore prefer the sidecar
|
|
22
|
+
* when it exists and fall back to `getLedgerEvents()` only when it does not.
|
|
23
|
+
* 2. Every write reaches the descriptor synchronously at record time, so a
|
|
24
|
+
* lost tail is impossible; the descriptor is released when the process
|
|
25
|
+
* exits (`closeLogStreams` closes every shared writer) or earlier through
|
|
26
|
+
* {@link closeLedger}.
|
|
27
|
+
*
|
|
28
|
+
* Values that could carry credentials are scrubbed before they are stored or
|
|
29
|
+
* written: `command`, `path`, `detail`, `causeDetail` and `outputExcerpt` pass
|
|
30
|
+
* through sensitive-assignment redaction plus the BOM property sanitizer, so a
|
|
31
|
+
* leaked token can never reach a report file that agents upload.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync } from "node:fs";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
isSensitiveEnvironmentVariableName,
|
|
40
|
+
readEnvironmentVariable,
|
|
41
|
+
} from "./activity.js";
|
|
42
|
+
import { safeReadFileSync } from "./fs.js";
|
|
43
|
+
import { thoughtLog } from "./logger.js";
|
|
44
|
+
import { dirNameStr } from "./paths.js";
|
|
45
|
+
import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
|
|
46
|
+
import { openSyncFileWriter } from "./syncFileWriter.js";
|
|
47
|
+
|
|
48
|
+
/** Longest command-output excerpt an event keeps, taken from the tail: build
|
|
49
|
+
* tools print the banner first and the error last. */
|
|
50
|
+
const OUTPUT_EXCERPT_LIMIT = 2000;
|
|
51
|
+
|
|
52
|
+
/** Slots an excerpt-bearing event charges against the in-memory event cap. */
|
|
53
|
+
const EXCERPT_EVENT_WEIGHT = 2;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A structured build-adequacy event.
|
|
57
|
+
*
|
|
58
|
+
* @typedef {Object} LedgerEvent
|
|
59
|
+
* @property {string} kind One of LEDGER_EVENT_KINDS.
|
|
60
|
+
* @property {string} ecosystem Canonical project type ("java", "npm", "python", …).
|
|
61
|
+
* @property {string} [tool] Tool identity ("maven", "gradle", "swift", "dotnet").
|
|
62
|
+
* @property {string} [wanted] Version or version range the project asked for.
|
|
63
|
+
* @property {string} [found] Version actually resolved, or undefined when absent.
|
|
64
|
+
* @property {string} [source] How `found` was determined ("PATH", "wrapper", "sdkman", "env").
|
|
65
|
+
* @property {string} [path] Absolute path of the resolved tool or the file that was read.
|
|
66
|
+
* @property {string} [command] Command line, already redacted.
|
|
67
|
+
* @property {number} [exitCode]
|
|
68
|
+
* @property {string} [detail] One sentence, present tense, no remediation text.
|
|
69
|
+
* @property {string} [causeDetail] Short diagnosed cause of a failure ("gradle cannot run on Java 21"), when the producer derived one.
|
|
70
|
+
* @property {string} [outputExcerpt] Redacted, at most the last 2000 characters of the failed command's combined stdout and stderr, with interior newlines preserved. Suppressed entirely by `CDXGEN_INTROSPECT_NO_OUTPUT`.
|
|
71
|
+
* @property {string} [remediationId] Key into the remediation catalog (Deliverable 06).
|
|
72
|
+
* @property {string} [impact] One of LEDGER_EVENT_IMPACTS.
|
|
73
|
+
* @property {string} timestamp ISO 8601.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The closed set of event kinds. `kind` is validated against this object: a
|
|
78
|
+
* kind that is not listed is dropped (with a think-log note) rather than
|
|
79
|
+
* recorded, so a producer bug can never fail a customer's SBOM run. Adding a
|
|
80
|
+
* kind here is a schema change; consumers must tolerate unknown kinds by
|
|
81
|
+
* ignoring them.
|
|
82
|
+
*
|
|
83
|
+
* @type {Readonly<Record<string, string>>}
|
|
84
|
+
*/
|
|
85
|
+
export const LEDGER_EVENT_KINDS = Object.freeze({
|
|
86
|
+
TOOL_EXPECTED: "tool.expected",
|
|
87
|
+
TOOL_RESOLVED: "tool.resolved",
|
|
88
|
+
TOOL_MISSING: "tool.missing",
|
|
89
|
+
TOOL_MISMATCH: "tool.mismatch",
|
|
90
|
+
COMMAND_ATTEMPTED: "command.attempted",
|
|
91
|
+
COMMAND_FAILED: "command.failed",
|
|
92
|
+
FALLBACK_ENGAGED: "fallback.engaged",
|
|
93
|
+
EVIDENCE_DEGRADED: "evidence.degraded",
|
|
94
|
+
LIFECYCLE_CLAIMED: "lifecycle.claimed",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const KNOWN_LEDGER_EVENT_KINDS = new Set(Object.values(LEDGER_EVENT_KINDS));
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The closed set of `impact` values, describing what a degraded outcome costs
|
|
101
|
+
* the fidelity of the generated BOM.
|
|
102
|
+
*
|
|
103
|
+
* @type {Readonly<Record<string, string>>}
|
|
104
|
+
*/
|
|
105
|
+
export const LEDGER_EVENT_IMPACTS = Object.freeze({
|
|
106
|
+
TRANSITIVE_DEPS: "transitive-deps",
|
|
107
|
+
VERSIONS: "versions",
|
|
108
|
+
INTEGRITY: "integrity",
|
|
109
|
+
LICENSES: "licenses",
|
|
110
|
+
COMPONENTS: "components",
|
|
111
|
+
NONE: "none",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reserved `ecosystem` value for events that describe the recorder itself
|
|
116
|
+
* rather than a project ecosystem, such as the truncation marker. Consumers
|
|
117
|
+
* that group events by ecosystem must treat it as a global observation instead
|
|
118
|
+
* of creating an ecosystem entry for it.
|
|
119
|
+
*
|
|
120
|
+
* @type {string}
|
|
121
|
+
*/
|
|
122
|
+
export const LEDGER_TOOL_ECOSYSTEM = "cdxgen";
|
|
123
|
+
|
|
124
|
+
/** Default cap on the in-memory event buffer. */
|
|
125
|
+
const DEFAULT_MAX_LEDGER_EVENTS = 10000;
|
|
126
|
+
/** Smallest usable cap: below this the keep-first-half/keep-last-half split degenerates. */
|
|
127
|
+
const MIN_MAX_LEDGER_EVENTS = 10;
|
|
128
|
+
/** Largest accepted cap; a larger value would be a memory hazard rather than a configuration. */
|
|
129
|
+
const MAX_MAX_LEDGER_EVENTS = 1000000;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Module-load snapshot of the build-introspection environment: `CDXGEN_INTROSPECT`
|
|
133
|
+
* is `true`/`1`, or `CDXGEN_INTROSPECT_LEDGER` names a sidecar file. Library
|
|
134
|
+
* callers that configure introspection before importing this module can read
|
|
135
|
+
* it directly; the CLI enables introspection after argument parsing, so every
|
|
136
|
+
* internal gate reads {@link isLedgerEnabled}, which resolves the same
|
|
137
|
+
* environment on each call.
|
|
138
|
+
*
|
|
139
|
+
* @type {boolean}
|
|
140
|
+
*/
|
|
141
|
+
export const LEDGER_ENABLED =
|
|
142
|
+
["true", "1"].includes(readEnvironmentVariable("CDXGEN_INTROSPECT")) ||
|
|
143
|
+
Boolean(readEnvironmentVariable("CDXGEN_INTROSPECT_LEDGER"));
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* True when build-introspection recording is enabled for this run: the same
|
|
147
|
+
* environment as {@link LEDGER_ENABLED}, read live. The CLI turns
|
|
148
|
+
* introspection on after argument parsing by setting `CDXGEN_INTROSPECT`, so
|
|
149
|
+
* a module-load snapshot would miss every event recorded after that point;
|
|
150
|
+
* the environment only ever flips in that one direction during a run.
|
|
151
|
+
*
|
|
152
|
+
* @returns {boolean} True when events should be recorded.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* Whether build introspection is enabled for this run. The flag decides when
|
|
156
|
+
* it was given; otherwise the environment speaks, which is how a run that
|
|
157
|
+
* enabled introspection through a profile reaches the recorders.
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} [options] CLI options.
|
|
160
|
+
* @returns {boolean} True when the run records and reports a verdict.
|
|
161
|
+
*/
|
|
162
|
+
export function isIntrospectionEnabled(options) {
|
|
163
|
+
if (typeof options?.introspect === "boolean") {
|
|
164
|
+
return options.introspect;
|
|
165
|
+
}
|
|
166
|
+
return ["true", "1"].includes(readEnvironmentVariable("CDXGEN_INTROSPECT"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isLedgerEnabled() {
|
|
170
|
+
return (
|
|
171
|
+
["true", "1"].includes(readEnvironmentVariable("CDXGEN_INTROSPECT")) ||
|
|
172
|
+
Boolean(readEnvironmentVariable("CDXGEN_INTROSPECT_LEDGER"))
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolve the in-memory event cap from `CDXGEN_INTROSPECT_MAX_EVENTS`.
|
|
178
|
+
* Non-numeric values fall back to the default; the accepted range is clamped.
|
|
179
|
+
*
|
|
180
|
+
* @param {string|undefined} rawValue Raw environment variable value.
|
|
181
|
+
* @returns {number} Effective event cap.
|
|
182
|
+
*/
|
|
183
|
+
function resolveMaxLedgerEvents(rawValue) {
|
|
184
|
+
if (rawValue === undefined || rawValue === "") {
|
|
185
|
+
return DEFAULT_MAX_LEDGER_EVENTS;
|
|
186
|
+
}
|
|
187
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
188
|
+
if (Number.isNaN(parsed)) {
|
|
189
|
+
return DEFAULT_MAX_LEDGER_EVENTS;
|
|
190
|
+
}
|
|
191
|
+
return Math.min(
|
|
192
|
+
Math.max(parsed, MIN_MAX_LEDGER_EVENTS),
|
|
193
|
+
MAX_MAX_LEDGER_EVENTS,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const MAX_LEDGER_EVENTS = resolveMaxLedgerEvents(
|
|
198
|
+
readEnvironmentVariable("CDXGEN_INTROSPECT_MAX_EVENTS"),
|
|
199
|
+
);
|
|
200
|
+
/** Number of leading events never evicted once truncation begins. */
|
|
201
|
+
const LEDGER_HEAD_KEEP = Math.floor(MAX_LEDGER_EVENTS / 2);
|
|
202
|
+
|
|
203
|
+
// The sidecar writer opens lazily on the first recorded event rather than at
|
|
204
|
+
// module load, because the CLI can name a sidecar (its own automatic temp
|
|
205
|
+
// sidecar or the user's) only after argument parsing. The first open still
|
|
206
|
+
// truncates once on the main thread, and every worker instance appends to the
|
|
207
|
+
// same path. A path that cannot be opened leaves the writer null and the run
|
|
208
|
+
// in-memory-only; JSONL lines are never allowed to spill into the diagnostic
|
|
209
|
+
// stream.
|
|
210
|
+
const LEDGER_WRITER_UNRESOLVED = Symbol("unresolved");
|
|
211
|
+
let ledgerFileWriter = LEDGER_WRITER_UNRESOLVED;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resolve (once) and return the sidecar writer for this thread, or null when
|
|
215
|
+
* no sidecar is configured or the path could not be opened.
|
|
216
|
+
*
|
|
217
|
+
* @returns {Writable|null} The sidecar writer, or null.
|
|
218
|
+
*/
|
|
219
|
+
function resolveLedgerWriter() {
|
|
220
|
+
if (ledgerFileWriter === LEDGER_WRITER_UNRESOLVED) {
|
|
221
|
+
const ledgerFilePath = readEnvironmentVariable("CDXGEN_INTROSPECT_LEDGER");
|
|
222
|
+
ledgerFileWriter = ledgerFilePath
|
|
223
|
+
? openSyncFileWriter(ledgerFilePath, null)
|
|
224
|
+
: null;
|
|
225
|
+
}
|
|
226
|
+
return ledgerFileWriter || null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const ledgerEvents = [];
|
|
230
|
+
let truncationRecorded = false;
|
|
231
|
+
let ledgerFileClosed = false;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Credential-name fragments that make a compound name sensitive even when no
|
|
235
|
+
* underscore separates the parts (`PGPASSWORD`, `registryToken`).
|
|
236
|
+
*
|
|
237
|
+
* @type {RegExp}
|
|
238
|
+
*/
|
|
239
|
+
const COMPOUND_CREDENTIAL_NAME_PATTERN =
|
|
240
|
+
/(?:token|secret|pass(?:word)?|passwd|credential)/i;
|
|
241
|
+
|
|
242
|
+
/** Short flags whose value is a credential in build-tool command lines. */
|
|
243
|
+
const SENSITIVE_SHORT_FLAGS = new Set(["-p"]);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Test whether an assignment name (the part before `=` in a command argument)
|
|
247
|
+
* looks like a credential carrier. Leading flag dashes are stripped, hyphens
|
|
248
|
+
* are normalised to underscores for the existing sensitive-name pattern, a
|
|
249
|
+
* leading JVM-style property letter (`-Dpassword=…`) is also tried, and a
|
|
250
|
+
* compound name whose parts run together (`PGPASSWORD=…`) is caught by the
|
|
251
|
+
* fragment pattern.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} name Assignment name as it appeared in the command.
|
|
254
|
+
* @returns {boolean} True when the name matches the sensitive-name pattern.
|
|
255
|
+
*/
|
|
256
|
+
function isSensitiveAssignmentName(name) {
|
|
257
|
+
const normalisedName = name.replace(/^-+/, "").replaceAll("-", "_");
|
|
258
|
+
return (
|
|
259
|
+
isSensitiveEnvironmentVariableName(normalisedName) ||
|
|
260
|
+
isSensitiveEnvironmentVariableName(normalisedName.slice(1)) ||
|
|
261
|
+
COMPOUND_CREDENTIAL_NAME_PATTERN.test(normalisedName)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Replace the value of every credential-bearing argument with a `[redacted]`
|
|
267
|
+
* marker, preserving all other arguments and whitespace. Both spellings are
|
|
268
|
+
* covered: the assignment form (`--password=x`, `-Dpassword=x`, `PGPASSWORD=x`)
|
|
269
|
+
* redacts within the token itself, and the space-separated flag form
|
|
270
|
+
* (`--password x`, `-p x`, `--registry-token x`) redacts the token that
|
|
271
|
+
* follows the flag.
|
|
272
|
+
*
|
|
273
|
+
* The assignment branch keeps everything after `=` as the value, so a value
|
|
274
|
+
* that runs into trailing punctuation (`FOO=bar)`) loses the punctuation with
|
|
275
|
+
* the value; that is the pre-existing shape of this redactor and is not
|
|
276
|
+
* widened here.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} text Command line or path to scrub.
|
|
279
|
+
* @returns {string} Text with sensitive assignment and flag values replaced.
|
|
280
|
+
*/
|
|
281
|
+
function redactSensitiveAssignments(text) {
|
|
282
|
+
const pieces = text.split(/(\s+)/);
|
|
283
|
+
for (let index = 0; index < pieces.length; index += 2) {
|
|
284
|
+
const piece = pieces[index];
|
|
285
|
+
const separator = piece.indexOf("=");
|
|
286
|
+
if (separator > 0) {
|
|
287
|
+
if (isSensitiveAssignmentName(piece.slice(0, separator))) {
|
|
288
|
+
pieces[index] = `${piece.slice(0, separator)}=[redacted]`;
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
(piece.startsWith("--") && isSensitiveAssignmentName(piece)) ||
|
|
294
|
+
SENSITIVE_SHORT_FLAGS.has(piece)
|
|
295
|
+
) {
|
|
296
|
+
const valueIndex = index + 2;
|
|
297
|
+
if (pieces[valueIndex] !== undefined) {
|
|
298
|
+
pieces[valueIndex] = "[redacted]";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return pieces.join("");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Credential-bearing headers whose value is a credential, echoed by verbose
|
|
307
|
+
* HTTP clients and CI logs. The value runs to the end of the line because a
|
|
308
|
+
* scheme word (`Basic`, `Bearer`) and the token behind it are one value. The
|
|
309
|
+
* header name survives so the output stays diagnosable.
|
|
310
|
+
*
|
|
311
|
+
* @type {RegExp}
|
|
312
|
+
*/
|
|
313
|
+
const AUTHORIZATION_HEADER_PATTERN =
|
|
314
|
+
/((?:x-)?(?:proxy-)?authorization|cookie)(\s*:\s*)(("[^"]*"|'[^']*')|[^\r\n]*)/giu;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* A value of any length behind a strong credential name and an unambiguous
|
|
318
|
+
* `:` or `=` separator. A bare name followed only by whitespace stays out of
|
|
319
|
+
* this pattern: prose like "the password is unknown" would otherwise lose
|
|
320
|
+
* words without losing the secret behind them.
|
|
321
|
+
*
|
|
322
|
+
* @type {RegExp}
|
|
323
|
+
*/
|
|
324
|
+
const CREDENTIAL_NAMED_VALUE_PATTERN =
|
|
325
|
+
/((?:token|secret|pass(?:word)?|passwd|passphrase|credential)[a-z0-9_-]*\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/giu;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* A token-shaped run of 32 or more characters beside a credential name, with
|
|
329
|
+
* `:`, `=` or whitespace as the separator. The length floor keeps checksums,
|
|
330
|
+
* digests and base64 artifacts that are not beside a credential name intact.
|
|
331
|
+
*
|
|
332
|
+
* @type {RegExp}
|
|
333
|
+
*/
|
|
334
|
+
const CREDENTIAL_ADJACENT_VALUE_PATTERN =
|
|
335
|
+
/((?:api[-_]?key|access[-_]?key|secret[-_]?key|key|token|secret|pass(?:word)?|passwd|credential)[a-z0-9_-]*(?:\s*[=:]\s*|\s+))("[^"\s]{32,}"|'[^'\s]{32,}'|[a-z0-9._~+/=-]{32,})/giu;
|
|
336
|
+
|
|
337
|
+
/** Placeholder replacing the user's home directory in free-text fields. */
|
|
338
|
+
const HOME_DIR_PLACEHOLDER = "~";
|
|
339
|
+
|
|
340
|
+
/** The user's home directory, resolved once; empty when it names no usable directory. */
|
|
341
|
+
let cachedHomeDir;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The user's home directory, resolved once, or an empty string when the
|
|
345
|
+
* platform answers with a root-equivalent path that must not be rewritten.
|
|
346
|
+
*
|
|
347
|
+
* @returns {string} Home directory path, or an empty string.
|
|
348
|
+
*/
|
|
349
|
+
function homeDirPath() {
|
|
350
|
+
if (cachedHomeDir === undefined) {
|
|
351
|
+
let home = "";
|
|
352
|
+
try {
|
|
353
|
+
home = homedir() || "";
|
|
354
|
+
} catch {
|
|
355
|
+
home = "";
|
|
356
|
+
}
|
|
357
|
+
cachedHomeDir = home === "/" || home === "\\" ? "" : home;
|
|
358
|
+
}
|
|
359
|
+
return cachedHomeDir;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Replace every occurrence of the user's home directory with `~`, so build
|
|
364
|
+
* output that names absolute paths under it stops identifying the machine.
|
|
365
|
+
*
|
|
366
|
+
* @param {string} text Free text of untrusted origin.
|
|
367
|
+
* @returns {string} Text with the home directory path rewritten.
|
|
368
|
+
*/
|
|
369
|
+
function redactHomeDirectory(text) {
|
|
370
|
+
const home = homeDirPath();
|
|
371
|
+
if (!home || !text.includes(home)) {
|
|
372
|
+
return text;
|
|
373
|
+
}
|
|
374
|
+
return text.split(home).join(HOME_DIR_PLACEHOLDER);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Scrub a free-text ledger field: home paths first, sensitive assignment and
|
|
379
|
+
* flag values next, credential-bearing headers and adjacent token runs, then
|
|
380
|
+
* the BOM property sanitizer for URL userinfo/query strings and known
|
|
381
|
+
* credential shapes. This is the one redactor every introspection free-text
|
|
382
|
+
* field goes through, in the ledger and in both reports.
|
|
383
|
+
*
|
|
384
|
+
* @param {string} fieldName Ledger field being redacted.
|
|
385
|
+
* @param {string} value Raw field value.
|
|
386
|
+
* @returns {string} Redacted field value.
|
|
387
|
+
*/
|
|
388
|
+
function redactLedgerText(fieldName, value) {
|
|
389
|
+
// The BOM property sanitizer runs first: it strips URL userinfo and query
|
|
390
|
+
// strings, so the assignment redactor below never meets a query parameter
|
|
391
|
+
// and cannot leave a redaction marker inside a URL.
|
|
392
|
+
let redacted = sanitizeBomPropertyValue(
|
|
393
|
+
fieldName,
|
|
394
|
+
redactHomeDirectory(String(value)),
|
|
395
|
+
);
|
|
396
|
+
redacted = redactSensitiveAssignments(redacted);
|
|
397
|
+
redacted = redacted.replace(
|
|
398
|
+
AUTHORIZATION_HEADER_PATTERN,
|
|
399
|
+
(_match, header, separator) => `${header}${separator}[redacted]`,
|
|
400
|
+
);
|
|
401
|
+
redacted = redacted.replace(
|
|
402
|
+
CREDENTIAL_NAMED_VALUE_PATTERN,
|
|
403
|
+
(_match, prefix) => `${prefix}[redacted]`,
|
|
404
|
+
);
|
|
405
|
+
redacted = redacted.replace(
|
|
406
|
+
CREDENTIAL_ADJACENT_VALUE_PATTERN,
|
|
407
|
+
(_match, prefix) => `${prefix}[redacted]`,
|
|
408
|
+
);
|
|
409
|
+
return redacted;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Redact a free-text introspection value with the ledger's own redactor. Both
|
|
414
|
+
* report renderers run their free text through this function so a field can
|
|
415
|
+
* never be redacted differently in the ledger, the JSON report and the
|
|
416
|
+
* markdown report.
|
|
417
|
+
*
|
|
418
|
+
* @param {string|undefined} value Free text of untrusted origin.
|
|
419
|
+
* @returns {string|undefined} Redacted text.
|
|
420
|
+
*/
|
|
421
|
+
export function redactIntrospectionText(value) {
|
|
422
|
+
if (value === undefined || value === null) {
|
|
423
|
+
return value;
|
|
424
|
+
}
|
|
425
|
+
return redactLedgerText("detail", `${value}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Combined stdout and stderr of a spawned command, as the raw material of an
|
|
430
|
+
* output excerpt. Producers that already hold a spawn result call this so the
|
|
431
|
+
* recorder, not the producer, owns truncation and redaction.
|
|
432
|
+
*
|
|
433
|
+
* @param {Object|undefined} spawnResult A spawnSync-style result.
|
|
434
|
+
* @returns {string|undefined} The combined output, or undefined when the command produced none.
|
|
435
|
+
*/
|
|
436
|
+
export function commandOutputText(spawnResult) {
|
|
437
|
+
if (!spawnResult) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
const stdout = spawnResult.stdout ? spawnResult.stdout.toString() : "";
|
|
441
|
+
const stderr = spawnResult.stderr ? spawnResult.stderr.toString() : "";
|
|
442
|
+
const combined = stdout || stderr ? `${stdout}\n${stderr}` : "";
|
|
443
|
+
return combined.trim() ? combined : undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The tail of a command output, bounded to the excerpt limit. Interior
|
|
448
|
+
* newlines are the signal a reader diagnoses from, so only the edges are
|
|
449
|
+
* trimmed.
|
|
450
|
+
*
|
|
451
|
+
* @param {string} rawOutput Combined command output.
|
|
452
|
+
* @returns {string} At most the last OUTPUT_EXCERPT_LIMIT characters.
|
|
453
|
+
*/
|
|
454
|
+
function tailExcerpt(rawOutput) {
|
|
455
|
+
const trimmed = rawOutput.trim();
|
|
456
|
+
return trimmed.length > OUTPUT_EXCERPT_LIMIT
|
|
457
|
+
? trimmed.slice(-OUTPUT_EXCERPT_LIMIT)
|
|
458
|
+
: trimmed;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Whether another excerpt-bearing event fits inside the event cap. Such an
|
|
463
|
+
* event charges a second slot, so a run that fills its cap with excerpts
|
|
464
|
+
* starts dropping excerpts while every event still lands — a truncated
|
|
465
|
+
* ledger poisons the whole verdict, a missing excerpt degrades one entry.
|
|
466
|
+
*
|
|
467
|
+
* @returns {boolean} True when the excerpt may be stored.
|
|
468
|
+
*/
|
|
469
|
+
function hasExcerptHeadroom() {
|
|
470
|
+
return ledgerEvents.length + EXCERPT_EVENT_WEIGHT <= MAX_LEDGER_EVENTS;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Whether the run opted out of carrying tool output at all. `true`/`1` match
|
|
475
|
+
* the CDXGEN_INTROSPECT convention; read live so a run can set the variable
|
|
476
|
+
* after the modules load.
|
|
477
|
+
*
|
|
478
|
+
* @returns {boolean} True when output excerpts must not be recorded.
|
|
479
|
+
*/
|
|
480
|
+
function outputSuppressed() {
|
|
481
|
+
return ["true", "1"].includes(
|
|
482
|
+
readEnvironmentVariable("CDXGEN_INTROSPECT_NO_OUTPUT"),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Assemble a stored event from a kind and the producer's fields. Optional
|
|
488
|
+
* fields are kept in schema order and omitted when absent; every stored value
|
|
489
|
+
* is a plain string or number so the JSONL sidecar stays schema-stable.
|
|
490
|
+
*
|
|
491
|
+
* @param {string} kind Validated event kind.
|
|
492
|
+
* @param {Partial<LedgerEvent>} fields Producer-supplied fields.
|
|
493
|
+
* @returns {LedgerEvent} Event ready to store and append to the sidecar.
|
|
494
|
+
*/
|
|
495
|
+
function buildLedgerEvent(kind, fields) {
|
|
496
|
+
const event = /** @type {LedgerEvent} */ ({ kind });
|
|
497
|
+
event.ecosystem = fields.ecosystem;
|
|
498
|
+
if (fields.tool) {
|
|
499
|
+
event.tool = String(fields.tool);
|
|
500
|
+
}
|
|
501
|
+
if (fields.wanted) {
|
|
502
|
+
event.wanted = String(fields.wanted);
|
|
503
|
+
}
|
|
504
|
+
if (fields.found) {
|
|
505
|
+
event.found = String(fields.found);
|
|
506
|
+
}
|
|
507
|
+
if (fields.source) {
|
|
508
|
+
event.source = String(fields.source);
|
|
509
|
+
}
|
|
510
|
+
if (fields.path) {
|
|
511
|
+
event.path = redactLedgerText("path", fields.path);
|
|
512
|
+
}
|
|
513
|
+
if (fields.command) {
|
|
514
|
+
event.command = redactLedgerText("command", fields.command);
|
|
515
|
+
}
|
|
516
|
+
if (typeof fields.exitCode === "number") {
|
|
517
|
+
event.exitCode = fields.exitCode;
|
|
518
|
+
}
|
|
519
|
+
if (fields.detail) {
|
|
520
|
+
event.detail = redactLedgerText("detail", fields.detail);
|
|
521
|
+
}
|
|
522
|
+
if (fields.causeDetail) {
|
|
523
|
+
event.causeDetail = redactLedgerText("causeDetail", fields.causeDetail);
|
|
524
|
+
}
|
|
525
|
+
if (
|
|
526
|
+
fields.outputExcerpt &&
|
|
527
|
+
`${fields.outputExcerpt}`.trim() &&
|
|
528
|
+
!outputSuppressed() &&
|
|
529
|
+
hasExcerptHeadroom()
|
|
530
|
+
) {
|
|
531
|
+
event.outputExcerpt = redactLedgerText(
|
|
532
|
+
"outputExcerpt",
|
|
533
|
+
tailExcerpt(`${fields.outputExcerpt}`),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (fields.remediationId) {
|
|
537
|
+
event.remediationId = String(fields.remediationId);
|
|
538
|
+
}
|
|
539
|
+
if (fields.impact) {
|
|
540
|
+
event.impact = String(fields.impact);
|
|
541
|
+
}
|
|
542
|
+
event.timestamp = new Date().toISOString();
|
|
543
|
+
return event;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Append one event to the JSONL sidecar. Failures are swallowed: the sidecar
|
|
548
|
+
* is an observation channel and must never abort BOM generation, and the
|
|
549
|
+
* in-memory buffer still holds the event.
|
|
550
|
+
*
|
|
551
|
+
* @param {LedgerEvent} event Event to append.
|
|
552
|
+
* @returns {void}
|
|
553
|
+
*/
|
|
554
|
+
function persistLedgerEvent(event) {
|
|
555
|
+
const writer = resolveLedgerWriter();
|
|
556
|
+
if (!writer || ledgerFileClosed) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
writer.write(`${JSON.stringify(event)}\n`);
|
|
561
|
+
} catch {
|
|
562
|
+
// The sidecar is best-effort; the in-memory buffer keeps the event.
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Store an event in the in-memory buffer and persist it. Once the buffer
|
|
568
|
+
* reaches its cap, the first half stays pinned, a single truncation marker is
|
|
569
|
+
* inserted after it, and the tail slides forward one event at a time; the
|
|
570
|
+
* sidecar, when enabled, keeps every event.
|
|
571
|
+
*
|
|
572
|
+
* @param {LedgerEvent} event Event to store.
|
|
573
|
+
* @returns {LedgerEvent} The stored event.
|
|
574
|
+
*/
|
|
575
|
+
function appendLedgerEvent(event) {
|
|
576
|
+
if (ledgerEvents.length >= MAX_LEDGER_EVENTS) {
|
|
577
|
+
if (!truncationRecorded) {
|
|
578
|
+
truncationRecorded = true;
|
|
579
|
+
// The marker describes the recorder, not the ecosystem whose event
|
|
580
|
+
// happened to reach the cap, so it carries the reserved tool ecosystem.
|
|
581
|
+
const marker = buildLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
|
|
582
|
+
ecosystem: LEDGER_TOOL_ECOSYSTEM,
|
|
583
|
+
detail: `The in-memory build ledger reached its cap of ${MAX_LEDGER_EVENTS} events and now keeps only the earliest ${LEDGER_HEAD_KEEP} and the most recent events; the JSONL sidecar, when enabled, retains every event.`,
|
|
584
|
+
});
|
|
585
|
+
ledgerEvents.splice(LEDGER_HEAD_KEEP, 0, marker);
|
|
586
|
+
persistLedgerEvent(marker);
|
|
587
|
+
}
|
|
588
|
+
ledgerEvents.splice(
|
|
589
|
+
LEDGER_HEAD_KEEP + 1,
|
|
590
|
+
ledgerEvents.length + 1 - MAX_LEDGER_EVENTS,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
ledgerEvents.push(event);
|
|
594
|
+
persistLedgerEvent(event);
|
|
595
|
+
return event;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Record one build-adequacy event. Returns immediately after a single boolean
|
|
600
|
+
* test when introspection is disabled, so producers may pass plain values
|
|
601
|
+
* without gating the call themselves.
|
|
602
|
+
*
|
|
603
|
+
* Events with a kind outside LEDGER_EVENT_KINDS, or without a non-empty
|
|
604
|
+
* `ecosystem`, are dropped with a think-log note instead of throwing: the
|
|
605
|
+
* ledger is instrumentation, and a producer bug must never fail an SBOM run.
|
|
606
|
+
*
|
|
607
|
+
* @param {string} kind Event kind; one of LEDGER_EVENT_KINDS.
|
|
608
|
+
* @param {Partial<LedgerEvent>} [fields] Event fields; `command`, `path`, `detail`, `causeDetail` and `outputExcerpt` are redacted before storage, and `outputExcerpt` is additionally bounded to its tail.
|
|
609
|
+
* @returns {LedgerEvent|undefined} The stored event, or undefined when recording is disabled or the event was dropped.
|
|
610
|
+
*/
|
|
611
|
+
export function recordLedgerEvent(kind, fields = {}) {
|
|
612
|
+
if (!isLedgerEnabled()) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
if (!KNOWN_LEDGER_EVENT_KINDS.has(kind)) {
|
|
616
|
+
thoughtLog(
|
|
617
|
+
`Dropping a build ledger event with unknown kind ${kind}; expected one of ${Object.values(LEDGER_EVENT_KINDS).join(", ")}.`,
|
|
618
|
+
);
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
if (typeof fields?.ecosystem !== "string" || !fields.ecosystem) {
|
|
622
|
+
thoughtLog(
|
|
623
|
+
`Dropping a ${kind} build ledger event without a non-empty ecosystem.`,
|
|
624
|
+
);
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
return appendLedgerEvent(buildLedgerEvent(kind, fields));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Remediation catalog loaded from `data/remediations.json`, mapping every
|
|
632
|
+
* known `remediationId` to its ecosystem, default event kind and default
|
|
633
|
+
* impact. The ids are a public contract matched by agents, so the catalog file
|
|
634
|
+
* and the ids referenced by producers must stay in lock-step; a poku test
|
|
635
|
+
* asserts the two sets are equal.
|
|
636
|
+
*
|
|
637
|
+
* Loaded lazily on the first {@link recordDegradation} call so a disabled run
|
|
638
|
+
* pays no file read and a missing or unreadable file degrades to a null
|
|
639
|
+
* catalog instead of failing module load.
|
|
640
|
+
*
|
|
641
|
+
* @type {Record<string, {ecosystem: string, kind: string, impact: string, title: string}>|null|undefined}
|
|
642
|
+
*/
|
|
643
|
+
let remediationCatalog;
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Load (once) the remediation catalog from `data/remediations.json`.
|
|
647
|
+
*
|
|
648
|
+
* @returns {Record<string, {ecosystem: string, kind: string, impact: string, title: string}>|null} Catalog keyed by id, or null when it cannot be read.
|
|
649
|
+
*/
|
|
650
|
+
function loadRemediationCatalog() {
|
|
651
|
+
if (remediationCatalog !== undefined) {
|
|
652
|
+
return remediationCatalog;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
remediationCatalog = JSON.parse(
|
|
656
|
+
readFileSync(join(dirNameStr, "data", "remediations.json"), "utf-8"),
|
|
657
|
+
);
|
|
658
|
+
} catch {
|
|
659
|
+
// The catalog is a data file shipped with cdxgen; its absence means ids
|
|
660
|
+
// cannot be resolved and degradation events would carry no kind.
|
|
661
|
+
remediationCatalog = null;
|
|
662
|
+
}
|
|
663
|
+
return remediationCatalog;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Resolve the event kind for a remediation id from the catalog.
|
|
668
|
+
*
|
|
669
|
+
* @param {string} remediationId Remediation id to look up.
|
|
670
|
+
* @returns {string|undefined} Default event kind, or undefined when the id is unknown or the catalog is unavailable.
|
|
671
|
+
*/
|
|
672
|
+
export function remediationKindFor(remediationId) {
|
|
673
|
+
return loadRemediationCatalog()?.[remediationId]?.kind;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* The full remediation catalog, for consumers that need the extended entry
|
|
678
|
+
* fields (target tier, actions, verify clause) rather than a single resolved
|
|
679
|
+
* kind. Returns the cached parsed catalog; null when the data file is
|
|
680
|
+
* missing or unreadable.
|
|
681
|
+
*
|
|
682
|
+
* @returns {Record<string, Object>|null} Catalog keyed by remediation id, or null when unavailable.
|
|
683
|
+
*/
|
|
684
|
+
export function getRemediationCatalog() {
|
|
685
|
+
return loadRemediationCatalog();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Record a degraded outcome together with the remediation that would restore
|
|
690
|
+
* the lost evidence. The kind and the remediationId come from the catalog
|
|
691
|
+
* entry for `remediationId`, so a site only supplies the facts it observed;
|
|
692
|
+
* `ecosystem` and `impact` are required from the caller because only the site
|
|
693
|
+
* knows which scanned ecosystem paid the cost and how much. A site whose
|
|
694
|
+
* degradation differs in kind from the catalog default (a command that threw
|
|
695
|
+
* rather than a fallback that engaged) may pass an explicit `kind` in fields.
|
|
696
|
+
*
|
|
697
|
+
* Events for unknown ids are dropped with a think-log note: an id outside the
|
|
698
|
+
* catalog is a producer bug and the catalog is the contract.
|
|
699
|
+
*
|
|
700
|
+
* @param {string} remediationId Key into data/remediations.json.
|
|
701
|
+
* @param {Partial<LedgerEvent> & {kind?: string}} fields Event fields; `ecosystem` and `impact` are required, `kind` overrides the catalog default.
|
|
702
|
+
* @returns {LedgerEvent|undefined} The stored event, or undefined when recording is disabled or the event was dropped.
|
|
703
|
+
*/
|
|
704
|
+
export function recordDegradation(remediationId, fields = {}) {
|
|
705
|
+
if (!isLedgerEnabled()) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
const kind = fields.kind || remediationKindFor(remediationId);
|
|
709
|
+
if (!kind) {
|
|
710
|
+
thoughtLog(
|
|
711
|
+
`Dropping a degradation event for unknown remediationId ${remediationId}; add it to data/remediations.json.`,
|
|
712
|
+
);
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
const { kind: _kindOverride, ...rest } = fields;
|
|
716
|
+
return recordLedgerEvent(kind, {
|
|
717
|
+
...rest,
|
|
718
|
+
remediationId,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Snapshot of the events recorded by this thread. Each event and the array
|
|
724
|
+
* itself are frozen, so a consumer cannot mutate the recorder's state.
|
|
725
|
+
* In a multi-worker run this buffer is this thread's view only; the JSONL
|
|
726
|
+
* sidecar is the complete record.
|
|
727
|
+
*
|
|
728
|
+
* @returns {LedgerEvent[]} Frozen array of frozen events.
|
|
729
|
+
*/
|
|
730
|
+
export function getLedgerEvents() {
|
|
731
|
+
return Object.freeze(
|
|
732
|
+
ledgerEvents.map((event) => Object.freeze({ ...event })),
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Clear the in-memory buffer and the truncation state. Intended for tests; a
|
|
738
|
+
* production run never resets its own ledger. The sidecar file is append-only
|
|
739
|
+
* and is not touched.
|
|
740
|
+
*
|
|
741
|
+
* @returns {void}
|
|
742
|
+
*/
|
|
743
|
+
export function resetLedgerEvents() {
|
|
744
|
+
ledgerEvents.length = 0;
|
|
745
|
+
truncationRecorded = false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Parse a JSONL sidecar produced by `CDXGEN_INTROSPECT_LEDGER` back into
|
|
750
|
+
* events. Blank lines and lines that fail to parse — a torn final line from an
|
|
751
|
+
* interrupted run is expected — are skipped. Prefer this over the in-memory
|
|
752
|
+
* buffer whenever the sidecar exists: worker threads append to the same file,
|
|
753
|
+
* so it is the only complete record of a multi-worker run.
|
|
754
|
+
*
|
|
755
|
+
* @param {string} ledgerPath Path to the JSONL sidecar.
|
|
756
|
+
* @returns {LedgerEvent[]} Frozen array of frozen events, empty when the file is missing or unreadable.
|
|
757
|
+
*/
|
|
758
|
+
export function loadLedgerFile(ledgerPath) {
|
|
759
|
+
const content = safeReadFileSync(ledgerPath, "utf-8");
|
|
760
|
+
if (typeof content !== "string" || !content.length) {
|
|
761
|
+
return Object.freeze([]);
|
|
762
|
+
}
|
|
763
|
+
const events = [];
|
|
764
|
+
for (const line of content.split("\n")) {
|
|
765
|
+
const trimmedLine = line.trim();
|
|
766
|
+
if (!trimmedLine) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
let parsed;
|
|
770
|
+
try {
|
|
771
|
+
parsed = JSON.parse(trimmedLine);
|
|
772
|
+
} catch {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
776
|
+
events.push(Object.freeze(parsed));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return Object.freeze(events);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Close the sidecar descriptor held by this thread. Idempotent; further
|
|
784
|
+
* events stay in memory only. The descriptor is also released when the
|
|
785
|
+
* process exits through `closeLogStreams`.
|
|
786
|
+
*
|
|
787
|
+
* @returns {void}
|
|
788
|
+
*/
|
|
789
|
+
export function closeLedger() {
|
|
790
|
+
if (ledgerFileClosed) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
ledgerFileClosed = true;
|
|
794
|
+
// A writer that was never resolved means no event reached the sidecar, so
|
|
795
|
+
// there is no descriptor to release and no file was created.
|
|
796
|
+
if (ledgerFileWriter !== LEDGER_WRITER_UNRESOLVED) {
|
|
797
|
+
ledgerFileWriter?.closeSync();
|
|
798
|
+
}
|
|
799
|
+
}
|