@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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-facts collector for command shaping.
|
|
3
|
+
*
|
|
4
|
+
* Gathers, once per run and cached, the project facts the command resolver
|
|
5
|
+
* consumes: which maven and gradle wrapper scripts the project root carries,
|
|
6
|
+
* which Python manager owns its lock files, and which npm client the project
|
|
7
|
+
* names or locks. Detection is filesystem reads only — existence and, for
|
|
8
|
+
* the platform's own shell scripts, the executable bit. Nothing here spawns
|
|
9
|
+
* a process: whether a wrapper actually runs is a question the run itself
|
|
10
|
+
* answers elsewhere, and shaping that cannot know says so through the
|
|
11
|
+
* resolver's fallbacks.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
safeExistsSync,
|
|
18
|
+
safeIsExecutableSync,
|
|
19
|
+
safeReadFileSync,
|
|
20
|
+
} from "../../../core/fs.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The POSIX wrapper scripts whose executable bit is meaningful, mapped to
|
|
24
|
+
* the fact names the resolver reads.
|
|
25
|
+
*
|
|
26
|
+
* @type {ReadonlyArray<[string, string]>}
|
|
27
|
+
*/
|
|
28
|
+
const POSIX_WRAPPERS = [
|
|
29
|
+
["mvnw", "mvnw"],
|
|
30
|
+
["gradlew", "gradlew"],
|
|
31
|
+
["composer.phar", "composerPhar"],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The Windows wrapper batches whose presence is the practical usability
|
|
36
|
+
* test, mapped to the fact names the resolver reads.
|
|
37
|
+
*
|
|
38
|
+
* @type {ReadonlyArray<[string, string]>}
|
|
39
|
+
*/
|
|
40
|
+
const WINDOWS_WRAPPERS = [
|
|
41
|
+
["mvnw.cmd", "mvnwCmd"],
|
|
42
|
+
["gradlew.bat", "gradlewBat"],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Python managers detected through the lock file that owns them. Poetry is
|
|
47
|
+
* completed by the build backend its pyproject.toml declares, because a
|
|
48
|
+
* stray poetry.lock alone does not make a project poetry-managed.
|
|
49
|
+
*
|
|
50
|
+
* @type {Readonly<Record<string, string>>}
|
|
51
|
+
*/
|
|
52
|
+
const MANAGER_LOCK_FILES = Object.freeze({
|
|
53
|
+
"uv.lock": "uv",
|
|
54
|
+
"poetry.lock": "poetry",
|
|
55
|
+
"pdm.lock": "pdm",
|
|
56
|
+
"Pipfile.lock": "pipenv",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* pyproject.toml build backends that settle which competing manager governs
|
|
61
|
+
* the project.
|
|
62
|
+
*
|
|
63
|
+
* @type {Readonly<Record<string, string>>}
|
|
64
|
+
*/
|
|
65
|
+
const BACKEND_MANAGERS = Object.freeze({
|
|
66
|
+
"poetry.core.masonry.api": "poetry",
|
|
67
|
+
"pdm.backend": "pdm",
|
|
68
|
+
"pdm.pep517.api": "pdm",
|
|
69
|
+
uv: "uv",
|
|
70
|
+
uv_build: "uv",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Facts gathered so far, keyed by the project root they describe. */
|
|
74
|
+
const factsCache = new Map();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a path exists and, on POSIX, carries the executable bit for the
|
|
78
|
+
* current user. On Windows every readable file is runnable, so existence is
|
|
79
|
+
* the whole answer.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} filePath Path to check.
|
|
82
|
+
* @param {boolean} checkExecutable True when the executable bit matters.
|
|
83
|
+
* @returns {boolean} Usability of the path as an executable.
|
|
84
|
+
*/
|
|
85
|
+
function isUsableExecutable(filePath, checkExecutable) {
|
|
86
|
+
return checkExecutable
|
|
87
|
+
? safeIsExecutableSync(filePath)
|
|
88
|
+
: safeExistsSync(filePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read the build backend a pyproject.toml declares, from its
|
|
93
|
+
* `[build-system]` section only, so a mention elsewhere cannot answer.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} pyprojectPath Path to pyproject.toml.
|
|
96
|
+
* @returns {string|undefined} The declared backend, when present.
|
|
97
|
+
*/
|
|
98
|
+
function declaredBuildBackend(pyprojectPath) {
|
|
99
|
+
let content;
|
|
100
|
+
try {
|
|
101
|
+
content = safeReadFileSync(pyprojectPath, "utf-8");
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
let inBuildSystem = false;
|
|
106
|
+
for (const line of content.split(/\r?\n/)) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (trimmed.startsWith("[")) {
|
|
109
|
+
inBuildSystem = trimmed === "[build-system]";
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!inBuildSystem || !trimmed.startsWith("build-backend")) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const eq = trimmed.indexOf("=");
|
|
116
|
+
if (eq === -1) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return trimmed
|
|
120
|
+
.slice(eq + 1)
|
|
121
|
+
.trim()
|
|
122
|
+
.replace(/^["']|["']$/g, "")
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether a pyproject.toml declares a `[tool.<name>]` section, so a
|
|
130
|
+
* manager's own configuration marks the project as its own.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} pyprojectPath Path to pyproject.toml.
|
|
133
|
+
* @param {string} toolName Tool section name, e.g. `poetry`.
|
|
134
|
+
* @returns {boolean} True when the section is present.
|
|
135
|
+
*/
|
|
136
|
+
function hasToolSection(pyprojectPath, toolName) {
|
|
137
|
+
let content;
|
|
138
|
+
try {
|
|
139
|
+
content = safeReadFileSync(pyprojectPath, "utf-8");
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const section = `[tool.${toolName}]`;
|
|
144
|
+
for (const line of content.split(/\r?\n/)) {
|
|
145
|
+
const trimmed = line.trim();
|
|
146
|
+
if (trimmed === section || trimmed.startsWith(`${section.slice(0, -1)}.`)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Detect the Python manager in play at a project root: the manager whose
|
|
155
|
+
* lock file is present, with poetry confirmed by the `[tool.poetry]`
|
|
156
|
+
* section its pyproject.toml declares. Two competing managers are settled
|
|
157
|
+
* by the declared build backend; when it settles nothing, both candidates
|
|
158
|
+
* travel in the facts so the report can ask instead of guessing.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} projectRoot Directory to inspect.
|
|
161
|
+
* @returns {{manager: string|undefined, candidates: string[]}} The manager in play and every candidate detected.
|
|
162
|
+
*/
|
|
163
|
+
function detectPythonManager(projectRoot) {
|
|
164
|
+
const pyprojectPath = join(projectRoot, "pyproject.toml");
|
|
165
|
+
const hasPyproject = safeExistsSync(pyprojectPath);
|
|
166
|
+
const candidates = [];
|
|
167
|
+
for (const [lockName, manager] of Object.entries(MANAGER_LOCK_FILES)) {
|
|
168
|
+
if (!safeExistsSync(join(projectRoot, lockName))) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (manager === "poetry") {
|
|
172
|
+
if (!hasPyproject || !hasToolSection(pyprojectPath, "poetry")) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!candidates.includes(manager)) {
|
|
177
|
+
candidates.push(manager);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const ordered = candidates.sort();
|
|
181
|
+
if (ordered.length <= 1) {
|
|
182
|
+
return { manager: ordered[0], candidates: ordered };
|
|
183
|
+
}
|
|
184
|
+
const backend = hasPyproject
|
|
185
|
+
? declaredBuildBackend(pyprojectPath)
|
|
186
|
+
: undefined;
|
|
187
|
+
const settled = backend ? BACKEND_MANAGERS[backend] : undefined;
|
|
188
|
+
if (settled && ordered.includes(settled)) {
|
|
189
|
+
return { manager: settled, candidates: ordered };
|
|
190
|
+
}
|
|
191
|
+
return { manager: undefined, candidates: ordered };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detect the npm client a project root uses: the `packageManager` field of
|
|
196
|
+
* its package.json when the project names one, else the lock file present,
|
|
197
|
+
* else npm.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} projectRoot Directory to inspect.
|
|
200
|
+
* @returns {string} The npm client name.
|
|
201
|
+
*/
|
|
202
|
+
function detectNpmClient(projectRoot) {
|
|
203
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
204
|
+
if (safeExistsSync(packageJsonPath)) {
|
|
205
|
+
try {
|
|
206
|
+
const packageJson = JSON.parse(
|
|
207
|
+
safeReadFileSync(packageJsonPath, "utf-8"),
|
|
208
|
+
);
|
|
209
|
+
const pinned = `${packageJson?.packageManager || ""}`;
|
|
210
|
+
const at = pinned.indexOf("@");
|
|
211
|
+
const name = (at > 0 ? pinned.slice(0, at) : pinned).trim();
|
|
212
|
+
if (name) {
|
|
213
|
+
return name;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// An unreadable package.json leaves the lock files to answer.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const lockClients = [
|
|
220
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
221
|
+
["yarn.lock", "yarn"],
|
|
222
|
+
["bun.lockb", "bun"],
|
|
223
|
+
["bun.lock", "bun"],
|
|
224
|
+
["deno.lock", "deno"],
|
|
225
|
+
["package-lock.json", "npm"],
|
|
226
|
+
];
|
|
227
|
+
for (const [lockName, client] of lockClients) {
|
|
228
|
+
if (safeExistsSync(join(projectRoot, lockName))) {
|
|
229
|
+
return client;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return "npm";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Gather the command-shaping facts for a project root. The result is cached
|
|
237
|
+
* per root, so repeated scoring passes over the same project read the
|
|
238
|
+
* filesystem once.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} projectRoot Directory that was scanned.
|
|
241
|
+
* @param {Object} [options] Test hooks.
|
|
242
|
+
* @param {"posix"|"windows"} [options.platform] Platform override; defaults to the runtime platform.
|
|
243
|
+
* @returns {import("./shapeCommand.js").CommandFacts} Facts for the resolver.
|
|
244
|
+
*/
|
|
245
|
+
export function collectCommandFacts(projectRoot, options = {}) {
|
|
246
|
+
const platform =
|
|
247
|
+
options.platform ||
|
|
248
|
+
(globalThis.process?.platform === "win32" ? "windows" : "posix");
|
|
249
|
+
const cacheKey = `${platform}\n${projectRoot || ""}`;
|
|
250
|
+
const cached = factsCache.get(cacheKey);
|
|
251
|
+
if (cached) {
|
|
252
|
+
return cached;
|
|
253
|
+
}
|
|
254
|
+
const facts = {
|
|
255
|
+
projectRoot: projectRoot || "",
|
|
256
|
+
platform,
|
|
257
|
+
wrappers: {},
|
|
258
|
+
pythonManager: undefined,
|
|
259
|
+
pythonManagerCandidates: [],
|
|
260
|
+
npmClient: "npm",
|
|
261
|
+
};
|
|
262
|
+
if (projectRoot && safeExistsSync(projectRoot)) {
|
|
263
|
+
const checkExecutable = platform === "posix";
|
|
264
|
+
for (const [fileName, factName] of POSIX_WRAPPERS) {
|
|
265
|
+
const wrapperPath = join(projectRoot, fileName);
|
|
266
|
+
if (isUsableExecutable(wrapperPath, checkExecutable)) {
|
|
267
|
+
facts.wrappers[factName] = true;
|
|
268
|
+
} else if (safeExistsSync(wrapperPath)) {
|
|
269
|
+
facts.wrappers[`${factName}Inexecutable`] = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const [fileName, factName] of WINDOWS_WRAPPERS) {
|
|
273
|
+
if (isUsableExecutable(join(projectRoot, fileName), false)) {
|
|
274
|
+
facts.wrappers[factName] = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const python = detectPythonManager(projectRoot);
|
|
278
|
+
facts.pythonManager = python.manager;
|
|
279
|
+
facts.pythonManagerCandidates = python.candidates;
|
|
280
|
+
facts.npmClient = detectNpmClient(projectRoot);
|
|
281
|
+
}
|
|
282
|
+
factsCache.set(cacheKey, facts);
|
|
283
|
+
return facts;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Forget the gathered facts. Test hook; a production run gathers once.
|
|
288
|
+
*
|
|
289
|
+
* @returns {void}
|
|
290
|
+
*/
|
|
291
|
+
export function clearCommandFactsCache() {
|
|
292
|
+
factsCache.clear();
|
|
293
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report emission for build introspection: resolving report destinations,
|
|
3
|
+
* writing the markdown and JSON reports, and printing the console summary.
|
|
4
|
+
*
|
|
5
|
+
* This module is the only place in the introspection chain that touches the
|
|
6
|
+
* filesystem or prints, which keeps every renderer above it pure and
|
|
7
|
+
* testable against committed fixtures. All writes go through the safe
|
|
8
|
+
* wrappers, and dry-run mode produces the report without writing anything.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { isDryRun } from "../../../core/activity.js";
|
|
14
|
+
import {
|
|
15
|
+
safeExistsSync,
|
|
16
|
+
safeMkdirSync,
|
|
17
|
+
safeWriteSync,
|
|
18
|
+
} from "../../../core/fs.js";
|
|
19
|
+
import { diagnosticStream } from "../../../core/syncFileWriter.js";
|
|
20
|
+
import {
|
|
21
|
+
buildIntrospectionJson,
|
|
22
|
+
renderIntrospectionConsole,
|
|
23
|
+
renderIntrospectionMarkdown,
|
|
24
|
+
} from "./report.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Report destination meaning "write this report to the diagnostic stream".
|
|
28
|
+
*
|
|
29
|
+
* @type {string}
|
|
30
|
+
*/
|
|
31
|
+
export const INTROSPECTION_STDERR_TARGET = "-";
|
|
32
|
+
|
|
33
|
+
/** Default markdown report name when the run has no output file (`--print`). */
|
|
34
|
+
const DEFAULT_REPORT_NAME = "cdxgen-introspection.md";
|
|
35
|
+
/** Default JSON report name when the run has no output file (`--print`). */
|
|
36
|
+
const DEFAULT_JSON_NAME = "cdxgen-introspection.json";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the report destinations for a run. A user-provided path wins; the
|
|
40
|
+
* defaults are derived from the BOM output path so the reports travel with
|
|
41
|
+
* the BOM, falling back to the working directory when the BOM goes to stdout.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} [options] CLI options.
|
|
44
|
+
* @param {string} [options.output] Path the BOM is written to, or "-" for stdout.
|
|
45
|
+
* @param {string} [options.introspectReport] Requested markdown report path.
|
|
46
|
+
* @param {string} [options.introspectJson] Requested JSON report path.
|
|
47
|
+
* @returns {{reportPath: string, jsonPath: string}} Resolved destinations; "-" means the diagnostic stream.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveIntrospectionReportPaths(options = {}) {
|
|
50
|
+
const output =
|
|
51
|
+
typeof options?.output === "string" &&
|
|
52
|
+
options.output &&
|
|
53
|
+
options.output !== INTROSPECTION_STDERR_TARGET
|
|
54
|
+
? options.output
|
|
55
|
+
: undefined;
|
|
56
|
+
const reportPath =
|
|
57
|
+
options?.introspectReport ||
|
|
58
|
+
(output ? `${output}.introspection.md` : DEFAULT_REPORT_NAME);
|
|
59
|
+
const jsonPath =
|
|
60
|
+
options?.introspectJson ||
|
|
61
|
+
(output ? `${output}.introspection.json` : DEFAULT_JSON_NAME);
|
|
62
|
+
return { reportPath, jsonPath };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Last summary printed in this process, so a repeated verdict stays quiet. */
|
|
66
|
+
let lastPrintedSummary;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Serialize the JSON report the way it is written to disk: stable key order
|
|
70
|
+
* comes from the renderer, two-space indentation from the document convention.
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} report Report document from buildIntrospectionJson.
|
|
73
|
+
* @returns {string} Serialized report.
|
|
74
|
+
*/
|
|
75
|
+
function serializeReport(report) {
|
|
76
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Write text to a path, creating the parent directory when needed.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} filePath Destination path.
|
|
83
|
+
* @param {string} content File content.
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
function writeReportFile(filePath, content) {
|
|
87
|
+
const parentDir = dirname(filePath);
|
|
88
|
+
if (parentDir && !safeExistsSync(parentDir)) {
|
|
89
|
+
safeMkdirSync(parentDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
safeWriteSync(filePath, content);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deliver one report to its destination: the diagnostic stream for "-", the
|
|
96
|
+
* named file otherwise. Returns the destination actually used so the console
|
|
97
|
+
* summary names it.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} target Requested destination.
|
|
100
|
+
* @param {string} content Report text.
|
|
101
|
+
* @returns {string} The destination the report reached.
|
|
102
|
+
*/
|
|
103
|
+
function deliverReport(target, content) {
|
|
104
|
+
if (target === INTROSPECTION_STDERR_TARGET) {
|
|
105
|
+
diagnosticStream.write(content);
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
writeReportFile(target, content);
|
|
109
|
+
return target;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Mark every remediation in a scoring document blocked because a dry-run
|
|
114
|
+
* records intent without executing anything: no fix the report proposes can
|
|
115
|
+
* be applied by this run, so the loop must not try.
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} scored Scoring document from scoreReflection, mutated in place.
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
120
|
+
export function blockRemediationsForDryRun(scored) {
|
|
121
|
+
const remediations = Array.isArray(scored?.remediations)
|
|
122
|
+
? scored.remediations
|
|
123
|
+
: [];
|
|
124
|
+
for (const entry of remediations) {
|
|
125
|
+
if (entry?.blocked === true) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
entry.blocked = true;
|
|
129
|
+
entry.blockedReason =
|
|
130
|
+
"policy.dry-run: dry-run mode records intent without executing commands, so no fix can be applied by this run";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write the markdown and JSON reports for a completed reflection and return
|
|
136
|
+
* the destinations they reached. Dry-run mode prints the markdown report to
|
|
137
|
+
* the diagnostic stream instead of creating files, matching the read-only
|
|
138
|
+
* contract of the mode.
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} reflection Reflection document from reflectOnRun.
|
|
141
|
+
* @param {Object} scored Scoring document from scoreReflection.
|
|
142
|
+
* @param {Object} [options] CLI options naming the report destinations.
|
|
143
|
+
* @returns {{reportTarget: string|undefined, jsonTarget: string|undefined, dryRun: boolean}} Destinations reached; undefined means the report was not produced as a file.
|
|
144
|
+
*/
|
|
145
|
+
export function emitIntrospectionReports(reflection, scored, options = {}) {
|
|
146
|
+
const { reportPath, jsonPath } = resolveIntrospectionReportPaths(options);
|
|
147
|
+
const markdown = renderIntrospectionMarkdown(reflection, scored, options);
|
|
148
|
+
if (isDryRun) {
|
|
149
|
+
diagnosticStream.write(`${markdown}\n`);
|
|
150
|
+
return { reportTarget: undefined, jsonTarget: undefined, dryRun: true };
|
|
151
|
+
}
|
|
152
|
+
const reportTarget = deliverReport(reportPath, markdown);
|
|
153
|
+
const jsonTarget = deliverReport(
|
|
154
|
+
jsonPath,
|
|
155
|
+
serializeReport(buildIntrospectionJson(reflection, scored, options)),
|
|
156
|
+
);
|
|
157
|
+
return { reportTarget, jsonTarget, dryRun: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Print the console summary for a completed introspection. Called after the
|
|
162
|
+
* reports are delivered, so every path it names exists (or is the explicit
|
|
163
|
+
* stream marker). Diagnostics go to the diagnostic stream so a piped stdout
|
|
164
|
+
* never carries anything but the BOM payload.
|
|
165
|
+
*
|
|
166
|
+
* @param {Object} scored Scoring document from scoreReflection.
|
|
167
|
+
* @param {Object} [delivery] Destinations returned by emitIntrospectionReports.
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
export function printIntrospectionSummary(scored, delivery = {}) {
|
|
171
|
+
const lines = [
|
|
172
|
+
renderIntrospectionConsole(scored, {
|
|
173
|
+
introspectReport: delivery.reportTarget,
|
|
174
|
+
introspectJson: delivery.jsonTarget,
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
if (delivery.dryRun) {
|
|
178
|
+
lines.push(
|
|
179
|
+
"Build introspection: dry-run mode produced the report above without writing any file\n",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const summary = lines.join("");
|
|
183
|
+
// Evidence collection re-enters postProcess with an enriched BOM, so a run
|
|
184
|
+
// reflects more than once and the later verdict supersedes the earlier one.
|
|
185
|
+
// The reports are rewritten each time; the summary is printed only when it
|
|
186
|
+
// says something the reader has not already been told.
|
|
187
|
+
if (summary === lastPrintedSummary) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
lastPrintedSummary = summary;
|
|
191
|
+
diagnosticStream.write(summary);
|
|
192
|
+
}
|