@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
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
and $prop($, 'cdx:cargo:hasNativeBuild') = 'true'
|
|
251
251
|
and $count($$.components[
|
|
252
252
|
$prop($, 'cdx:github:action:ecosystem') = 'cargo'
|
|
253
|
-
and $contains($
|
|
253
|
+
and $contains($nullSafeProp($, 'cdx:github:action:role'), 'toolchain')
|
|
254
254
|
and $prop($, 'cdx:github:action:versionPinningType') != 'sha'
|
|
255
255
|
]) > 0
|
|
256
256
|
]
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
"buildScriptCapabilities": $prop($, 'cdx:cargo:buildScriptCapabilities'),
|
|
268
268
|
"toolchainActions": $$.components[
|
|
269
269
|
$prop($, 'cdx:github:action:ecosystem') = 'cargo'
|
|
270
|
-
and $contains($
|
|
270
|
+
and $contains($nullSafeProp($, 'cdx:github:action:role'), 'toolchain')
|
|
271
271
|
and $prop($, 'cdx:github:action:versionPinningType') != 'sha'
|
|
272
272
|
].$prop($, 'cdx:github:action:uses')
|
|
273
273
|
}
|
|
@@ -283,17 +283,17 @@
|
|
|
283
283
|
$prop($, 'cdx:rust:buildTool') = 'cargo'
|
|
284
284
|
and $prop($, 'cdx:cargo:hasNativeBuild') = 'true'
|
|
285
285
|
and (
|
|
286
|
-
$contains($
|
|
287
|
-
or $contains($
|
|
288
|
-
or $contains($
|
|
286
|
+
$contains($nullSafeProp($, 'cdx:cargo:buildScriptCapabilities'), 'process-execution')
|
|
287
|
+
or $contains($nullSafeProp($, 'cdx:cargo:buildScriptCapabilities'), 'network-access')
|
|
288
|
+
or $contains($nullSafeProp($, 'cdx:cargo:nativeBuildIndicators'), '-sys')
|
|
289
289
|
)
|
|
290
290
|
and $count($$.components[
|
|
291
291
|
$prop($, 'cdx:github:step:usesCargo') = 'true'
|
|
292
292
|
and (
|
|
293
|
-
$contains($
|
|
294
|
-
or $contains($
|
|
295
|
-
or $contains($
|
|
296
|
-
or $contains($
|
|
293
|
+
$contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'build')
|
|
294
|
+
or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'test')
|
|
295
|
+
or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'package')
|
|
296
|
+
or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'publish')
|
|
297
297
|
)
|
|
298
298
|
]) > 0
|
|
299
299
|
]
|
|
@@ -311,10 +311,10 @@
|
|
|
311
311
|
"cargoCommands": $$.components[
|
|
312
312
|
$prop($, 'cdx:github:step:usesCargo') = 'true'
|
|
313
313
|
and (
|
|
314
|
-
$contains($
|
|
315
|
-
or $contains($
|
|
316
|
-
or $contains($
|
|
317
|
-
or $contains($
|
|
314
|
+
$contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'build')
|
|
315
|
+
or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'test')
|
|
316
|
+
or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'package')
|
|
317
|
+
or $contains($nullSafeProp($, 'cdx:github:step:cargoSubcommands'), 'publish')
|
|
318
318
|
)
|
|
319
319
|
].$prop($, 'cdx:github:step:command')
|
|
320
320
|
}
|
package/lib/audit/index.js
CHANGED
|
@@ -6,9 +6,11 @@ import process from "node:process";
|
|
|
6
6
|
import { createBom } from "../cli/index.js";
|
|
7
7
|
import {
|
|
8
8
|
isDryRun,
|
|
9
|
+
isSecureMode,
|
|
9
10
|
readEnvironmentVariable,
|
|
10
11
|
recordActivity,
|
|
11
12
|
} from "../core/activity.js";
|
|
13
|
+
import { getRemediationCatalog } from "../core/buildLedger.js";
|
|
12
14
|
import {
|
|
13
15
|
getTmpDir,
|
|
14
16
|
safeExistsSync,
|
|
@@ -53,6 +55,12 @@ import {
|
|
|
53
55
|
isHbomLikeBom,
|
|
54
56
|
isObomLikeBom,
|
|
55
57
|
} from "../stages/postgen/auditBom.js";
|
|
58
|
+
import {
|
|
59
|
+
isIntrospectionEnabled,
|
|
60
|
+
reflectOnRun,
|
|
61
|
+
} from "../stages/postgen/introspection/reflect.js";
|
|
62
|
+
import { buildIntrospectionJson } from "../stages/postgen/introspection/report.js";
|
|
63
|
+
import { scoreReflection } from "../stages/postgen/introspection/score.js";
|
|
56
64
|
import { postProcess } from "../stages/postgen/postgen.js";
|
|
57
65
|
import { formatTargetLabel } from "./progress.js";
|
|
58
66
|
import { renderAuditReport } from "./reporters.js";
|
|
@@ -259,7 +267,7 @@ export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
|
|
|
259
267
|
...directAuditOptions,
|
|
260
268
|
getBomWithOras,
|
|
261
269
|
});
|
|
262
|
-
|
|
270
|
+
const result = {
|
|
263
271
|
auditOptions: directAuditOptions,
|
|
264
272
|
bomFormat: getCycloneDxFormat(inputBom.bomJson),
|
|
265
273
|
findings,
|
|
@@ -268,7 +276,43 @@ export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
|
|
|
268
276
|
specVersion: inputBom.bomJson?.specVersion,
|
|
269
277
|
status: "audited",
|
|
270
278
|
summary: summarizeDirectAuditFindings(findings),
|
|
271
|
-
}
|
|
279
|
+
};
|
|
280
|
+
if (isIntrospectionEnabled(options)) {
|
|
281
|
+
// The audited BOM was not generated by this run, so there is no ledger
|
|
282
|
+
// of its own; the verdict rests on BOM structure alone and says so.
|
|
283
|
+
try {
|
|
284
|
+
const reflection = await reflectOnRun(inputBom.bomJson, options, {
|
|
285
|
+
ledgerEvents: [],
|
|
286
|
+
ledgerSource: "none",
|
|
287
|
+
projectPath: inputBom.source || "",
|
|
288
|
+
});
|
|
289
|
+
reflection.scoring = scoreReflection(
|
|
290
|
+
reflection,
|
|
291
|
+
getRemediationCatalog(),
|
|
292
|
+
{
|
|
293
|
+
secureMode: isSecureMode,
|
|
294
|
+
inContainer:
|
|
295
|
+
readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true",
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
reflection.bom = {
|
|
299
|
+
serialNumber: inputBom.bomJson?.serialNumber,
|
|
300
|
+
componentCount: Array.isArray(inputBom.bomJson?.components)
|
|
301
|
+
? inputBom.bomJson.components.length
|
|
302
|
+
: 0,
|
|
303
|
+
};
|
|
304
|
+
result.introspection = buildIntrospectionJson(
|
|
305
|
+
reflection,
|
|
306
|
+
reflection.scoring,
|
|
307
|
+
options,
|
|
308
|
+
);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.warn(
|
|
311
|
+
`Build introspection failed to complete and was skipped: ${err?.message}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
results.push(result);
|
|
272
316
|
}
|
|
273
317
|
const summary = {
|
|
274
318
|
findingsBySeverity: {
|
|
@@ -1744,7 +1788,7 @@ async function recheckFindingsAgainstDefaultBranch(
|
|
|
1744
1788
|
if (!bomNSData?.bomJson) {
|
|
1745
1789
|
return undefined;
|
|
1746
1790
|
}
|
|
1747
|
-
const processedBomNSData = postProcess(
|
|
1791
|
+
const processedBomNSData = await postProcess(
|
|
1748
1792
|
bomNSData,
|
|
1749
1793
|
childOptions,
|
|
1750
1794
|
defaultSourceSelection.scanDir,
|
|
@@ -1948,7 +1992,7 @@ export async function auditTarget(target, options) {
|
|
|
1948
1992
|
options,
|
|
1949
1993
|
);
|
|
1950
1994
|
}
|
|
1951
|
-
const processedBomNSData = postProcess(
|
|
1995
|
+
const processedBomNSData = await postProcess(
|
|
1952
1996
|
bomNSData,
|
|
1953
1997
|
childOptions,
|
|
1954
1998
|
sourceSelection.scanDir,
|
package/lib/audit/scoring.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { thoughtLog } from "../core/logger.js";
|
|
2
|
+
import { SEVERITY_ORDER } from "../core/severity.js";
|
|
2
3
|
import {
|
|
3
4
|
hasRegistryProvenanceEvidenceProperties,
|
|
4
5
|
hasTrustedPublishingProperties,
|
|
5
6
|
} from "../inventory/provenanceUtils.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
export const SEVERITY_ORDER = {
|
|
9
|
-
none: -1,
|
|
10
|
-
low: 0,
|
|
11
|
-
medium: 1,
|
|
12
|
-
high: 2,
|
|
13
|
-
critical: 3,
|
|
14
|
-
};
|
|
8
|
+
export { SEVERITY_ORDER };
|
|
15
9
|
|
|
16
10
|
const BASE_FINDING_WEIGHT = {
|
|
17
11
|
low: 4,
|
package/lib/cli/bomAssembly.js
CHANGED
|
@@ -200,6 +200,8 @@ export const determineParentComponent = (options) => {
|
|
|
200
200
|
* @param {object|undefined} detectedParent Parent the generator derived
|
|
201
201
|
* @param {object[]} components The component list, appended to in place
|
|
202
202
|
* @param {object[]} dependencies Edges the generator produced
|
|
203
|
+
* @param {object} options CLI options
|
|
204
|
+
* @param {string} ptype Package type
|
|
203
205
|
* @returns {object[]} Edges to emit, re-anchored when an override applies
|
|
204
206
|
*/
|
|
205
207
|
function anchorDetectedParent(
|
|
@@ -207,6 +209,8 @@ function anchorDetectedParent(
|
|
|
207
209
|
detectedParent,
|
|
208
210
|
components,
|
|
209
211
|
dependencies,
|
|
212
|
+
options,
|
|
213
|
+
ptype,
|
|
210
214
|
) {
|
|
211
215
|
const detectedRef = detectedParent?.["bom-ref"];
|
|
212
216
|
const overrideRef = overrideParent?.["bom-ref"];
|
|
@@ -220,7 +224,25 @@ function anchorDetectedParent(
|
|
|
220
224
|
Array.isArray(components) &&
|
|
221
225
|
!components.some((comp) => comp["bom-ref"] === detectedRef)
|
|
222
226
|
) {
|
|
223
|
-
|
|
227
|
+
// The detected parent carries transient keys (`license`, `evidence`,
|
|
228
|
+
// `_integrity`, `qualifiers`, `repository`, `homepage`) that only
|
|
229
|
+
// `metadata.component` is cleaned of. As a regular component it must go
|
|
230
|
+
// through the same conversion or schema validation rejects the document
|
|
231
|
+
// (issue #4320). The copy keeps the caller's context object untouched.
|
|
232
|
+
//
|
|
233
|
+
// Its workspace members go through `listComponents`, the same conversion
|
|
234
|
+
// `addMetadata` gives them when the detected parent is the one reaching
|
|
235
|
+
// `metadata.component`. Without it they keep the raw parser shape
|
|
236
|
+
// (`homepage: { url }`) that the nested Component schema rejects
|
|
237
|
+
// (issue #4326).
|
|
238
|
+
components.push(
|
|
239
|
+
cleanParentComponent({
|
|
240
|
+
...detectedParent,
|
|
241
|
+
components: detectedParent.components?.length
|
|
242
|
+
? listComponents(options, {}, detectedParent.components, ptype)
|
|
243
|
+
: undefined,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
224
246
|
}
|
|
225
247
|
// The caller owns the array it passed, and postProcess merges these edges
|
|
226
248
|
// across generators, so the re-anchoring is returned rather than spliced in.
|
|
@@ -1302,8 +1324,7 @@ export function processHashes(pkg, component) {
|
|
|
1302
1324
|
addComponentHash(ahash.alg, ahash.content, component);
|
|
1303
1325
|
}
|
|
1304
1326
|
} else if (pkg._shasum) {
|
|
1305
|
-
|
|
1306
|
-
component.hashes.push(ahash);
|
|
1327
|
+
addComponentHash("SHA-1", pkg._shasum, component);
|
|
1307
1328
|
} else if (pkg._integrity) {
|
|
1308
1329
|
const integrity = parse(pkg._integrity) || {};
|
|
1309
1330
|
// Components may have multiple hashes with various lengths. Check each one
|
|
@@ -1326,24 +1347,69 @@ export function processHashes(pkg, component) {
|
|
|
1326
1347
|
}
|
|
1327
1348
|
}
|
|
1328
1349
|
|
|
1350
|
+
// Digest length in hex characters for each algorithm CycloneDX names. A digest
|
|
1351
|
+
// that decodes to the wrong length for its algorithm is corrupt, whatever
|
|
1352
|
+
// encoding it arrived in.
|
|
1353
|
+
const HASH_HEX_LENGTHS = {
|
|
1354
|
+
MD5: 32,
|
|
1355
|
+
"SHA-1": 40,
|
|
1356
|
+
"SHA-256": 64,
|
|
1357
|
+
"SHA-384": 96,
|
|
1358
|
+
"SHA-512": 128,
|
|
1359
|
+
"SHA3-256": 64,
|
|
1360
|
+
"SHA3-384": 96,
|
|
1361
|
+
"SHA3-512": 128,
|
|
1362
|
+
"BLAKE2b-256": 64,
|
|
1363
|
+
"BLAKE2b-384": 96,
|
|
1364
|
+
"BLAKE2b-512": 128,
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
// Lengths the CycloneDX hash-content pattern accepts, for algorithms not in the
|
|
1368
|
+
// table above.
|
|
1369
|
+
const HASH_HEX_LENGTHS_ANY = new Set([32, 40, 64, 96, 128]);
|
|
1370
|
+
|
|
1329
1371
|
/**
|
|
1330
1372
|
* Adds a hash to component.
|
|
1373
|
+
*
|
|
1374
|
+
* Registries hand out digests in hex or in the base64 form npm uses for
|
|
1375
|
+
* `integrity`, and both are accepted. A digest that is neither, or that decodes
|
|
1376
|
+
* to the wrong length for its algorithm, is dropped rather than copied through:
|
|
1377
|
+
* CycloneDX constrains `hash.content` to a hex digest, so emitting it anyway
|
|
1378
|
+
* fails schema validation for the whole document over one bad package.
|
|
1331
1379
|
*/
|
|
1332
1380
|
export function addComponentHash(alg, digest, component) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1381
|
+
const hash = normalizeHashContent(alg, digest);
|
|
1382
|
+
if (!hash) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
component.hashes.push({ alg: alg, content: hash });
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Normalize a digest to the lower-case hex form CycloneDX requires.
|
|
1390
|
+
*
|
|
1391
|
+
* @param {string} alg Hash algorithm name
|
|
1392
|
+
* @param {string} digest Digest in hex or base64
|
|
1393
|
+
* @returns {string|undefined} Hex digest, or undefined when it cannot be trusted
|
|
1394
|
+
*/
|
|
1395
|
+
export function normalizeHashContent(alg, digest) {
|
|
1396
|
+
if (!digest || typeof digest !== "string") {
|
|
1397
|
+
return undefined;
|
|
1398
|
+
}
|
|
1399
|
+
const expectedLength = HASH_HEX_LENGTHS[alg];
|
|
1400
|
+
const isExpectedLength = (value) =>
|
|
1401
|
+
expectedLength
|
|
1402
|
+
? value.length === expectedLength
|
|
1403
|
+
: HASH_HEX_LENGTHS_ANY.has(value.length);
|
|
1404
|
+
|
|
1335
1405
|
if (new RegExp(HASH_PATTERN).test(digest)) {
|
|
1336
|
-
|
|
1337
|
-
} else {
|
|
1338
|
-
// Check if base64 encoded
|
|
1339
|
-
const isBase64Encoded =
|
|
1340
|
-
Buffer.from(digest, "base64").toString("base64") === digest;
|
|
1341
|
-
hash = isBase64Encoded
|
|
1342
|
-
? Buffer.from(digest, "base64").toString("hex")
|
|
1343
|
-
: digest;
|
|
1406
|
+
return isExpectedLength(digest) ? digest : undefined;
|
|
1344
1407
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1408
|
+
// npm and friends carry the digest base64-encoded. Node decodes leniently, so
|
|
1409
|
+
// the decoded length is the check that matters: a truncated or invented value
|
|
1410
|
+
// decodes to something, just not to a digest of the right size.
|
|
1411
|
+
const decoded = Buffer.from(digest, "base64").toString("hex");
|
|
1412
|
+
return isExpectedLength(decoded) ? decoded : undefined;
|
|
1347
1413
|
}
|
|
1348
1414
|
|
|
1349
1415
|
/**
|
|
@@ -1397,6 +1463,8 @@ export const buildBomNSData = (options, pkgInfo, ptype, context) => {
|
|
|
1397
1463
|
context.parentComponent,
|
|
1398
1464
|
components,
|
|
1399
1465
|
context.dependencies || [],
|
|
1466
|
+
options,
|
|
1467
|
+
ptype,
|
|
1400
1468
|
);
|
|
1401
1469
|
if (
|
|
1402
1470
|
components &&
|
package/lib/cli/cliOptions.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join, resolve } from "node:path";
|
|
15
15
|
|
|
16
16
|
import { isSecureMode, readEnvironmentVariable } from "../core/activity.js";
|
|
17
|
+
import { PROJECT_TYPE_ALIASES } from "../core/env.js";
|
|
17
18
|
import {
|
|
18
19
|
DEFAULT_CDX_SPEC_VERSION,
|
|
19
20
|
normalizeCycloneDxComponentTypeFilter,
|
|
@@ -87,6 +88,21 @@ const POST_BUILD_SUPPORTED_PROJECT_TYPES = new Set([
|
|
|
87
88
|
// Helpers
|
|
88
89
|
// ---------------------------------------------------------------------------
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The formulation privacy notice, shared by every path that turns formulation
|
|
93
|
+
* collection on automatically: the warning about emails and secrets is
|
|
94
|
+
* identical whichever caller adds the section.
|
|
95
|
+
*
|
|
96
|
+
* @returns {OptionsWarning} The notice.
|
|
97
|
+
*/
|
|
98
|
+
function formulationPrivacyNotice() {
|
|
99
|
+
return {
|
|
100
|
+
level: "info",
|
|
101
|
+
message:
|
|
102
|
+
"NOTE: Automatically collecting formulation information. The section may include sensitive data such as emails and secrets.\nPlease review the generated SBOM before distribution or LLM training.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
/**
|
|
91
107
|
* Check whether a value was explicitly provided by the user (i.e., is not
|
|
92
108
|
* the yargs default). Yargs always populates default values, so we cannot
|
|
@@ -174,7 +190,39 @@ function resolveOutputTarget(args, { filePath, isRemoteOrPurl }) {
|
|
|
174
190
|
: resolve(join(filePath, args.output));
|
|
175
191
|
}
|
|
176
192
|
|
|
177
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Resolve the introspection opt-in from the flag and the environment.
|
|
195
|
+
*
|
|
196
|
+
* The parser maps `CDXGEN_INTROSPECT` onto the same boolean the flag sets, so
|
|
197
|
+
* a value outside the documented opt-in vocabulary — an exported but empty
|
|
198
|
+
* variable most of all — arrives indistinguishable from `--no-introspect`.
|
|
199
|
+
* The command line is therefore the authority on refusal, and the environment
|
|
200
|
+
* speaks only through `true` and `1`. An unmentioned flag stays undefined so a
|
|
201
|
+
* profile can still enable it.
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} args Parsed CLI arguments.
|
|
204
|
+
* @param {string[]} argv Raw command-line arguments the parser was given.
|
|
205
|
+
* @returns {boolean|undefined} The requested state, or undefined when nothing asked.
|
|
206
|
+
*/
|
|
207
|
+
function resolveIntrospectOption(args, argv) {
|
|
208
|
+
const refused =
|
|
209
|
+
args.introspect === false &&
|
|
210
|
+
argv.some((argument) =>
|
|
211
|
+
["--no-introspect", "--introspect=false"].includes(argument),
|
|
212
|
+
);
|
|
213
|
+
if (refused) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return args.introspect === true ||
|
|
217
|
+
["true", "1"].includes(readEnvironmentVariable("CDXGEN_INTROSPECT"))
|
|
218
|
+
? true
|
|
219
|
+
: undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildInitialOptions(
|
|
223
|
+
args,
|
|
224
|
+
{ filePath, isRemoteOrPurl, argv = process.argv },
|
|
225
|
+
) {
|
|
178
226
|
const res = Object.assign({}, args, {
|
|
179
227
|
projectType: args.type,
|
|
180
228
|
multiProject: args.recurse,
|
|
@@ -187,6 +235,10 @@ export function buildInitialOptions(args, { filePath, isRemoteOrPurl }) {
|
|
|
187
235
|
include: args.include || args.includeRegex,
|
|
188
236
|
noIgnore: args.noIgnore,
|
|
189
237
|
gitAiNotesRef: args.gitAiNotesRef || "refs/notes/ai",
|
|
238
|
+
// Introspection: the flag or the documented environment opt-in. An
|
|
239
|
+
// explicit --no-introspect stays `false` and beats both, while an
|
|
240
|
+
// unmentioned flag stays undefined so a profile can still enable it.
|
|
241
|
+
introspect: resolveIntrospectOption(args, argv),
|
|
190
242
|
// License normalization and enrichment are on by default; the only escape
|
|
191
243
|
// hatch is the corresponding environment variable.
|
|
192
244
|
licenseEnhance:
|
|
@@ -217,6 +269,24 @@ export function buildInitialOptions(args, { filePath, isRemoteOrPurl }) {
|
|
|
217
269
|
return res;
|
|
218
270
|
}
|
|
219
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Check whether the supplied project types include an OS inventory type
|
|
274
|
+
* (`os` or an alias such as `linux`, `windows`, `macos`).
|
|
275
|
+
*
|
|
276
|
+
* @param {string|string[]|undefined|null} projectTypes Project types.
|
|
277
|
+
* @returns {boolean} True when an OS inventory type is requested.
|
|
278
|
+
*/
|
|
279
|
+
function hasOsProjectType(projectTypes) {
|
|
280
|
+
if (!projectTypes) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
const values = Array.isArray(projectTypes) ? projectTypes : [projectTypes];
|
|
284
|
+
const osTypes = new Set(["os", ...(PROJECT_TYPE_ALIASES.os || [])]);
|
|
285
|
+
return values
|
|
286
|
+
.flatMap((projectType) => `${projectType}`.split(","))
|
|
287
|
+
.some((projectType) => osTypes.has(projectType.trim().toLowerCase()));
|
|
288
|
+
}
|
|
289
|
+
|
|
220
290
|
/**
|
|
221
291
|
* @typedef {object} OptionsWarning
|
|
222
292
|
* @property {"info"|"warn"|"error"} level Severity level
|
|
@@ -263,6 +333,17 @@ export function applyPostConstructionOverrides(options, context) {
|
|
|
263
333
|
"The cbom command does not support --component-type. Use cdxgen with --include-crypto when you need component-type filtering.",
|
|
264
334
|
});
|
|
265
335
|
}
|
|
336
|
+
// Crypto evidence collection analyses source code with atom, which is
|
|
337
|
+
// meaningless and extremely slow for OS inventories (discussion #4310).
|
|
338
|
+
// Refuse the combination and direct users to obom.
|
|
339
|
+
if (hasOsProjectType(options.projectType)) {
|
|
340
|
+
warnings.push({
|
|
341
|
+
level: "error",
|
|
342
|
+
message:
|
|
343
|
+
"The cbom command does not support OS inventory project types (-t os). Use the obom command instead to generate an OBOM for an operating system installation.",
|
|
344
|
+
});
|
|
345
|
+
return warnings;
|
|
346
|
+
}
|
|
266
347
|
options.includeCrypto = true;
|
|
267
348
|
}
|
|
268
349
|
options.evidence = true;
|
|
@@ -288,6 +369,20 @@ export function applyPostConstructionOverrides(options, context) {
|
|
|
288
369
|
options.installDeps = false;
|
|
289
370
|
}
|
|
290
371
|
|
|
372
|
+
// --- Introspection CI gate threshold -----------------------------------
|
|
373
|
+
if (options.introspectFailBelow !== undefined) {
|
|
374
|
+
const threshold = Number(options.introspectFailBelow);
|
|
375
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 100) {
|
|
376
|
+
warnings.push({
|
|
377
|
+
level: "error",
|
|
378
|
+
message:
|
|
379
|
+
"--introspect-fail-below requires a number between 0 and 100, where absent means the gate never fails.",
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
options.introspectFailBelow = threshold;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
291
386
|
// When a standard is requested, the minimum spec version is 1.6
|
|
292
387
|
if (options.standard) {
|
|
293
388
|
if (!userSetSpecVersion) {
|
|
@@ -351,6 +446,8 @@ export function applyAdvancedOptions(options, context = {}) {
|
|
|
351
446
|
const secureMode = context.isSecureMode ?? isSecureMode;
|
|
352
447
|
const warnings = [];
|
|
353
448
|
const isHbomOnly = isHbomOnlyProjectTypes(options.projectType);
|
|
449
|
+
const includeFormulationBeforeProfiles = options.includeFormulation === true;
|
|
450
|
+
const introspectExplicitlyDisabled = options.introspect === false;
|
|
354
451
|
|
|
355
452
|
// --- Profile expansion -------------------------------------------------
|
|
356
453
|
switch (options.profile) {
|
|
@@ -382,6 +479,14 @@ export function applyAdvancedOptions(options, context = {}) {
|
|
|
382
479
|
case "license-compliance":
|
|
383
480
|
process.env.FETCH_LICENSE = "true";
|
|
384
481
|
break;
|
|
482
|
+
case "introspect":
|
|
483
|
+
options.introspect = true;
|
|
484
|
+
options.introspectAnnotate = options.introspectAnnotate !== false;
|
|
485
|
+
// The ledger's own record of what ran; formulation is where a consumer
|
|
486
|
+
// looks for the build environment the SBOM was produced in.
|
|
487
|
+
options.includeFormulation = !isHbomOnly;
|
|
488
|
+
options.evidence = true;
|
|
489
|
+
break;
|
|
385
490
|
case "ml-tiny":
|
|
386
491
|
process.env.FETCH_LICENSE = "true";
|
|
387
492
|
options.deep = false;
|
|
@@ -441,17 +546,29 @@ export function applyAdvancedOptions(options, context = {}) {
|
|
|
441
546
|
}
|
|
442
547
|
}
|
|
443
548
|
|
|
549
|
+
// An explicit --no-introspect negates the introspect profile's headline
|
|
550
|
+
// capability while keeping the bundle's other effects: the user asked for
|
|
551
|
+
// the profile minus introspection.
|
|
552
|
+
if (options.profile === "introspect" && introspectExplicitlyDisabled) {
|
|
553
|
+
options.introspect = false;
|
|
554
|
+
warnings.push({
|
|
555
|
+
level: "info",
|
|
556
|
+
message:
|
|
557
|
+
"NOTE: Introspection stays off because --no-introspect was passed explicitly; the other introspect profile settings still apply.",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
444
561
|
// --- BOM audit → auto-formulation --------------------------------------
|
|
445
|
-
|
|
562
|
+
const introspectProfileAddsFormulation =
|
|
563
|
+
options.profile === "introspect" && !includeFormulationBeforeProfiles;
|
|
564
|
+
if (options.bomAudit || introspectProfileAddsFormulation) {
|
|
446
565
|
if (isHbomOnly) {
|
|
447
566
|
// HBOM-only: skip formulation
|
|
448
567
|
} else if (!options.includeFormulation) {
|
|
449
|
-
warnings.push(
|
|
450
|
-
level: "info",
|
|
451
|
-
message:
|
|
452
|
-
"NOTE: Automatically collecting formulation information. The section may include sensitive data such as emails and secrets.\nPlease review the generated SBOM before distribution or LLM training.",
|
|
453
|
-
});
|
|
568
|
+
warnings.push(formulationPrivacyNotice());
|
|
454
569
|
options.includeFormulation = true;
|
|
570
|
+
} else if (introspectProfileAddsFormulation) {
|
|
571
|
+
warnings.push(formulationPrivacyNotice());
|
|
455
572
|
}
|
|
456
573
|
}
|
|
457
574
|
|