@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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred `fail-on-error` exits for introspected runs.
|
|
3
|
+
*
|
|
4
|
+
* `--fail-on-error` promises an operator that a failing dependency extractor
|
|
5
|
+
* never produces a plausible-but-incomplete BOM, so the extractors exit the
|
|
6
|
+
* process the moment a build tool fails. Under `--introspect` that exit
|
|
7
|
+
* destroys the very evidence the run exists to produce: the ledger events
|
|
8
|
+
* recorded a statement earlier die with the process, `postProcess` never
|
|
9
|
+
* runs, and neither the BOM nor the fidelity report is written.
|
|
10
|
+
*
|
|
11
|
+
* On an introspected run the extractor instead records the failure here and
|
|
12
|
+
* throws {@link DeferredFailOnError}, which the extractor catches at its
|
|
13
|
+
* completion point so it still returns everything it collected before the
|
|
14
|
+
* failure — the incomplete-result fallbacks stay skipped, exactly as
|
|
15
|
+
* `fail-on-error` demands. The CLI then writes the BOM and both reports and
|
|
16
|
+
* exits with the dedicated {@link INTROSPECTION_FAILURE_EXIT_CODE} status, so
|
|
17
|
+
* the exit code still claims "an extractor failed", distinct from the
|
|
18
|
+
* fidelity gate's 4.
|
|
19
|
+
*
|
|
20
|
+
* Without introspection this module is a pass-through to `process.exit(1)`;
|
|
21
|
+
* the default contract of the flag is unchanged.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
isIntrospectionEnabled,
|
|
26
|
+
LEDGER_EVENT_KINDS,
|
|
27
|
+
recordLedgerEvent,
|
|
28
|
+
} from "./buildLedger.js";
|
|
29
|
+
import { thoughtLog } from "./logger.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Exit status used when an introspected run completes its outputs despite a
|
|
33
|
+
* `--fail-on-error` extractor failure. Distinct from 1 (generic failure),
|
|
34
|
+
* 3 (TEA publish failure) and 4 (the introspection fidelity gate).
|
|
35
|
+
*
|
|
36
|
+
* @type {number}
|
|
37
|
+
*/
|
|
38
|
+
export const INTROSPECTION_FAILURE_EXIT_CODE = 5;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Control-flow signal thrown by {@link deferFailOnError} on an introspected
|
|
42
|
+
* run. The extractor that called it must let it unwind to its completion
|
|
43
|
+
* point, keep everything it collected until then, and rethrow anything else.
|
|
44
|
+
*/
|
|
45
|
+
export class DeferredFailOnError extends Error {
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} message Human-readable failure summary.
|
|
48
|
+
*/
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "DeferredFailOnError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The extractor failures deferred so far. Populated only on introspected
|
|
57
|
+
* runs; the CLI reads it after the BOM and the reports are written to decide
|
|
58
|
+
* the process exit status.
|
|
59
|
+
*
|
|
60
|
+
* @type {Array<{ecosystem: string, tool: string, detail: string, exitCode: number|undefined, command: string|undefined, outputExcerpt: string|undefined}>}
|
|
61
|
+
*/
|
|
62
|
+
const deferredFailures = [];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Test whether an error is the fail-on-error deferral signal.
|
|
66
|
+
*
|
|
67
|
+
* @param {unknown} err The caught error.
|
|
68
|
+
* @returns {boolean} True when the error unwinds a deferred fail-on-error abort.
|
|
69
|
+
*/
|
|
70
|
+
export function isDeferredFailOnError(err) {
|
|
71
|
+
return err instanceof DeferredFailOnError;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Snapshot of the deferred failures recorded by this run.
|
|
76
|
+
*
|
|
77
|
+
* @returns {Array<Object>} Frozen copies of the recorded failures.
|
|
78
|
+
*/
|
|
79
|
+
export function getDeferredFailures() {
|
|
80
|
+
return deferredFailures.map((failure) => Object.freeze({ ...failure }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle a failing extractor on behalf of a `options.failOnError &&
|
|
85
|
+
* process.exit(1)` site. Without `fail-on-error` the run continues exactly
|
|
86
|
+
* as it would have. With the flag and no introspection the process exits 1,
|
|
87
|
+
* unchanged. On an introspected run the failure is recorded — including one
|
|
88
|
+
* ledger observation on the ecosystem row the failure cost evidence — and
|
|
89
|
+
* the deferral signal is thrown so the extractor stops where it is without
|
|
90
|
+
* taking any incomplete-result fallback.
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} options CLI options.
|
|
93
|
+
* @param {Object} failure What failed, for the operator message and the report.
|
|
94
|
+
* @param {string} failure.tool The build tool that failed.
|
|
95
|
+
* @param {string} [failure.ecosystem] The ecosystem the tool serves, so the
|
|
96
|
+
* observation lands on the row whose evidence the failure cost.
|
|
97
|
+
* @param {string} failure.detail One sentence, present tense.
|
|
98
|
+
* @param {number} [failure.exitCode] The tool's exit status, when known.
|
|
99
|
+
* @param {string} [failure.command] The redacted command that was attempted.
|
|
100
|
+
* @param {string} [failure.outputExcerpt] The failed command's combined output,
|
|
101
|
+
* when the caller already holds it; the recorder bounds and redacts it.
|
|
102
|
+
* @returns {void} Never returns on an introspected run; it throws.
|
|
103
|
+
*/
|
|
104
|
+
export function deferFailOnError(options, failure) {
|
|
105
|
+
if (!options?.failOnError) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const entry = {
|
|
109
|
+
ecosystem: failure?.ecosystem || "cdxgen",
|
|
110
|
+
tool: failure?.tool || "unknown",
|
|
111
|
+
detail: failure?.detail || "the extractor reported a failure",
|
|
112
|
+
exitCode:
|
|
113
|
+
typeof failure?.exitCode === "number" ? failure.exitCode : undefined,
|
|
114
|
+
command: failure?.command,
|
|
115
|
+
outputExcerpt: failure?.outputExcerpt,
|
|
116
|
+
};
|
|
117
|
+
if (!isIntrospectionEnabled(options)) {
|
|
118
|
+
process.exit(1);
|
|
119
|
+
// Unreachable in a real run; the explicit return keeps the deferral from
|
|
120
|
+
// continuing wherever process.exit is instrumented rather than fatal.
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
deferredFailures.push(entry);
|
|
124
|
+
thoughtLog(
|
|
125
|
+
`Deferring the fail-on-error exit for ${entry.tool} so the introspection verdict can be produced: ${entry.detail}`,
|
|
126
|
+
);
|
|
127
|
+
// A `command.failed` observation on the row the failure cost evidence: it
|
|
128
|
+
// carries no remediationId, so it reports the deferral without competing
|
|
129
|
+
// with the degradation the extractor recorded for the same failure, and it
|
|
130
|
+
// never trips the truncation marker, which keys on `evidence.degraded`
|
|
131
|
+
// under the reserved recorder ecosystem.
|
|
132
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.COMMAND_FAILED, {
|
|
133
|
+
ecosystem: entry.ecosystem,
|
|
134
|
+
tool: entry.tool,
|
|
135
|
+
exitCode: entry.exitCode,
|
|
136
|
+
command: entry.command,
|
|
137
|
+
outputExcerpt: entry.outputExcerpt,
|
|
138
|
+
detail: `fail-on-error was deferred so the BOM and the report could be written: ${entry.detail}`,
|
|
139
|
+
});
|
|
140
|
+
throw new DeferredFailOnError(`${entry.tool}: ${entry.detail}`);
|
|
141
|
+
}
|
package/lib/core/env.js
CHANGED
|
@@ -173,9 +173,33 @@ export function getJavaCommand() {
|
|
|
173
173
|
/** Resolved Python executable command (PYTHON_CMD env var > CONDA_PYTHON_EXE > "python"). */
|
|
174
174
|
export const PYTHON_CMD = getPythonCommand();
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Whether an executable of the given name is reachable through PATH.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} name Executable name, without a platform extension.
|
|
180
|
+
* @returns {boolean} True when a PATH entry holds the executable.
|
|
181
|
+
*/
|
|
182
|
+
function isOnPath(name) {
|
|
183
|
+
const searchPath = readEnvironmentVariable("PATH");
|
|
184
|
+
if (!searchPath) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const names =
|
|
188
|
+
process.platform === "win32" ? [`${name}.exe`, `${name}.bat`] : [name];
|
|
189
|
+
return searchPath
|
|
190
|
+
.split(process.platform === "win32" ? ";" : ":")
|
|
191
|
+
.some((binDir) =>
|
|
192
|
+
names.some((binName) => binDir && safeExistsSync(join(binDir, binName))),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
176
196
|
/**
|
|
177
197
|
* Returns the Python executable command to use, resolved in priority order:
|
|
178
|
-
* PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python".
|
|
198
|
+
* PYTHON_CMD env var > CONDA_PYTHON_EXE env var > "python" > "python3".
|
|
199
|
+
*
|
|
200
|
+
* Distributions that ship only a versioned interpreter leave a bare "python"
|
|
201
|
+
* unresolvable, which would fail every virtualenv this tool creates, so the
|
|
202
|
+
* versioned name is used when it is the only one PATH offers.
|
|
179
203
|
*
|
|
180
204
|
* @returns {string} Path or name of the Python executable
|
|
181
205
|
*/
|
|
@@ -185,6 +209,8 @@ export function getPythonCommand() {
|
|
|
185
209
|
pythonCmd = readEnvironmentVariable("PYTHON_CMD");
|
|
186
210
|
} else if (readEnvironmentVariable("CONDA_PYTHON_EXE")) {
|
|
187
211
|
pythonCmd = readEnvironmentVariable("CONDA_PYTHON_EXE");
|
|
212
|
+
} else if (!isOnPath("python") && isOnPath("python3")) {
|
|
213
|
+
pythonCmd = "python3";
|
|
188
214
|
}
|
|
189
215
|
return pythonCmd;
|
|
190
216
|
}
|
|
@@ -230,6 +256,19 @@ if (readEnvironmentVariable("CARGO_CMD")) {
|
|
|
230
256
|
CARGO_CMD = readEnvironmentVariable("CARGO_CMD");
|
|
231
257
|
}
|
|
232
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Target triple the cargo resolve graph is filtered to, so that a
|
|
261
|
+
* cross-compiled project can be described for the triple it ships on rather
|
|
262
|
+
* than for the union of every triple its lock file covers.
|
|
263
|
+
*
|
|
264
|
+
* `CDXGEN_CARGO_TARGET` is cdxgen's own name for it; `CARGO_BUILD_TARGET` is
|
|
265
|
+
* cargo's standard variable, which a cross-compiling build already sets.
|
|
266
|
+
*/
|
|
267
|
+
export const CARGO_TARGET =
|
|
268
|
+
readEnvironmentVariable("CDXGEN_CARGO_TARGET") ||
|
|
269
|
+
readEnvironmentVariable("CARGO_BUILD_TARGET") ||
|
|
270
|
+
undefined;
|
|
271
|
+
|
|
233
272
|
/** Resolved Clojure CLI executable command (CLJ_CMD env var, or "clj"). */
|
|
234
273
|
export let CLJ_CMD = "clj";
|
|
235
274
|
if (readEnvironmentVariable("CLJ_CMD")) {
|
|
@@ -281,6 +320,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
281
320
|
"java24",
|
|
282
321
|
"java25",
|
|
283
322
|
"java26",
|
|
323
|
+
"java27",
|
|
284
324
|
"groovy",
|
|
285
325
|
"kotlin",
|
|
286
326
|
"kt",
|
|
@@ -313,6 +353,9 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
313
353
|
"nodejs20",
|
|
314
354
|
"nodejs22",
|
|
315
355
|
"nodejs23",
|
|
356
|
+
"nodejs24",
|
|
357
|
+
"nodejs25",
|
|
358
|
+
"nodejs26",
|
|
316
359
|
"node",
|
|
317
360
|
"node8",
|
|
318
361
|
"node10",
|
|
@@ -323,11 +366,16 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
323
366
|
"node20",
|
|
324
367
|
"node22",
|
|
325
368
|
"node23",
|
|
369
|
+
"node24",
|
|
370
|
+
"node25",
|
|
371
|
+
"node26",
|
|
326
372
|
"js",
|
|
327
373
|
"javascript",
|
|
328
374
|
"typescript",
|
|
329
375
|
"ts",
|
|
330
376
|
"tsx",
|
|
377
|
+
"svelte",
|
|
378
|
+
"sveltekit",
|
|
331
379
|
"yarn",
|
|
332
380
|
"rush",
|
|
333
381
|
"bun",
|
|
@@ -335,6 +383,7 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
335
383
|
],
|
|
336
384
|
mcp: ["mcp"],
|
|
337
385
|
"ai-skill": ["ai-skill", "skill", "skills"],
|
|
386
|
+
agentic: ["agentic", "agentic-cli", "ai-tools", "ai-cli"],
|
|
338
387
|
py: [
|
|
339
388
|
"py",
|
|
340
389
|
"python",
|
|
@@ -346,6 +395,8 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
346
395
|
"python311",
|
|
347
396
|
"python312",
|
|
348
397
|
"python313",
|
|
398
|
+
"python314",
|
|
399
|
+
"python315",
|
|
349
400
|
"pixi",
|
|
350
401
|
"pip",
|
|
351
402
|
"poetry",
|
|
@@ -372,6 +423,8 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
372
423
|
"dotnet7",
|
|
373
424
|
"dotnet8",
|
|
374
425
|
"dotnet9",
|
|
426
|
+
"dotnet10",
|
|
427
|
+
"dotnet11",
|
|
375
428
|
"dotnet-framework",
|
|
376
429
|
"dotnet-framework47",
|
|
377
430
|
"dotnet-framework48",
|
|
@@ -391,8 +444,19 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
391
444
|
"hmiproj",
|
|
392
445
|
],
|
|
393
446
|
dart: ["dart", "flutter", "pub"],
|
|
394
|
-
haskell: ["haskell", "hackage", "cabal"],
|
|
447
|
+
haskell: ["haskell", "hackage", "cabal", "stack", "stackage"],
|
|
395
448
|
elixir: ["elixir", "hex", "mix"],
|
|
449
|
+
erlang: ["erlang", "rebar", "rebar3"],
|
|
450
|
+
julia: ["julia"],
|
|
451
|
+
r: ["r", "cran", "renv"],
|
|
452
|
+
ocaml: ["ocaml", "opam", "dune"],
|
|
453
|
+
elm: ["elm"],
|
|
454
|
+
crystal: ["crystal", "shards"],
|
|
455
|
+
nim: ["nim", "nimble"],
|
|
456
|
+
lua: ["lua", "luarocks"],
|
|
457
|
+
perl: ["perl", "cpan", "carton", "cpanfile"],
|
|
458
|
+
spack: ["spack"],
|
|
459
|
+
terraform: ["terraform", "opentofu", "tf"],
|
|
396
460
|
c: ["c", "cpp", "c++", "conan", "collider"],
|
|
397
461
|
clojure: ["clojure", "edn", "clj", "leiningen"],
|
|
398
462
|
github: ["github", "actions"],
|
|
@@ -459,6 +523,39 @@ export const PACKAGE_MANAGER_ALIASES = {
|
|
|
459
523
|
scala: ["sbt"],
|
|
460
524
|
};
|
|
461
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Project-type prefixes that accept a version suffix pinning a JVM build
|
|
528
|
+
* tool, e.g. `maven3.9.9`, `mvn3.9.9`, `gradle8.14`, `sbt1.10`, or
|
|
529
|
+
* `scala3.6.4`. Kept alongside the alias map because both describe how CLI
|
|
530
|
+
* project types map to base types.
|
|
531
|
+
*
|
|
532
|
+
* @type {string[]}
|
|
533
|
+
*/
|
|
534
|
+
export const JVM_BUILD_TOOL_TYPE_PREFIXES = [
|
|
535
|
+
"maven",
|
|
536
|
+
"mvn",
|
|
537
|
+
"gradle",
|
|
538
|
+
"sbt",
|
|
539
|
+
"scala",
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Check whether a project type pins a JVM build tool version, i.e. it starts
|
|
544
|
+
* with one of the JVM build tool prefixes followed by a digit.
|
|
545
|
+
*
|
|
546
|
+
* @param {string} projectType Project type from the CLI
|
|
547
|
+
* @returns {boolean} True for versioned JVM build tool types.
|
|
548
|
+
*/
|
|
549
|
+
export function isVersionedJvmToolProjectType(projectType) {
|
|
550
|
+
const pt = `${projectType || ""}`.toLowerCase();
|
|
551
|
+
for (const prefix of JVM_BUILD_TOOL_TYPE_PREFIXES) {
|
|
552
|
+
if (pt.startsWith(prefix) && /^\d/.test(pt.slice(prefix.length))) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
462
559
|
/**
|
|
463
560
|
* Method to check if a given feature flag is enabled.
|
|
464
561
|
*
|
|
@@ -553,6 +650,15 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
|
|
|
553
650
|
) {
|
|
554
651
|
baseProjectTypes.push("ruby");
|
|
555
652
|
}
|
|
653
|
+
// Versioned JVM build tool types such as maven3.9.9 or gradle8.14 are java
|
|
654
|
+
// types with a pinned build tool, so they must dispatch java parsing too.
|
|
655
|
+
if (
|
|
656
|
+
options.projectType?.length &&
|
|
657
|
+
projectTypes.includes("java") &&
|
|
658
|
+
options.projectType.some((pt) => isVersionedJvmToolProjectType(pt))
|
|
659
|
+
) {
|
|
660
|
+
baseProjectTypes.push("java");
|
|
661
|
+
}
|
|
556
662
|
const baseExcludeTypes = [];
|
|
557
663
|
for (const abt of Object.keys(PROJECT_TYPE_ALIASES)) {
|
|
558
664
|
if (
|
package/lib/core/fs.js
CHANGED
|
@@ -2,12 +2,15 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import {
|
|
5
|
+
accessSync,
|
|
5
6
|
copyFileSync,
|
|
6
7
|
createReadStream,
|
|
7
8
|
existsSync,
|
|
9
|
+
constants as fsConstants,
|
|
8
10
|
lstatSync,
|
|
9
11
|
mkdirSync,
|
|
10
12
|
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
11
14
|
rmSync,
|
|
12
15
|
unlinkSync,
|
|
13
16
|
writeFileSync,
|
|
@@ -111,6 +114,26 @@ export function safeExistsSync(filePath) {
|
|
|
111
114
|
return exists;
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Permission-aware check for whether a path exists and carries the executable
|
|
119
|
+
* bit for the current user. Answers false instead of throwing when the path
|
|
120
|
+
* is absent, unreadable, or blocked by secure mode.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} filePath File path to check.
|
|
123
|
+
* @returns {boolean} True when the path exists and is executable.
|
|
124
|
+
*/
|
|
125
|
+
export function safeIsExecutableSync(filePath) {
|
|
126
|
+
if (!safeExistsSync(filePath)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
accessSync(filePath, fsConstants.X_OK);
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
/**
|
|
115
138
|
* Permission- and dry-run-aware wrapper around writeFileSync. Records the
|
|
116
139
|
* activity and returns undefined when blocked.
|
|
@@ -146,6 +169,43 @@ export function safeWriteSync(filePath, data, options) {
|
|
|
146
169
|
recordFilesystemActivity("write", filePath, "completed");
|
|
147
170
|
}
|
|
148
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Permission-aware wrapper around readFileSync that returns undefined instead
|
|
174
|
+
* of throwing. Reads are allowed in dry-run mode, so unlike the write wrappers
|
|
175
|
+
* this only guards secure-mode read permission; the read itself is not recorded
|
|
176
|
+
* in the activity ledger because inspection of classified paths is already
|
|
177
|
+
* recorded by safeExistsSync and a read has no side effect to audit.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} filePath File path to read.
|
|
180
|
+
* @param {Object|string} [options] readFileSync options; defaults to UTF-8 text.
|
|
181
|
+
* @returns {string|Buffer|undefined} File content, or undefined when the path cannot be read.
|
|
182
|
+
*/
|
|
183
|
+
export function safeReadFileSync(filePath, options = "utf-8") {
|
|
184
|
+
if (!hasReadPermission(filePath)) {
|
|
185
|
+
if (DEBUG_MODE) {
|
|
186
|
+
console.log("cdxgen lacks read permission for a requested path.");
|
|
187
|
+
}
|
|
188
|
+
const pathMetadata = classifyActivityPath(filePath);
|
|
189
|
+
if (pathMetadata) {
|
|
190
|
+
recordPolicyActivity(filePath, {
|
|
191
|
+
metadata: {
|
|
192
|
+
classification: pathMetadata.classification,
|
|
193
|
+
ecosystem: pathMetadata.ecosystem,
|
|
194
|
+
policyType: "fs.read",
|
|
195
|
+
},
|
|
196
|
+
reason: `Denied read of ${pathMetadata.label} ${filePath} due to missing fs.read permission.`,
|
|
197
|
+
status: "blocked",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return readFileSync(filePath, options);
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
149
209
|
/**
|
|
150
210
|
* Safely create a directory without crashing due to a lack of permissions
|
|
151
211
|
*
|
package/lib/core/logger.js
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import { Console } from "node:console";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import process from "node:process";
|
|
4
|
-
|
|
5
|
-
import {
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
closeAllSyncFileWriters,
|
|
6
|
+
diagnosticStream,
|
|
7
|
+
openSyncFileWriter,
|
|
8
|
+
} from "./syncFileWriter.js";
|
|
9
|
+
|
|
10
|
+
export { diagnosticStream };
|
|
6
11
|
|
|
7
12
|
// Environment variables here are read with raw `process.env` rather than
|
|
8
13
|
// `readEnvironmentVariable`: that helper lives in activity.js, which imports
|
|
9
14
|
// this module, so routing these through it would form an import cycle.
|
|
10
15
|
|
|
11
|
-
// Stream contract: human-readable diagnostics default to stderr so stdout
|
|
12
|
-
// carries only the BOM payload. CDXGEN_LOG_STREAM=stdout restores the
|
|
13
|
-
// pre-v13 behaviour for callers that scraped cdxgen's stdout.
|
|
14
|
-
const logStream =
|
|
15
|
-
process.env.CDXGEN_LOG_STREAM === "stdout" ? process.stdout : process.stderr;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The stream every human-readable diagnostic is written to.
|
|
19
|
-
*
|
|
20
|
-
* Exported so that the live region and the table renderer resolve the same
|
|
21
|
-
* destination as the loggers do. A caller that sets `CDXGEN_LOG_STREAM=stdout`
|
|
22
|
-
* gets the pre-v13 behaviour for all of it, not just for the thought and trace
|
|
23
|
-
* logs.
|
|
24
|
-
*
|
|
25
|
-
* @type {NodeJS.WritableStream}
|
|
26
|
-
*/
|
|
27
|
-
export const diagnosticStream = logStream;
|
|
28
|
-
|
|
29
16
|
// Custom lightweight colors formatting helper replacing yoctocolors
|
|
30
17
|
const colors = {
|
|
31
18
|
cyanBright: (text) => `\x1b[96m${text}\x1b[39m`,
|
|
@@ -38,81 +25,9 @@ export const THINK_MODE =
|
|
|
38
25
|
["true", "1"].includes(process.env.CDXGEN_THINK_MODE) ||
|
|
39
26
|
process.env.CDXGEN_DEBUG_MODE === "verbose";
|
|
40
27
|
|
|
41
|
-
// Log files are written synchronously rather than through
|
|
42
|
-
// `fs.createWriteStream`. A write stream buffers, so anything still queued when
|
|
43
|
-
// the process exits is lost, leaving readers that tail these files a truncated
|
|
44
|
-
// log with no closing `</think>` marker. Writing through one shared descriptor
|
|
45
|
-
// per path also keeps the two Console channels from interleaving at
|
|
46
|
-
// independent file offsets.
|
|
47
|
-
const syncFileWriters = new Map();
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Open a synchronous writer for a log file path.
|
|
51
|
-
*
|
|
52
|
-
* `Console` requires a real stream, so this is a `Writable` whose `_write`
|
|
53
|
-
* completes synchronously: bytes reach the descriptor as each record is
|
|
54
|
-
* logged, and nothing is left queued for an event loop turn that a process
|
|
55
|
-
* exit may never take.
|
|
56
|
-
*
|
|
57
|
-
* @param {string} filePath Destination path
|
|
58
|
-
* @returns {Writable} Writer, or the diagnostic stream when the path is unwritable
|
|
59
|
-
*/
|
|
60
|
-
function openSyncFileWriter(filePath) {
|
|
61
|
-
const existing = syncFileWriters.get(filePath);
|
|
62
|
-
if (existing) {
|
|
63
|
-
return existing;
|
|
64
|
-
}
|
|
65
|
-
let fd;
|
|
66
|
-
try {
|
|
67
|
-
// Worker threads log to the same path through their own instance of this
|
|
68
|
-
// module, so the descriptor is opened in append mode: O_APPEND lands every
|
|
69
|
-
// write at the end of the file no matter how many writers share it, where
|
|
70
|
-
// a truncating open gives each writer its own offset and they punch holes
|
|
71
|
-
// through each other's records. The main thread truncates once so a run
|
|
72
|
-
// still starts from an empty log.
|
|
73
|
-
if (isMainThread) {
|
|
74
|
-
try {
|
|
75
|
-
fs.truncateSync(filePath, 0);
|
|
76
|
-
} catch {
|
|
77
|
-
// No file yet: the append open below creates it.
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
fd = fs.openSync(filePath, "a");
|
|
81
|
-
} catch {
|
|
82
|
-
// An unwritable path must not take the whole run down: fall back to the
|
|
83
|
-
// diagnostic stream so the records stay visible somewhere.
|
|
84
|
-
return logStream;
|
|
85
|
-
}
|
|
86
|
-
const writer = new Writable({
|
|
87
|
-
write(chunk, _encoding, callback) {
|
|
88
|
-
if (fd !== undefined) {
|
|
89
|
-
try {
|
|
90
|
-
fs.writeSync(fd, chunk);
|
|
91
|
-
} catch {
|
|
92
|
-
// A full disk or a revoked descriptor must not abort BOM generation.
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
callback();
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
writer.closeSync = () => {
|
|
99
|
-
if (fd === undefined) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
fs.closeSync(fd);
|
|
104
|
-
} finally {
|
|
105
|
-
fd = undefined;
|
|
106
|
-
syncFileWriters.delete(filePath);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
syncFileWriters.set(filePath, writer);
|
|
110
|
-
return writer;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
28
|
const thinkOutput = process.env.CDXGEN_THOUGHT_LOG
|
|
114
29
|
? openSyncFileWriter(process.env.CDXGEN_THOUGHT_LOG)
|
|
115
|
-
:
|
|
30
|
+
: diagnosticStream;
|
|
116
31
|
const thinkErrorOutput = process.env.CDXGEN_THOUGHT_LOG
|
|
117
32
|
? thinkOutput
|
|
118
33
|
: process.stderr;
|
|
@@ -131,7 +46,7 @@ export const TRACE_MODE =
|
|
|
131
46
|
|
|
132
47
|
const traceOutput = process.env.CDXGEN_TRACE_LOG
|
|
133
48
|
? openSyncFileWriter(process.env.CDXGEN_TRACE_LOG)
|
|
134
|
-
:
|
|
49
|
+
: diagnosticStream;
|
|
135
50
|
const traceErrorOutput = process.env.CDXGEN_TRACE_LOG
|
|
136
51
|
? traceOutput
|
|
137
52
|
: process.stderr;
|
|
@@ -186,7 +101,7 @@ export function thoughtEnd() {
|
|
|
186
101
|
function colorizeText(s) {
|
|
187
102
|
// Only colorize for an interactive terminal stream; piped/CI/file output
|
|
188
103
|
// stays free of escape bytes so the diagnostic stream is grep-safe.
|
|
189
|
-
if (process.env.CDXGEN_THOUGHT_LOG || !
|
|
104
|
+
if (process.env.CDXGEN_THOUGHT_LOG || !diagnosticStream.isTTY) {
|
|
190
105
|
return s;
|
|
191
106
|
}
|
|
192
107
|
s = s.replace(/(\d+)/g, colors.cyanBright("$1"));
|
|
@@ -270,9 +185,7 @@ export function closeLogStreams() {
|
|
|
270
185
|
}
|
|
271
186
|
logStreamsClosed = true;
|
|
272
187
|
thoughtEnd();
|
|
273
|
-
|
|
274
|
-
writer.closeSync();
|
|
275
|
-
}
|
|
188
|
+
closeAllSyncFileWriters();
|
|
276
189
|
}
|
|
277
190
|
|
|
278
191
|
// The writers are synchronous, so this handler only has to emit the terminator
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared severity ordering for finding and confidence ladders.
|
|
3
|
+
*
|
|
4
|
+
* Lives in the core layer so modules below the audit layer can order labels
|
|
5
|
+
* such as `low`/`medium`/`high` without importing the audit package; the audit
|
|
6
|
+
* scoring module re-exports this constant under its historical name.
|
|
7
|
+
*
|
|
8
|
+
* @type {Readonly<Record<string, number>>}
|
|
9
|
+
*/
|
|
10
|
+
export const SEVERITY_ORDER = Object.freeze({
|
|
11
|
+
none: -1,
|
|
12
|
+
low: 0,
|
|
13
|
+
medium: 1,
|
|
14
|
+
high: 2,
|
|
15
|
+
critical: 3,
|
|
16
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { Writable } from "node:stream";
|
|
4
|
+
import { isMainThread } from "node:worker_threads";
|
|
5
|
+
|
|
6
|
+
// Stream contract: human-readable diagnostics default to stderr so stdout
|
|
7
|
+
// carries only the BOM payload. CDXGEN_LOG_STREAM=stdout restores the
|
|
8
|
+
// pre-v13 behaviour for callers that scraped cdxgen's stdout.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The stream every human-readable diagnostic is written to.
|
|
12
|
+
*
|
|
13
|
+
* Also the fallback destination of {@link openSyncFileWriter} when a log path
|
|
14
|
+
* cannot be opened, so an unwritable path keeps the records visible somewhere
|
|
15
|
+
* instead of taking the run down.
|
|
16
|
+
*
|
|
17
|
+
* @type {NodeJS.WritableStream}
|
|
18
|
+
*/
|
|
19
|
+
export const diagnosticStream =
|
|
20
|
+
process.env.CDXGEN_LOG_STREAM === "stdout" ? process.stdout : process.stderr;
|
|
21
|
+
|
|
22
|
+
// Log files are written synchronously rather than through
|
|
23
|
+
// `fs.createWriteStream`. A write stream buffers, so anything still queued when
|
|
24
|
+
// the process exits is lost, leaving readers that tail these files a truncated
|
|
25
|
+
// log. Writing through one shared descriptor per path also keeps concurrent
|
|
26
|
+
// writers from interleaving at independent file offsets.
|
|
27
|
+
const syncFileWriters = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Open a synchronous writer for a log file path, or return the writer already
|
|
31
|
+
* open for that path.
|
|
32
|
+
*
|
|
33
|
+
* `Console` requires a real stream, so this is a `Writable` whose `_write`
|
|
34
|
+
* completes synchronously: bytes reach the descriptor as each record is
|
|
35
|
+
* written, and nothing is left queued for an event loop turn that a process
|
|
36
|
+
* exit may never take.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} filePath Destination path
|
|
39
|
+
* @param {NodeJS.WritableStream|null} [fallbackStream] Stream returned when the path cannot be opened; defaults to the diagnostic stream. Pass `null` to get `undefined` instead, for callers that must never let data records spill into a diagnostic stream.
|
|
40
|
+
* @returns {Writable|NodeJS.WritableStream|undefined} Writer for the path, the fallback stream, or `undefined` when the path cannot be opened and no fallback was requested
|
|
41
|
+
*/
|
|
42
|
+
export function openSyncFileWriter(
|
|
43
|
+
filePath,
|
|
44
|
+
fallbackStream = diagnosticStream,
|
|
45
|
+
) {
|
|
46
|
+
const existing = syncFileWriters.get(filePath);
|
|
47
|
+
if (existing) {
|
|
48
|
+
return existing;
|
|
49
|
+
}
|
|
50
|
+
let fd;
|
|
51
|
+
try {
|
|
52
|
+
// Worker threads open the same path through their own instance of this
|
|
53
|
+
// module, so the descriptor is opened in append mode: O_APPEND lands every
|
|
54
|
+
// write at the end of the file no matter how many writers share it, where
|
|
55
|
+
// a truncating open gives each writer its own offset and they punch holes
|
|
56
|
+
// through each other's records. The main thread truncates once so a run
|
|
57
|
+
// still starts from an empty log.
|
|
58
|
+
if (isMainThread) {
|
|
59
|
+
try {
|
|
60
|
+
fs.truncateSync(filePath, 0);
|
|
61
|
+
} catch {
|
|
62
|
+
// No file yet: the append open below creates it.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fd = fs.openSync(filePath, "a");
|
|
66
|
+
} catch {
|
|
67
|
+
return fallbackStream ?? undefined;
|
|
68
|
+
}
|
|
69
|
+
const writer = new Writable({
|
|
70
|
+
write(chunk, _encoding, callback) {
|
|
71
|
+
if (fd !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
fs.writeSync(fd, chunk);
|
|
74
|
+
} catch {
|
|
75
|
+
// A full disk or a revoked descriptor must not abort BOM generation.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
callback();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
writer.closeSync = () => {
|
|
82
|
+
if (fd === undefined) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
fs.closeSync(fd);
|
|
87
|
+
} finally {
|
|
88
|
+
fd = undefined;
|
|
89
|
+
syncFileWriters.delete(filePath);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
syncFileWriters.set(filePath, writer);
|
|
93
|
+
return writer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Close every synchronous file writer opened by {@link openSyncFileWriter}.
|
|
98
|
+
* Safe to call more than once and after individual writers were closed.
|
|
99
|
+
*
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
|
102
|
+
export function closeAllSyncFileWriters() {
|
|
103
|
+
for (const writer of [...syncFileWriters.values()]) {
|
|
104
|
+
writer.closeSync();
|
|
105
|
+
}
|
|
106
|
+
}
|