@cdxgen/cdxgen 13.0.0 → 13.1.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 +26 -13
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +140 -8
- package/bin/convert.js +7 -0
- package/bin/evinse.js +1 -1
- package/bin/hbom.js +13 -4
- package/bin/tracebom.js +9 -1
- package/data/component-tags.json +8 -1
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1144 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -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/cli/bomAssembly.js +83 -15
- package/lib/cli/cliOptions.js +124 -7
- package/lib/cli/index.js +76 -48
- package/lib/cli/jsBom.js +32 -3
- package/lib/cli/jvmBom.js +1494 -1315
- package/lib/cli/managedBom.js +97 -28
- package/lib/cli/nativeBom.js +189 -10
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +69 -1
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +76 -25
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/ui.js +46 -2
- package/lib/ecosystems/ecosystems.js +9 -0
- package/lib/ecosystems/gradleutils.js +355 -15
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-misc.js +16 -2
- package/lib/ecosystems/parsers-python.js +171 -35
- package/lib/ecosystems/parsers-rust.js +12 -2
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/utils.js +9 -1
- package/lib/evinser/evinser.js +30 -12
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/auditCategories.js +9 -0
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +431 -22
- package/lib/inventory/envcontext.js +211 -19
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +123 -16
- package/lib/inventory/table.js +9 -5
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +216 -40
- package/lib/managers/docker.js +26 -5
- 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 +227 -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 +115 -2
- 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/validator/bomValidator.js +11 -2
- package/package.json +14 -14
- 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/cli/bomAssembly.d.ts +14 -0
- 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 +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/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 +22 -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 +14 -1
- 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/ui.d.ts +1 -1
- package/types/lib/core/ui.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +33 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- 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 +20 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/utils.d.ts +4 -4
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +5 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- 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/auditCategories.d.ts +8 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +20 -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/fetchBatch.d.ts +9 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts +83 -2
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/table.d.ts +4 -1
- package/types/lib/inventory/table.d.ts.map +1 -1
- 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 +71 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- 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 +57 -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 +2 -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
|
}
|
|
@@ -459,6 +485,39 @@ export const PACKAGE_MANAGER_ALIASES = {
|
|
|
459
485
|
scala: ["sbt"],
|
|
460
486
|
};
|
|
461
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Project-type prefixes that accept a version suffix pinning a JVM build
|
|
490
|
+
* tool, e.g. `maven3.9.9`, `mvn3.9.9`, `gradle8.14`, `sbt1.10`, or
|
|
491
|
+
* `scala3.6.4`. Kept alongside the alias map because both describe how CLI
|
|
492
|
+
* project types map to base types.
|
|
493
|
+
*
|
|
494
|
+
* @type {string[]}
|
|
495
|
+
*/
|
|
496
|
+
export const JVM_BUILD_TOOL_TYPE_PREFIXES = [
|
|
497
|
+
"maven",
|
|
498
|
+
"mvn",
|
|
499
|
+
"gradle",
|
|
500
|
+
"sbt",
|
|
501
|
+
"scala",
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check whether a project type pins a JVM build tool version, i.e. it starts
|
|
506
|
+
* with one of the JVM build tool prefixes followed by a digit.
|
|
507
|
+
*
|
|
508
|
+
* @param {string} projectType Project type from the CLI
|
|
509
|
+
* @returns {boolean} True for versioned JVM build tool types.
|
|
510
|
+
*/
|
|
511
|
+
export function isVersionedJvmToolProjectType(projectType) {
|
|
512
|
+
const pt = `${projectType || ""}`.toLowerCase();
|
|
513
|
+
for (const prefix of JVM_BUILD_TOOL_TYPE_PREFIXES) {
|
|
514
|
+
if (pt.startsWith(prefix) && /^\d/.test(pt.slice(prefix.length))) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
462
521
|
/**
|
|
463
522
|
* Method to check if a given feature flag is enabled.
|
|
464
523
|
*
|
|
@@ -553,6 +612,15 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
|
|
|
553
612
|
) {
|
|
554
613
|
baseProjectTypes.push("ruby");
|
|
555
614
|
}
|
|
615
|
+
// Versioned JVM build tool types such as maven3.9.9 or gradle8.14 are java
|
|
616
|
+
// types with a pinned build tool, so they must dispatch java parsing too.
|
|
617
|
+
if (
|
|
618
|
+
options.projectType?.length &&
|
|
619
|
+
projectTypes.includes("java") &&
|
|
620
|
+
options.projectType.some((pt) => isVersionedJvmToolProjectType(pt))
|
|
621
|
+
) {
|
|
622
|
+
baseProjectTypes.push("java");
|
|
623
|
+
}
|
|
556
624
|
const baseExcludeTypes = [];
|
|
557
625
|
for (const abt of Object.keys(PROJECT_TYPE_ALIASES)) {
|
|
558
626
|
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,17 +1,18 @@
|
|
|
1
1
|
import { Console } from "node:console";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import process from "node:process";
|
|
4
3
|
|
|
4
|
+
import {
|
|
5
|
+
closeAllSyncFileWriters,
|
|
6
|
+
diagnosticStream,
|
|
7
|
+
openSyncFileWriter,
|
|
8
|
+
} from "./syncFileWriter.js";
|
|
9
|
+
|
|
10
|
+
export { diagnosticStream };
|
|
11
|
+
|
|
5
12
|
// Environment variables here are read with raw `process.env` rather than
|
|
6
13
|
// `readEnvironmentVariable`: that helper lives in activity.js, which imports
|
|
7
14
|
// this module, so routing these through it would form an import cycle.
|
|
8
15
|
|
|
9
|
-
// Stream contract: human-readable diagnostics default to stderr so stdout
|
|
10
|
-
// carries only the BOM payload. CDXGEN_LOG_STREAM=stdout restores the
|
|
11
|
-
// pre-v13 behaviour for callers that scraped cdxgen's stdout.
|
|
12
|
-
const logStream =
|
|
13
|
-
process.env.CDXGEN_LOG_STREAM === "stdout" ? process.stdout : process.stderr;
|
|
14
|
-
|
|
15
16
|
// Custom lightweight colors formatting helper replacing yoctocolors
|
|
16
17
|
const colors = {
|
|
17
18
|
cyanBright: (text) => `\x1b[96m${text}\x1b[39m`,
|
|
@@ -25,10 +26,10 @@ export const THINK_MODE =
|
|
|
25
26
|
process.env.CDXGEN_DEBUG_MODE === "verbose";
|
|
26
27
|
|
|
27
28
|
const thinkOutput = process.env.CDXGEN_THOUGHT_LOG
|
|
28
|
-
?
|
|
29
|
-
:
|
|
29
|
+
? openSyncFileWriter(process.env.CDXGEN_THOUGHT_LOG)
|
|
30
|
+
: diagnosticStream;
|
|
30
31
|
const thinkErrorOutput = process.env.CDXGEN_THOUGHT_LOG
|
|
31
|
-
?
|
|
32
|
+
? thinkOutput
|
|
32
33
|
: process.stderr;
|
|
33
34
|
const thinkLogger = new Console({
|
|
34
35
|
stdout: thinkOutput,
|
|
@@ -44,10 +45,10 @@ export const TRACE_MODE =
|
|
|
44
45
|
process.env.CDXGEN_DEBUG_MODE === "verbose";
|
|
45
46
|
|
|
46
47
|
const traceOutput = process.env.CDXGEN_TRACE_LOG
|
|
47
|
-
?
|
|
48
|
-
:
|
|
48
|
+
? openSyncFileWriter(process.env.CDXGEN_TRACE_LOG)
|
|
49
|
+
: diagnosticStream;
|
|
49
50
|
const traceErrorOutput = process.env.CDXGEN_TRACE_LOG
|
|
50
|
-
?
|
|
51
|
+
? traceOutput
|
|
51
52
|
: process.stderr;
|
|
52
53
|
const traceLogger = new Console({
|
|
53
54
|
stdout: traceOutput,
|
|
@@ -80,14 +81,18 @@ export function thoughtLog(s, args) {
|
|
|
80
81
|
thinkLogger.log(colorizeText(`${s}`));
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
let thoughtBlockClosed = false;
|
|
85
|
+
|
|
83
86
|
/**
|
|
84
87
|
* Closes the think log group by emitting the closing `</think>` marker.
|
|
85
|
-
* Has no effect if THINK_MODE is not enabled
|
|
88
|
+
* Has no effect if THINK_MODE is not enabled, and emits the marker at most
|
|
89
|
+
* once so an explicit call followed by the exit handler cannot produce two.
|
|
86
90
|
*
|
|
87
91
|
* @returns {void}
|
|
88
92
|
*/
|
|
89
93
|
export function thoughtEnd() {
|
|
90
|
-
if (THINK_MODE) {
|
|
94
|
+
if (THINK_MODE && !thoughtBlockClosed) {
|
|
95
|
+
thoughtBlockClosed = true;
|
|
91
96
|
thinkLogger.groupEnd();
|
|
92
97
|
thinkLogger.log(colorizeText("</think>"));
|
|
93
98
|
}
|
|
@@ -96,13 +101,40 @@ export function thoughtEnd() {
|
|
|
96
101
|
function colorizeText(s) {
|
|
97
102
|
// Only colorize for an interactive terminal stream; piped/CI/file output
|
|
98
103
|
// stays free of escape bytes so the diagnostic stream is grep-safe.
|
|
99
|
-
if (process.env.CDXGEN_THOUGHT_LOG || !
|
|
104
|
+
if (process.env.CDXGEN_THOUGHT_LOG || !diagnosticStream.isTTY) {
|
|
100
105
|
return s;
|
|
101
106
|
}
|
|
102
107
|
s = s.replace(/(\d+)/g, colors.cyanBright("$1"));
|
|
103
108
|
return colors.dim(s);
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
// Fields serialized for each trace type. The allow-list is per-type because a
|
|
112
|
+
// single shared list either drops the fields a type exists to carry or leaks
|
|
113
|
+
// unbounded caller objects into the stream: `activity` and `phase` records
|
|
114
|
+
// carry no field in common with `spawn` and `http`.
|
|
115
|
+
const TRACE_FIELDS = {
|
|
116
|
+
activity: [
|
|
117
|
+
"identifier",
|
|
118
|
+
"kind",
|
|
119
|
+
"status",
|
|
120
|
+
"target",
|
|
121
|
+
"reason",
|
|
122
|
+
"networkIntent",
|
|
123
|
+
"command",
|
|
124
|
+
"cwd",
|
|
125
|
+
],
|
|
126
|
+
phase: ["phase", "state", "detail", "note", "done", "total", "elapsedMs"],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const DEFAULT_TRACE_FIELDS = [
|
|
130
|
+
"command",
|
|
131
|
+
"cwd",
|
|
132
|
+
"protocol",
|
|
133
|
+
"host",
|
|
134
|
+
"path",
|
|
135
|
+
"pathname",
|
|
136
|
+
];
|
|
137
|
+
|
|
106
138
|
/**
|
|
107
139
|
* Log trace messages
|
|
108
140
|
*
|
|
@@ -123,15 +155,10 @@ export function traceLog(traceType, args) {
|
|
|
123
155
|
message.type = traceType;
|
|
124
156
|
}
|
|
125
157
|
if (args) {
|
|
126
|
-
for (const k of [
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"
|
|
130
|
-
"host",
|
|
131
|
-
"path",
|
|
132
|
-
"pathname",
|
|
133
|
-
]) {
|
|
134
|
-
if (args[k]) {
|
|
158
|
+
for (const k of TRACE_FIELDS[traceType] || DEFAULT_TRACE_FIELDS) {
|
|
159
|
+
// Numeric progress counters are meaningful at zero, so presence is
|
|
160
|
+
// tested rather than truthiness.
|
|
161
|
+
if (args[k] !== undefined && args[k] !== null && args[k] !== "") {
|
|
135
162
|
message[k] = args[k];
|
|
136
163
|
}
|
|
137
164
|
}
|
|
@@ -140,3 +167,27 @@ export function traceLog(traceType, args) {
|
|
|
140
167
|
traceLogger.log(JSON.stringify(message, null, null));
|
|
141
168
|
}
|
|
142
169
|
}
|
|
170
|
+
|
|
171
|
+
let logStreamsClosed = false;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Terminate the think block and close the log file descriptors.
|
|
175
|
+
*
|
|
176
|
+
* Readers tail these files as a live feed, so a run that ends without the
|
|
177
|
+
* closing `</think>` marker leaves the consumer holding an unterminated block.
|
|
178
|
+
* Registered on process exit and safe to call more than once.
|
|
179
|
+
*
|
|
180
|
+
* @returns {void}
|
|
181
|
+
*/
|
|
182
|
+
export function closeLogStreams() {
|
|
183
|
+
if (logStreamsClosed) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
logStreamsClosed = true;
|
|
187
|
+
thoughtEnd();
|
|
188
|
+
closeAllSyncFileWriters();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The writers are synchronous, so this handler only has to emit the terminator
|
|
192
|
+
// and release the descriptors — work that is legal during `exit`.
|
|
193
|
+
process.on("exit", closeLogStreams);
|
|
@@ -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
|
+
}
|