@cdxgen/cdxgen 13.0.1 → 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 +125 -4
- package/bin/evinse.js +1 -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 +20 -1
- package/lib/cli/cliOptions.js +94 -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 +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- 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/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 +206 -18
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- 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.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 +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/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/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.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.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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command shaping for remediation actions.
|
|
3
|
+
*
|
|
4
|
+
* The remediation catalog authors commands with template variables where the
|
|
5
|
+
* right value depends on the scanned project: which maven or gradle
|
|
6
|
+
* executable the repo wraps, which Python manager owns its lock files, which
|
|
7
|
+
* client npm itself should use. This module substitutes those variables from
|
|
8
|
+
* a facts object the caller gathered earlier, and names on every shaped
|
|
9
|
+
* command the detection it relied on, so a reviewer can tell a correct
|
|
10
|
+
* detection from a lucky default.
|
|
11
|
+
*
|
|
12
|
+
* Every function here is pure: the facts object is the only input beyond the
|
|
13
|
+
* template string, nothing reads the filesystem, and the same facts always
|
|
14
|
+
* yield the same command. The filesystem reads live in the collector
|
|
15
|
+
* (`commandFacts.js`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Template variables this module substitutes, beside the `{{version}}` and
|
|
20
|
+
* `{{major}}` placeholders the version cascade fills.
|
|
21
|
+
*
|
|
22
|
+
* @type {Readonly<string[]>}
|
|
23
|
+
*/
|
|
24
|
+
export const SHAPE_VARIABLES = Object.freeze([
|
|
25
|
+
"mvn",
|
|
26
|
+
"gradle",
|
|
27
|
+
"pythonManager",
|
|
28
|
+
"npmClient",
|
|
29
|
+
"go",
|
|
30
|
+
"cargo",
|
|
31
|
+
"composer",
|
|
32
|
+
"dart",
|
|
33
|
+
"mix",
|
|
34
|
+
"cabal",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Python managers a command can name, with the lock command each catalog
|
|
39
|
+
* entry pairs the variable with.
|
|
40
|
+
*
|
|
41
|
+
* @type {Readonly<string[]>}
|
|
42
|
+
*/
|
|
43
|
+
export const PYTHON_MANAGERS = Object.freeze(["uv", "poetry", "pdm", "pipenv"]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Project facts one shaping pass consumes. Produced once per run by the
|
|
47
|
+
* collector; every field is optional so a caller without collector facts
|
|
48
|
+
* still resolves the fallbacks.
|
|
49
|
+
*
|
|
50
|
+
* @typedef {Object} CommandFacts
|
|
51
|
+
* @property {string} [projectRoot] Directory the facts were gathered from.
|
|
52
|
+
* @property {"posix"|"windows"} [platform] Platform the command targets; defaults to posix.
|
|
53
|
+
* @property {Object} [wrappers] Wrapper presence facts, executable-bit included for the platform's script.
|
|
54
|
+
* @property {boolean} [wrappers.mvnw] ./mvnw exists and is executable.
|
|
55
|
+
* @property {boolean} [wrappers.mvnwInexecutable] ./mvnw exists but is not executable.
|
|
56
|
+
* @property {boolean} [wrappers.mvnwCmd] mvnw.cmd exists (Windows variant).
|
|
57
|
+
* @property {boolean} [wrappers.gradlew] ./gradlew exists and is executable.
|
|
58
|
+
* @property {boolean} [wrappers.gradlewInexecutable] ./gradlew exists but is not executable.
|
|
59
|
+
* @property {boolean} [wrappers.gradlewBat] gradlew.bat exists (Windows variant).
|
|
60
|
+
* @property {boolean} [wrappers.composerPhar] ./composer.phar exists and is executable.
|
|
61
|
+
* @property {string|undefined} [pythonManager] The Python manager in play.
|
|
62
|
+
* @property {string[]} [pythonManagerCandidates] Every manager detected, when two or more compete.
|
|
63
|
+
* @property {string} [npmClient] The npm client the project names or locks.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/** Wrapper executable the POSIX {{mvn}} variable resolves to. */
|
|
67
|
+
const MAVEN_WRAPPER_POSIX = "./mvnw";
|
|
68
|
+
/** Wrapper executable the Windows {{mvn}} variable resolves to. */
|
|
69
|
+
const MAVEN_WRAPPER_WINDOWS = "mvnw.cmd";
|
|
70
|
+
/** Wrapper executable the POSIX {{gradle}} variable resolves to. */
|
|
71
|
+
const GRADLE_WRAPPER_POSIX = "./gradlew";
|
|
72
|
+
/** Wrapper executable the Windows {{gradle}} variable resolves to. */
|
|
73
|
+
const GRADLE_WRAPPER_WINDOWS = "gradlew.bat";
|
|
74
|
+
/** Wrapper executable the POSIX {{composer}} variable resolves to. */
|
|
75
|
+
const COMPOSER_PHAR_POSIX = "./composer.phar";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The facts for the platform one command variant targets: the collector's
|
|
79
|
+
* platform fact describes the run, while a Windows command string needs
|
|
80
|
+
* Windows resolution even on a POSIX run.
|
|
81
|
+
*
|
|
82
|
+
* @param {CommandFacts} facts Gathered facts.
|
|
83
|
+
* @param {boolean} windows True for a Windows command variant.
|
|
84
|
+
* @returns {CommandFacts} Facts pinned to the requested platform.
|
|
85
|
+
*/
|
|
86
|
+
function factsForPlatform(facts, windows) {
|
|
87
|
+
const platform = windows
|
|
88
|
+
? "windows"
|
|
89
|
+
: facts?.platform === "windows"
|
|
90
|
+
? "windows"
|
|
91
|
+
: "posix";
|
|
92
|
+
return { ...(facts || {}), platform };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the `{{mvn}}` variable for one platform: the project's wrapper
|
|
97
|
+
* when it is present and usable, the plain executable otherwise.
|
|
98
|
+
*
|
|
99
|
+
* @param {CommandFacts} facts Platform-pinned facts.
|
|
100
|
+
* @returns {{value: string, shapedBy: string|undefined}} Substitute and the detection behind it, when there was one.
|
|
101
|
+
*/
|
|
102
|
+
function resolveMaven(facts) {
|
|
103
|
+
const windows = facts.platform === "windows";
|
|
104
|
+
const wrapper = windows ? facts?.wrappers?.mvnwCmd : facts?.wrappers?.mvnw;
|
|
105
|
+
if (wrapper) {
|
|
106
|
+
return {
|
|
107
|
+
value: windows ? MAVEN_WRAPPER_WINDOWS : MAVEN_WRAPPER_POSIX,
|
|
108
|
+
shapedBy: `wrapper:${windows ? MAVEN_WRAPPER_WINDOWS : MAVEN_WRAPPER_POSIX}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!windows && facts?.wrappers?.mvnwInexecutable) {
|
|
112
|
+
return {
|
|
113
|
+
value: "mvn",
|
|
114
|
+
shapedBy: `wrapper-not-executable:${MAVEN_WRAPPER_POSIX}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { value: "mvn", shapedBy: undefined };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the `{{gradle}}` variable for one platform: the project's wrapper
|
|
122
|
+
* when it is present and usable, the plain executable otherwise.
|
|
123
|
+
*
|
|
124
|
+
* @param {CommandFacts} facts Platform-pinned facts.
|
|
125
|
+
* @returns {{value: string, shapedBy: string|undefined}} Substitute and the detection behind it, when there was one.
|
|
126
|
+
*/
|
|
127
|
+
function resolveGradle(facts) {
|
|
128
|
+
const windows = facts.platform === "windows";
|
|
129
|
+
const wrapper = windows
|
|
130
|
+
? facts?.wrappers?.gradlewBat
|
|
131
|
+
: facts?.wrappers?.gradlew;
|
|
132
|
+
if (wrapper) {
|
|
133
|
+
return {
|
|
134
|
+
value: windows ? GRADLE_WRAPPER_WINDOWS : GRADLE_WRAPPER_POSIX,
|
|
135
|
+
shapedBy: `wrapper:${windows ? GRADLE_WRAPPER_WINDOWS : GRADLE_WRAPPER_POSIX}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (!windows && facts?.wrappers?.gradlewInexecutable) {
|
|
139
|
+
return {
|
|
140
|
+
value: "gradle",
|
|
141
|
+
shapedBy: `wrapper-not-executable:${GRADLE_WRAPPER_POSIX}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { value: "gradle", shapedBy: undefined };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the `{{pythonManager}}` variable: the manager the collector
|
|
149
|
+
* detected, left unresolved when two managers compete and the project does
|
|
150
|
+
* not settle the question, or when none was detected.
|
|
151
|
+
*
|
|
152
|
+
* @param {CommandFacts} facts Platform-pinned facts.
|
|
153
|
+
* @returns {{value: string|undefined, shapedBy: string|undefined, shapedNote: string|undefined}} Substitute, the detection behind it, and the question for the agent when detection could not answer.
|
|
154
|
+
*/
|
|
155
|
+
function resolvePythonManager(facts) {
|
|
156
|
+
const manager = facts?.pythonManager;
|
|
157
|
+
if (manager) {
|
|
158
|
+
return {
|
|
159
|
+
value: manager,
|
|
160
|
+
shapedBy: `manager:${manager}`,
|
|
161
|
+
shapedNote: undefined,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const candidates = Array.isArray(facts?.pythonManagerCandidates)
|
|
165
|
+
? facts.pythonManagerCandidates.filter(Boolean)
|
|
166
|
+
: [];
|
|
167
|
+
if (candidates.length > 1) {
|
|
168
|
+
return {
|
|
169
|
+
value: undefined,
|
|
170
|
+
shapedBy: "manager:ambiguous",
|
|
171
|
+
shapedNote: `this project carries the lock files of ${candidates.join(" and ")}; confirm which one manages it before locking`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
value: undefined,
|
|
176
|
+
shapedBy: "manager:unresolved",
|
|
177
|
+
shapedNote: undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the `{{npmClient}}` variable: the client the project names or
|
|
183
|
+
* locks, npm when nothing says otherwise.
|
|
184
|
+
*
|
|
185
|
+
* @param {CommandFacts} facts Platform-pinned facts.
|
|
186
|
+
* @returns {{value: string, shapedBy: string|undefined}} Substitute and the detection behind it, when there was one.
|
|
187
|
+
*/
|
|
188
|
+
function resolveNpmClient(facts) {
|
|
189
|
+
const client = facts?.npmClient || "npm";
|
|
190
|
+
return {
|
|
191
|
+
value: client,
|
|
192
|
+
shapedBy: client === "npm" ? undefined : `npm-client:${client}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve the `{{composer}}` variable: the project's local composer.phar when
|
|
198
|
+
* it is present and usable, the plain executable otherwise.
|
|
199
|
+
*
|
|
200
|
+
* @param {CommandFacts} facts Platform-pinned facts.
|
|
201
|
+
* @returns {{value: string, shapedBy: string|undefined}} Substitute and the detection behind it, when there was one.
|
|
202
|
+
*/
|
|
203
|
+
function resolveComposer(facts) {
|
|
204
|
+
const windows = facts.platform === "windows";
|
|
205
|
+
if (!windows && facts?.wrappers?.composerPhar) {
|
|
206
|
+
return {
|
|
207
|
+
value: COMPOSER_PHAR_POSIX,
|
|
208
|
+
shapedBy: `wrapper:${COMPOSER_PHAR_POSIX}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { value: "composer", shapedBy: undefined };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolve a tool variable whose executable no wrapper or version manager in
|
|
216
|
+
* the facts overrides: the plain executable name.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} executable The tool's executable name.
|
|
219
|
+
* @returns {(facts: CommandFacts) => {value: string, shapedBy: string|undefined}} Resolver for that tool's variable.
|
|
220
|
+
*/
|
|
221
|
+
function resolvePlainExecutable(executable) {
|
|
222
|
+
return () => ({ value: executable, shapedBy: undefined });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const VARIABLE_RESOLVERS = {
|
|
226
|
+
mvn: resolveMaven,
|
|
227
|
+
gradle: resolveGradle,
|
|
228
|
+
pythonManager: resolvePythonManager,
|
|
229
|
+
npmClient: resolveNpmClient,
|
|
230
|
+
go: resolvePlainExecutable("go"),
|
|
231
|
+
cargo: resolvePlainExecutable("cargo"),
|
|
232
|
+
composer: resolveComposer,
|
|
233
|
+
dart: resolvePlainExecutable("dart"),
|
|
234
|
+
mix: resolvePlainExecutable("mix"),
|
|
235
|
+
cabal: resolvePlainExecutable("cabal"),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Substitute one template variable, leaving an unresolved `{{pythonManager}}`
|
|
240
|
+
* verbatim so the command stays an honest question.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} command Command carrying the variable.
|
|
243
|
+
* @param {string} variable Variable name without braces.
|
|
244
|
+
* @param {CommandFacts} facts Platform-pinned facts.
|
|
245
|
+
* @returns {{command: string, shapedBy: string|undefined, shapedNote: string|undefined}} Command after substitution.
|
|
246
|
+
*/
|
|
247
|
+
function substituteVariable(command, variable, facts) {
|
|
248
|
+
const placeholder = `{{${variable}}}`;
|
|
249
|
+
if (!command.includes(placeholder)) {
|
|
250
|
+
return { command, shapedBy: undefined, shapedNote: undefined };
|
|
251
|
+
}
|
|
252
|
+
const resolved = VARIABLE_RESOLVERS[variable](facts);
|
|
253
|
+
if (!resolved.value) {
|
|
254
|
+
return {
|
|
255
|
+
command,
|
|
256
|
+
shapedBy: resolved.shapedBy,
|
|
257
|
+
shapedNote: resolved.shapedNote,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
command: command.split(placeholder).join(resolved.value),
|
|
262
|
+
shapedBy: resolved.shapedBy,
|
|
263
|
+
shapedNote: resolved.shapedNote,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Shape one command string: substitute the template variables the catalog
|
|
269
|
+
* authors into it and report which detection drove the result. A command
|
|
270
|
+
* without any `{{` marker is returned byte-identical and unshaped.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} templateCommand Catalog command template.
|
|
273
|
+
* @param {CommandFacts} [facts] Facts gathered for the scanned project.
|
|
274
|
+
* @returns {{command: string, shapedBy: string|undefined, shapedNote: string|undefined}} The shaped command, the detection behind it, and the question to surface when shaping could not answer.
|
|
275
|
+
*/
|
|
276
|
+
export function shapeCommand(templateCommand, facts = {}) {
|
|
277
|
+
if (typeof templateCommand !== "string" || !templateCommand.includes("{{")) {
|
|
278
|
+
return {
|
|
279
|
+
command: templateCommand,
|
|
280
|
+
shapedBy: undefined,
|
|
281
|
+
shapedNote: undefined,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const windows = facts?.platform === "windows";
|
|
285
|
+
const platformFacts = factsForPlatform(facts, windows);
|
|
286
|
+
let command = templateCommand;
|
|
287
|
+
const shapedBy = new Set();
|
|
288
|
+
const notes = new Set();
|
|
289
|
+
for (const variable of SHAPE_VARIABLES) {
|
|
290
|
+
const result = substituteVariable(command, variable, platformFacts);
|
|
291
|
+
command = result.command;
|
|
292
|
+
if (result.shapedBy) {
|
|
293
|
+
shapedBy.add(result.shapedBy);
|
|
294
|
+
}
|
|
295
|
+
if (result.shapedNote) {
|
|
296
|
+
notes.add(result.shapedNote);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
command,
|
|
301
|
+
shapedBy: shapedBy.size ? [...shapedBy].sort().join("; ") : undefined,
|
|
302
|
+
shapedNote: notes.size ? [...notes].sort().join("; ") : undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root reachability repair for the `dependencies` graph.
|
|
3
|
+
*
|
|
4
|
+
* CycloneDX expects `metadata.component` to appear in `dependencies` with the
|
|
5
|
+
* first level of the tree in its `dependsOn`. That edge is emitted by each
|
|
6
|
+
* ecosystem leg from whatever manifest declares the direct dependencies, so a
|
|
7
|
+
* leg that resolves a project entirely from a lock file, an export command, or
|
|
8
|
+
* a pre-resolved graph can produce a complete transitive tree that no consumer
|
|
9
|
+
* can enter: every edge is present, but nothing is reachable from the root.
|
|
10
|
+
*
|
|
11
|
+
* This module repairs that shape at finalization time, independent of the
|
|
12
|
+
* ecosystem. When the parent has no outgoing edge and the tree does contain
|
|
13
|
+
* edges, the components that nothing else points to are the roots of the graph,
|
|
14
|
+
* and attaching them to the parent restores reachability for the whole tree.
|
|
15
|
+
*
|
|
16
|
+
* The repair is deliberately weaker than a leg that knows the manifest: a
|
|
17
|
+
* direct dependency that some transitive dependency also requires is not a
|
|
18
|
+
* root here and stays out of the parent's `dependsOn`. It is a safety net that
|
|
19
|
+
* keeps a tree traversable, not a replacement for emitting the edge at the
|
|
20
|
+
* source, and it records what it inferred so consumers can tell the two apart.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readEnvironmentVariable } from "../../core/activity.js";
|
|
24
|
+
import { thoughtLog } from "../../core/logger.js";
|
|
25
|
+
|
|
26
|
+
/** Property recording how many root edges were inferred rather than observed. */
|
|
27
|
+
export const INFERRED_ROOTS_PROPERTY = "cdx:bom:inferredRootDependencies";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collects the bom-refs of every component in the document, including nested
|
|
31
|
+
* subcomponents and the subcomponents of the parent itself.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} bomJson CycloneDX BOM
|
|
34
|
+
* @returns {Set<string>} Known component bom-refs
|
|
35
|
+
*/
|
|
36
|
+
function collectComponentRefs(bomJson) {
|
|
37
|
+
const refs = new Set();
|
|
38
|
+
const visit = (component) => {
|
|
39
|
+
if (!component) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (component["bom-ref"]) {
|
|
43
|
+
refs.add(component["bom-ref"]);
|
|
44
|
+
}
|
|
45
|
+
for (const sub of component.components || []) {
|
|
46
|
+
visit(sub);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
for (const component of bomJson?.components || []) {
|
|
50
|
+
visit(component);
|
|
51
|
+
}
|
|
52
|
+
for (const sub of bomJson?.metadata?.component?.components || []) {
|
|
53
|
+
visit(sub);
|
|
54
|
+
}
|
|
55
|
+
for (const service of bomJson?.services || []) {
|
|
56
|
+
if (service?.["bom-ref"]) {
|
|
57
|
+
refs.add(service["bom-ref"]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return refs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Adds the missing edge from the parent component to the roots of the
|
|
65
|
+
* dependency graph. A no-op unless the document has a parent component with no
|
|
66
|
+
* outgoing edge, a tree that already contains edges, and at least one component
|
|
67
|
+
* that nothing points to.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} bomJson CycloneDX BOM
|
|
70
|
+
*
|
|
71
|
+
* @returns {Object} The same BOM, repaired in place when applicable
|
|
72
|
+
*/
|
|
73
|
+
export function repairParentDependencyEdge(bomJson) {
|
|
74
|
+
if (readEnvironmentVariable("CDXGEN_PARENT_EDGE_REPAIR") === "false") {
|
|
75
|
+
return bomJson;
|
|
76
|
+
}
|
|
77
|
+
const dependencies = bomJson?.dependencies;
|
|
78
|
+
const parentRef = bomJson?.metadata?.component?.["bom-ref"];
|
|
79
|
+
if (!parentRef || !Array.isArray(dependencies) || !dependencies.length) {
|
|
80
|
+
return bomJson;
|
|
81
|
+
}
|
|
82
|
+
const lowerParentRef = parentRef.toLowerCase();
|
|
83
|
+
let parentEntry;
|
|
84
|
+
let hasEdges = false;
|
|
85
|
+
const referenced = new Set();
|
|
86
|
+
for (const adep of dependencies) {
|
|
87
|
+
if (!adep?.ref) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (adep.ref.toLowerCase() === lowerParentRef) {
|
|
91
|
+
parentEntry = adep;
|
|
92
|
+
}
|
|
93
|
+
if (adep.dependsOn?.length) {
|
|
94
|
+
hasEdges = true;
|
|
95
|
+
}
|
|
96
|
+
for (const ref of adep.dependsOn || []) {
|
|
97
|
+
referenced.add(ref);
|
|
98
|
+
}
|
|
99
|
+
// A crypto asset that is only ever provided is not a root of the tree.
|
|
100
|
+
for (const ref of adep.provides || []) {
|
|
101
|
+
referenced.add(ref);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (parentEntry?.dependsOn?.length) {
|
|
105
|
+
return bomJson;
|
|
106
|
+
}
|
|
107
|
+
// Without a single edge there is no tree to enter, only a flat inventory such
|
|
108
|
+
// as a container image or an OS package list, where a fabricated root edge
|
|
109
|
+
// would assert a relationship that was never observed.
|
|
110
|
+
if (!hasEdges) {
|
|
111
|
+
return bomJson;
|
|
112
|
+
}
|
|
113
|
+
const componentRefs = collectComponentRefs(bomJson);
|
|
114
|
+
const roots = [];
|
|
115
|
+
for (const adep of dependencies) {
|
|
116
|
+
const ref = adep?.ref;
|
|
117
|
+
if (
|
|
118
|
+
!ref ||
|
|
119
|
+
ref.toLowerCase() === lowerParentRef ||
|
|
120
|
+
referenced.has(ref) ||
|
|
121
|
+
!componentRefs.has(ref)
|
|
122
|
+
) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
roots.push(ref);
|
|
126
|
+
}
|
|
127
|
+
// Every component being a root means the observed edges all pointed at refs
|
|
128
|
+
// that no component in the document declares, so the graph cannot be trusted.
|
|
129
|
+
if (!roots.length || roots.length === componentRefs.size) {
|
|
130
|
+
return bomJson;
|
|
131
|
+
}
|
|
132
|
+
roots.sort();
|
|
133
|
+
if (parentEntry) {
|
|
134
|
+
parentEntry.dependsOn = roots;
|
|
135
|
+
} else {
|
|
136
|
+
dependencies.splice(0, 0, { ref: parentRef, dependsOn: roots });
|
|
137
|
+
}
|
|
138
|
+
if (!bomJson.metadata.properties) {
|
|
139
|
+
bomJson.metadata.properties = [];
|
|
140
|
+
}
|
|
141
|
+
const existing = bomJson.metadata.properties.find(
|
|
142
|
+
(p) => p.name === INFERRED_ROOTS_PROPERTY,
|
|
143
|
+
);
|
|
144
|
+
if (existing) {
|
|
145
|
+
existing.value = `${roots.length}`;
|
|
146
|
+
} else {
|
|
147
|
+
bomJson.metadata.properties.push({
|
|
148
|
+
name: INFERRED_ROOTS_PROPERTY,
|
|
149
|
+
value: `${roots.length}`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
thoughtLog(
|
|
153
|
+
`The parent component had no direct dependencies, so I attached the ${roots.length} component(s) that nothing else depends on to keep the tree reachable.`,
|
|
154
|
+
);
|
|
155
|
+
return bomJson;
|
|
156
|
+
}
|
|
@@ -7,10 +7,12 @@ import { Purl } from "@cdxgen/cdx-purl";
|
|
|
7
7
|
import {
|
|
8
8
|
DEBUG_MODE,
|
|
9
9
|
isDryRun,
|
|
10
|
+
isSecureMode,
|
|
10
11
|
readEnvironmentVariable,
|
|
11
12
|
resetActivityContext,
|
|
12
13
|
setActivityContext,
|
|
13
14
|
} from "../../core/activity.js";
|
|
15
|
+
import { getRemediationCatalog } from "../../core/buildLedger.js";
|
|
14
16
|
import { hasAnyProjectType } from "../../core/env.js";
|
|
15
17
|
import {
|
|
16
18
|
getTimestamp,
|
|
@@ -44,6 +46,23 @@ import { enhanceBom } from "../../inventory/licenseEnhancer.js";
|
|
|
44
46
|
import { applyMcpPinningState } from "../../inventory/mcpPinning.js";
|
|
45
47
|
import { buildReleaseNotesFromGit } from "../../inventory/source.js";
|
|
46
48
|
import { extractTags, findBomType, textualMetadata } from "./annotator.js";
|
|
49
|
+
import {
|
|
50
|
+
applyFormulationRunIdMarker,
|
|
51
|
+
applyIntrospectionToBom,
|
|
52
|
+
hasIntrospectionVerdict,
|
|
53
|
+
} from "./introspection/annotate.js";
|
|
54
|
+
import { collectCommandFacts } from "./introspection/commandFacts.js";
|
|
55
|
+
import {
|
|
56
|
+
blockRemediationsForDryRun,
|
|
57
|
+
emitIntrospectionReports,
|
|
58
|
+
printIntrospectionSummary,
|
|
59
|
+
} from "./introspection/emit.js";
|
|
60
|
+
import {
|
|
61
|
+
isIntrospectionEnabled,
|
|
62
|
+
reflectOnRun,
|
|
63
|
+
} from "./introspection/reflect.js";
|
|
64
|
+
import { scoreReflection } from "./introspection/score.js";
|
|
65
|
+
import { repairParentDependencyEdge } from "./parentEdge.js";
|
|
47
66
|
import { sortBomCollections } from "./sortBom.js";
|
|
48
67
|
import {
|
|
49
68
|
applyComponentTypeFilter,
|
|
@@ -435,6 +454,94 @@ function buildAuditFindingsCitation(bomJson) {
|
|
|
435
454
|
});
|
|
436
455
|
}
|
|
437
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Run the build-introspection reflection over the final BOM, score it, write
|
|
459
|
+
* the reports, and carry the verdict inside the BOM as metadata properties and
|
|
460
|
+
* annotations. Introspection is opt-in; when disabled this is a no-op, and
|
|
461
|
+
* when enabled a reflection failure warns instead of failing the completed
|
|
462
|
+
* BOM.
|
|
463
|
+
*
|
|
464
|
+
* The CI gate is decided here but enforced by the CLI after the BOM is
|
|
465
|
+
* written, so a below-threshold score never costs the user their output.
|
|
466
|
+
*
|
|
467
|
+
* @param {Object} bomNSData BOM with namespaces object
|
|
468
|
+
* @param {Object} options CLI options
|
|
469
|
+
* @param {string} [filePath] Source path that was scanned
|
|
470
|
+
*
|
|
471
|
+
* @returns {Promise<Object>} The same bomNSData, with `reflection` (carrying its `scoring`) attached when introspection ran
|
|
472
|
+
*/
|
|
473
|
+
async function applyIntrospection(bomNSData, options, filePath) {
|
|
474
|
+
if (!isIntrospectionEnabled(options) || !bomNSData?.bomJson) {
|
|
475
|
+
return bomNSData;
|
|
476
|
+
}
|
|
477
|
+
// Evidence collection re-processes a finished BOM through a fresh wrapper,
|
|
478
|
+
// but that pass carries neither the scanned path nor the generation's
|
|
479
|
+
// ecosystem context: its ledger events belong to the evinse probes and its
|
|
480
|
+
// marker scan finds nothing, so it would grade a project it never saw. The
|
|
481
|
+
// verdict describes how the BOM was generated, so it stays with the pass
|
|
482
|
+
// that generated it — the document itself is what carries that across.
|
|
483
|
+
if (bomNSData.reflection || hasIntrospectionVerdict(bomNSData.bomJson)) {
|
|
484
|
+
return bomNSData;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const reflection = await reflectOnRun(bomNSData.bomJson, options, {
|
|
488
|
+
projectPath: filePath || options.filePath || "",
|
|
489
|
+
});
|
|
490
|
+
reflection.scoring = scoreReflection(reflection, getRemediationCatalog(), {
|
|
491
|
+
secureMode: isSecureMode,
|
|
492
|
+
inContainer: readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true",
|
|
493
|
+
commandFacts: collectCommandFacts(filePath || options.filePath || ""),
|
|
494
|
+
});
|
|
495
|
+
// Report consumers correlate a report with the BOM it describes through
|
|
496
|
+
// these two facts, so they travel on the reflection document itself.
|
|
497
|
+
reflection.bom = {
|
|
498
|
+
serialNumber: bomNSData.bomJson?.serialNumber,
|
|
499
|
+
componentCount: Array.isArray(bomNSData.bomJson?.components)
|
|
500
|
+
? bomNSData.bomJson.components.length
|
|
501
|
+
: 0,
|
|
502
|
+
};
|
|
503
|
+
bomNSData.reflection = reflection;
|
|
504
|
+
if (isDryRun) {
|
|
505
|
+
blockRemediationsForDryRun(reflection.scoring);
|
|
506
|
+
}
|
|
507
|
+
// A document at a spec version that predates annotations still carries
|
|
508
|
+
// the metadata properties; only the annotations are version-gated.
|
|
509
|
+
const annotationsSupported =
|
|
510
|
+
normalizeCycloneDxSpecVersion(
|
|
511
|
+
options?.specVersion || bomNSData.bomJson?.specVersion,
|
|
512
|
+
) >= 1.5;
|
|
513
|
+
applyFormulationRunIdMarker(bomNSData.bomJson, reflection.runId);
|
|
514
|
+
applyIntrospectionToBom(bomNSData.bomJson, reflection, reflection.scoring, {
|
|
515
|
+
annotations: annotationsSupported,
|
|
516
|
+
});
|
|
517
|
+
const delivery = emitIntrospectionReports(
|
|
518
|
+
reflection,
|
|
519
|
+
reflection.scoring,
|
|
520
|
+
options,
|
|
521
|
+
);
|
|
522
|
+
printIntrospectionSummary(reflection.scoring, delivery);
|
|
523
|
+
const threshold = options?.introspectFailBelow;
|
|
524
|
+
if (typeof threshold === "number" && Number.isFinite(threshold)) {
|
|
525
|
+
const score = reflection.scoring.overallScore;
|
|
526
|
+
bomNSData.introspectionGate = {
|
|
527
|
+
threshold,
|
|
528
|
+
score,
|
|
529
|
+
passed: score >= threshold,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (!reflection.ledgerComplete) {
|
|
533
|
+
console.warn(
|
|
534
|
+
"Build introspection: the ledger is incomplete (buffer truncated or recorder event); treat the verdict with care and re-run with CDXGEN_INTROSPECT_LEDGER set.",
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
} catch (err) {
|
|
538
|
+
console.warn(
|
|
539
|
+
`Build introspection failed to complete and was skipped: ${err?.message}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return bomNSData;
|
|
543
|
+
}
|
|
544
|
+
|
|
438
545
|
/**
|
|
439
546
|
* Filter and enhance BOM post generation.
|
|
440
547
|
*
|
|
@@ -442,9 +549,9 @@ function buildAuditFindingsCitation(bomJson) {
|
|
|
442
549
|
* @param {Object} options CLI options
|
|
443
550
|
* @param {string} [filePath] Source path used for formulation and metadata context
|
|
444
551
|
*
|
|
445
|
-
* @returns {Object} Modified bomNSData
|
|
552
|
+
* @returns {Promise<Object>} Modified bomNSData
|
|
446
553
|
*/
|
|
447
|
-
export function postProcess(bomNSData, options, filePath) {
|
|
554
|
+
export async function postProcess(bomNSData, options, filePath) {
|
|
448
555
|
let jsonPayload = bomNSData.bomJson;
|
|
449
556
|
if (
|
|
450
557
|
typeof bomNSData.bomJson === "string" ||
|
|
@@ -466,6 +573,9 @@ export function postProcess(bomNSData, options, filePath) {
|
|
|
466
573
|
);
|
|
467
574
|
bomNSData.bomJson = applyReleaseNotes(bomNSData.bomJson, options, filePath);
|
|
468
575
|
bomNSData.bomJson = applyComponentTypeFilter(bomNSData.bomJson, options);
|
|
576
|
+
// Filtering can leave the tree without a way in, so this runs on the final
|
|
577
|
+
// component set and before annotations, which report on reachability.
|
|
578
|
+
bomNSData.bomJson = repairParentDependencyEdge(bomNSData.bomJson);
|
|
469
579
|
bomNSData.bomJson = applySpecVersionCompatibility(bomNSData.bomJson, options);
|
|
470
580
|
bomNSData.bomJson = validateTlpClassification(bomNSData.bomJson, options);
|
|
471
581
|
// Support for automatic annotations
|
|
@@ -495,6 +605,9 @@ export function postProcess(bomNSData, options, filePath) {
|
|
|
495
605
|
cleanupEnv(options);
|
|
496
606
|
cleanupTmpDir();
|
|
497
607
|
sortBomCollections(bomNSData.bomJson);
|
|
608
|
+
// Reflection reads the finished BOM, so it runs after sorting; it returns
|
|
609
|
+
// its verdict alongside the BOM instead of mutating it.
|
|
610
|
+
await applyIntrospection(bomNSData, options, filePath);
|
|
498
611
|
return bomNSData;
|
|
499
612
|
}
|
|
500
613
|
|
|
@@ -494,6 +494,23 @@ function registerCdxHelpers(expression, helpers = {}) {
|
|
|
494
494
|
expression.registerFunction("formulationComponents", (bomJson) =>
|
|
495
495
|
getFormulationComponents(bomJson),
|
|
496
496
|
);
|
|
497
|
+
// Formulation-derived rules need to know whether the formulation section
|
|
498
|
+
// describes the run being reflected (whose probes the ledger already
|
|
499
|
+
// counted) or a foreign BOM's record. The caller that read the evidence
|
|
500
|
+
// passes its origin verdict through the options; without one, rules gated
|
|
501
|
+
// on a specific origin stay silent.
|
|
502
|
+
expression.registerFunction(
|
|
503
|
+
"formulationOrigin",
|
|
504
|
+
() => helpers?.formulationEvidence?.origin || "unknown",
|
|
505
|
+
);
|
|
506
|
+
// The toolchain mismatch a foreign BOM's formulation proves: the recorded
|
|
507
|
+
// version of this runtime's own family differs from the runtime in use, so
|
|
508
|
+
// the record describes another machine. Computed by the caller, which owns
|
|
509
|
+
// the runtime facts.
|
|
510
|
+
expression.registerFunction(
|
|
511
|
+
"formulationRuntimeMismatch",
|
|
512
|
+
() => helpers?.formulationEvidence?.runtimeMismatch,
|
|
513
|
+
);
|
|
497
514
|
return expression;
|
|
498
515
|
}
|
|
499
516
|
|