@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
|
@@ -6,16 +6,21 @@ import { join, resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import { DEBUG_MODE } from "../../core/activity.js";
|
|
9
|
-
import {
|
|
10
|
-
import { buildAnnotationText } from "../../helpers/annotationFormatter.js";
|
|
9
|
+
import { safeExistsSync } from "../../core/fs.js";
|
|
11
10
|
import { collectAiOversight } from "../../inventory/aiOversightCollector.js";
|
|
12
11
|
import { collectAiProvenance } from "../../inventory/aiProvenanceCollector.js";
|
|
13
12
|
import {
|
|
14
13
|
expandBomAuditCategories,
|
|
14
|
+
OPT_IN_BOM_AUDIT_CATEGORIES,
|
|
15
15
|
validateBomAuditCategories,
|
|
16
16
|
} from "../../inventory/auditCategories.js";
|
|
17
17
|
import { isHbomLikeBom as isHbomLikeBomDocument } from "../../inventory/hbomAnalysis.js";
|
|
18
18
|
import { table } from "../../inventory/table.js";
|
|
19
|
+
import {
|
|
20
|
+
buildDocumentAnnotation,
|
|
21
|
+
evidenceProperties,
|
|
22
|
+
warnMissingAnnotator,
|
|
23
|
+
} from "./annotationBuilder.js";
|
|
19
24
|
import { evaluateRules, loadRules } from "./ruleEngine.js";
|
|
20
25
|
|
|
21
26
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -53,6 +58,12 @@ async function loadConfiguredBomAuditRules(options = {}) {
|
|
|
53
58
|
);
|
|
54
59
|
}
|
|
55
60
|
}
|
|
61
|
+
} else {
|
|
62
|
+
// Without explicit categories every loaded rule would activate, so rules
|
|
63
|
+
// belonging to opt-in-only categories are removed here.
|
|
64
|
+
activeRules = rules.filter(
|
|
65
|
+
(r) => !OPT_IN_BOM_AUDIT_CATEGORIES.includes(r.category),
|
|
66
|
+
);
|
|
56
67
|
}
|
|
57
68
|
return {
|
|
58
69
|
activeRules,
|
|
@@ -369,89 +380,75 @@ export function formatAnnotations(findings, bomJson) {
|
|
|
369
380
|
if (!findings?.length) {
|
|
370
381
|
return [];
|
|
371
382
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
{ name: "cdx:audit:severity", value: f.severity },
|
|
388
|
-
{ name: "cdx:audit:category", value: f.category },
|
|
389
|
-
];
|
|
390
|
-
if (f.name) {
|
|
391
|
-
properties.push({ name: "cdx:audit:name", value: f.name });
|
|
392
|
-
}
|
|
393
|
-
if (f.mitigation) {
|
|
394
|
-
properties.push({ name: "cdx:audit:mitigation", value: f.mitigation });
|
|
395
|
-
}
|
|
396
|
-
if (f.attackTactics?.length) {
|
|
397
|
-
properties.push({
|
|
398
|
-
name: "cdx:audit:attack:tactics",
|
|
399
|
-
value: f.attackTactics.join(","),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
if (f.attackTechniques?.length) {
|
|
403
|
-
properties.push({
|
|
404
|
-
name: "cdx:audit:attack:techniques",
|
|
405
|
-
value: f.attackTechniques.join(","),
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
if (f.standards && typeof f.standards === "object") {
|
|
409
|
-
for (const [standardName, entries] of Object.entries(f.standards)) {
|
|
383
|
+
let warnedMissingAnnotator = false;
|
|
384
|
+
return findings
|
|
385
|
+
.map((f) => {
|
|
386
|
+
const properties = [
|
|
387
|
+
{ name: "cdx:audit:ruleId", value: f.ruleId },
|
|
388
|
+
{ name: "cdx:audit:severity", value: f.severity },
|
|
389
|
+
{ name: "cdx:audit:category", value: f.category },
|
|
390
|
+
];
|
|
391
|
+
if (f.name) {
|
|
392
|
+
properties.push({ name: "cdx:audit:name", value: f.name });
|
|
393
|
+
}
|
|
394
|
+
if (f.mitigation) {
|
|
395
|
+
properties.push({ name: "cdx:audit:mitigation", value: f.mitigation });
|
|
396
|
+
}
|
|
397
|
+
if (f.attackTactics?.length) {
|
|
410
398
|
properties.push({
|
|
411
|
-
name:
|
|
412
|
-
value:
|
|
399
|
+
name: "cdx:audit:attack:tactics",
|
|
400
|
+
value: f.attackTactics.join(","),
|
|
413
401
|
});
|
|
414
402
|
}
|
|
415
|
-
|
|
416
|
-
if (f?.location?.purl) {
|
|
417
|
-
properties.push({
|
|
418
|
-
name: "cdx:audit:location:purl",
|
|
419
|
-
value: f.location.purl,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
if (f.location?.file) {
|
|
423
|
-
properties.push({
|
|
424
|
-
name: "cdx:audit:location:file",
|
|
425
|
-
value: f.location.file,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
if (f.location?.bomRef) {
|
|
429
|
-
properties.push({
|
|
430
|
-
name: "cdx:audit:location:bomRef",
|
|
431
|
-
value: f.location.bomRef,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
if (f.evidence && typeof f.evidence === "object") {
|
|
435
|
-
for (const [key, value] of Object.entries(f.evidence)) {
|
|
436
|
-
const propValue =
|
|
437
|
-
typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
403
|
+
if (f.attackTechniques?.length) {
|
|
438
404
|
properties.push({
|
|
439
|
-
name:
|
|
440
|
-
value:
|
|
405
|
+
name: "cdx:audit:attack:techniques",
|
|
406
|
+
value: f.attackTechniques.join(","),
|
|
441
407
|
});
|
|
442
408
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
409
|
+
if (f.standards && typeof f.standards === "object") {
|
|
410
|
+
for (const [standardName, entries] of Object.entries(f.standards)) {
|
|
411
|
+
properties.push({
|
|
412
|
+
name: `cdx:audit:standards:${standardName}`,
|
|
413
|
+
value: Array.isArray(entries) ? entries.join(",") : String(entries),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (f?.location?.purl) {
|
|
418
|
+
properties.push({
|
|
419
|
+
name: "cdx:audit:location:purl",
|
|
420
|
+
value: f.location.purl,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (f.location?.file) {
|
|
424
|
+
properties.push({
|
|
425
|
+
name: "cdx:audit:location:file",
|
|
426
|
+
value: f.location.file,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (f.location?.bomRef) {
|
|
430
|
+
properties.push({
|
|
431
|
+
name: "cdx:audit:location:bomRef",
|
|
432
|
+
value: f.location.bomRef,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
properties.push(...evidenceProperties("cdx:audit", f.evidence));
|
|
436
|
+
return buildDocumentAnnotation({
|
|
437
|
+
bomJson,
|
|
438
|
+
message: f.message,
|
|
439
|
+
properties,
|
|
440
|
+
});
|
|
441
|
+
})
|
|
442
|
+
.filter((annotation) => {
|
|
443
|
+
if (!annotation) {
|
|
444
|
+
if (!warnedMissingAnnotator) {
|
|
445
|
+
warnMissingAnnotator("audit");
|
|
446
|
+
warnedMissingAnnotator = true;
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
});
|
|
455
452
|
}
|
|
456
453
|
|
|
457
454
|
/**
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOM mutations that carry the introspection verdict inside the document:
|
|
3
|
+
* eight metadata properties and a block of document-level annotations.
|
|
4
|
+
*
|
|
5
|
+
* Sidecar report files get lost; the BOM travels. A consumer who receives
|
|
6
|
+
* only the BOM still learns how much to trust it from the metadata properties,
|
|
7
|
+
* and the annotations carry one entry per remediation the loop should act on.
|
|
8
|
+
* No component or dependency is touched.
|
|
9
|
+
*
|
|
10
|
+
* Both mutations are replace-by-name: a document that was already introspected
|
|
11
|
+
* (a BOM enriched again after an evidence pass) loses its previous
|
|
12
|
+
* introspection state and gains a fresh one, so the verdict always describes
|
|
13
|
+
* the final document.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
buildDocumentAnnotation,
|
|
18
|
+
evidenceProperties,
|
|
19
|
+
} from "../annotationBuilder.js";
|
|
20
|
+
import { FORMULATION_RUN_ID_PROPERTY } from "./formulationEvidence.js";
|
|
21
|
+
import { overallAssessment } from "./report.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Version of the introspection BOM surface. Matches the JSON report's
|
|
25
|
+
* schemaVersion so the two stay comparable.
|
|
26
|
+
*
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
export const INTROSPECTION_SCHEMA_VERSION = "1.1";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Namespace of every property and annotation entry this module emits.
|
|
33
|
+
*
|
|
34
|
+
* @type {string}
|
|
35
|
+
*/
|
|
36
|
+
export const INTROSPECTION_PROPERTY_PREFIX = "cdx:introspection";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The metadata properties that summarize the verdict, in emission order. The
|
|
40
|
+
* overall tier and confidence are omitted when the run produced no scored row
|
|
41
|
+
* (nothing was graded, so no verdict is claimed); per-ecosystem rows cover the
|
|
42
|
+
* scored ecosystems only, since unsupported ones have neither a tier nor a
|
|
43
|
+
* score.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} reflection Reflection document from reflectOnRun.
|
|
46
|
+
* @param {Object} scored Scoring document from scoreReflection.
|
|
47
|
+
* @returns {{name: string, value: string}[]} Metadata property entries.
|
|
48
|
+
*/
|
|
49
|
+
export function introspectionMetadataProperties(reflection, scored) {
|
|
50
|
+
const assessment = overallAssessment(scored);
|
|
51
|
+
const properties = [
|
|
52
|
+
{
|
|
53
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:schemaVersion`,
|
|
54
|
+
value: INTROSPECTION_SCHEMA_VERSION,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
if (assessment.tier) {
|
|
58
|
+
properties.push({
|
|
59
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:tier`,
|
|
60
|
+
value: assessment.tier,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
properties.push({
|
|
64
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:score`,
|
|
65
|
+
value: String(scored?.overallScore ?? 100),
|
|
66
|
+
});
|
|
67
|
+
if (assessment.confidence) {
|
|
68
|
+
properties.push({
|
|
69
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:confidence`,
|
|
70
|
+
value: assessment.confidence,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
properties.push({
|
|
74
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:ledgerComplete`,
|
|
75
|
+
value: String(reflection?.ledgerComplete !== false),
|
|
76
|
+
});
|
|
77
|
+
for (const row of sortedScoredRows(scored)) {
|
|
78
|
+
properties.push({
|
|
79
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:ecosystem:${row.ecosystem}:tier`,
|
|
80
|
+
value: `${row.tier}`,
|
|
81
|
+
});
|
|
82
|
+
properties.push({
|
|
83
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:ecosystem:${row.ecosystem}:score`,
|
|
84
|
+
value: String(row.score),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
properties.push({
|
|
88
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:remediationCount`,
|
|
89
|
+
value: String(actionableRemediations(scored).length),
|
|
90
|
+
});
|
|
91
|
+
return properties;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The annotation describing the run as a whole. Its structured facts are the
|
|
96
|
+
* metadata properties, so the summary is readable without the rest of the
|
|
97
|
+
* document.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} bomJson CycloneDX BOM the annotation describes.
|
|
100
|
+
* @param {Object} reflection Reflection document from reflectOnRun.
|
|
101
|
+
* @param {Object} scored Scoring document from scoreReflection.
|
|
102
|
+
* @returns {Object|undefined} The summary annotation, or undefined without a cdxgen annotator.
|
|
103
|
+
*/
|
|
104
|
+
function summaryAnnotation(bomJson, reflection, scored) {
|
|
105
|
+
const assessment = overallAssessment(scored);
|
|
106
|
+
const remediationCount = (
|
|
107
|
+
Array.isArray(scored?.remediations) ? scored.remediations : []
|
|
108
|
+
).length;
|
|
109
|
+
// Annotation text is escaped as markdown before it reaches the BOM, so the
|
|
110
|
+
// summary is phrased without brackets or pipes that would survive as
|
|
111
|
+
// backslashes in the rendered document.
|
|
112
|
+
const message =
|
|
113
|
+
`Build introspection: overall tier ${assessment.tier ?? "ungraded"}, ` +
|
|
114
|
+
`score ${scored?.overallScore ?? 100} of 100, ` +
|
|
115
|
+
`confidence ${assessment.confidence ?? "not available"}. ` +
|
|
116
|
+
`${remediationCount} remediation${remediationCount === 1 ? "" : "s"} ranked.`;
|
|
117
|
+
return buildDocumentAnnotation({
|
|
118
|
+
bomJson,
|
|
119
|
+
message,
|
|
120
|
+
properties: introspectionMetadataProperties(reflection, scored),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* One annotation per non-blocked remediation, in ranked order. Blocked
|
|
126
|
+
* entries are constraints of the environment rather than work the loop can
|
|
127
|
+
* do, so they stay in the report files and out of the document. A remediation
|
|
128
|
+
* that subsumes other rules names them, so nothing the run detected goes
|
|
129
|
+
* unreported.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} bomJson CycloneDX BOM the annotations describe.
|
|
132
|
+
* @param {Object} scored Scoring document from scoreReflection.
|
|
133
|
+
* @returns {Object[]} Remediation annotations.
|
|
134
|
+
*/
|
|
135
|
+
function remediationAnnotations(bomJson, scored) {
|
|
136
|
+
const tierByEcosystem = new Map(
|
|
137
|
+
sortedScoredRows(scored).map((row) => [row.ecosystem, row.tier]),
|
|
138
|
+
);
|
|
139
|
+
const annotations = [];
|
|
140
|
+
for (const entry of actionableRemediations(scored)) {
|
|
141
|
+
const properties = [
|
|
142
|
+
{
|
|
143
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:remediationId`,
|
|
144
|
+
value: `${entry.remediationId}`,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:ecosystem`,
|
|
148
|
+
value: `${entry.ecosystem}`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:currentTier`,
|
|
152
|
+
value: `${tierByEcosystem.get(entry.ecosystem) || entry.targetTier}`,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:targetTier`,
|
|
156
|
+
value: `${entry.targetTier}`,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:expectedGain`,
|
|
160
|
+
value: Number(entry.expectedGain ?? 0).toFixed(2),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:confidence`,
|
|
164
|
+
value: `${entry.confidence}`,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
const rules = Array.isArray(entry.verify?.rules) ? entry.verify.rules : [];
|
|
168
|
+
if (rules.length) {
|
|
169
|
+
properties.push({
|
|
170
|
+
name: `${INTROSPECTION_PROPERTY_PREFIX}:verify:rules`,
|
|
171
|
+
value: rules.join(","),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const evidence = { source: entry.source };
|
|
175
|
+
if (entry.impact) {
|
|
176
|
+
evidence.impact = entry.impact;
|
|
177
|
+
}
|
|
178
|
+
if (entry.severity) {
|
|
179
|
+
evidence.severity = entry.severity;
|
|
180
|
+
}
|
|
181
|
+
if (Number.isFinite(entry.evidenceCount)) {
|
|
182
|
+
evidence.events = entry.evidenceCount;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(entry.subsumes) && entry.subsumes.length) {
|
|
185
|
+
evidence.subsumes = entry.subsumes;
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
Array.isArray(entry.verify?.eventsCleared) &&
|
|
189
|
+
entry.verify.eventsCleared.length
|
|
190
|
+
) {
|
|
191
|
+
evidence.eventsCleared = entry.verify.eventsCleared;
|
|
192
|
+
}
|
|
193
|
+
properties.push(
|
|
194
|
+
...evidenceProperties(INTROSPECTION_PROPERTY_PREFIX, evidence),
|
|
195
|
+
);
|
|
196
|
+
const annotation = buildDocumentAnnotation({
|
|
197
|
+
bomJson,
|
|
198
|
+
message: `${entry.summary || entry.remediationId}`,
|
|
199
|
+
properties,
|
|
200
|
+
});
|
|
201
|
+
if (annotation) {
|
|
202
|
+
annotations.push(annotation);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return annotations;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Attach the introspection verdict to the document: metadata properties plus
|
|
210
|
+
* the summary and remediation annotations. Any introspection state left on the
|
|
211
|
+
* document by a previous pass is removed first, so enrichment flows that run
|
|
212
|
+
* post-processing twice leave exactly one verdict.
|
|
213
|
+
*
|
|
214
|
+
* @param {Object} bomJson CycloneDX BOM, mutated in place.
|
|
215
|
+
* @param {Object} reflection Reflection document from reflectOnRun.
|
|
216
|
+
* @param {Object} scored Scoring document from scoreReflection.
|
|
217
|
+
* @param {Object} [guards] Emission guards.
|
|
218
|
+
* @param {boolean} [guards.annotations] False skips the annotations, for documents at a spec version that predates them.
|
|
219
|
+
* @returns {Object} The mutated BOM.
|
|
220
|
+
*/
|
|
221
|
+
export function applyIntrospectionToBom(
|
|
222
|
+
bomJson,
|
|
223
|
+
reflection,
|
|
224
|
+
scored,
|
|
225
|
+
guards = {},
|
|
226
|
+
) {
|
|
227
|
+
if (!bomJson?.metadata) {
|
|
228
|
+
return bomJson;
|
|
229
|
+
}
|
|
230
|
+
const freshProperties = introspectionMetadataProperties(reflection, scored);
|
|
231
|
+
const retainedProperties = (
|
|
232
|
+
Array.isArray(bomJson.metadata.properties)
|
|
233
|
+
? bomJson.metadata.properties
|
|
234
|
+
: []
|
|
235
|
+
).filter(
|
|
236
|
+
(property) =>
|
|
237
|
+
!`${property?.name || ""}`.startsWith(INTROSPECTION_PROPERTY_PREFIX),
|
|
238
|
+
);
|
|
239
|
+
bomJson.metadata.properties = [...retainedProperties, ...freshProperties];
|
|
240
|
+
if (guards.annotations === false) {
|
|
241
|
+
return bomJson;
|
|
242
|
+
}
|
|
243
|
+
const retainedAnnotations = (
|
|
244
|
+
Array.isArray(bomJson.annotations) ? bomJson.annotations : []
|
|
245
|
+
).filter(
|
|
246
|
+
(annotation) =>
|
|
247
|
+
!`${annotation?.text || ""}`.includes(INTROSPECTION_PROPERTY_PREFIX),
|
|
248
|
+
);
|
|
249
|
+
const freshAnnotations = [
|
|
250
|
+
summaryAnnotation(bomJson, reflection, scored),
|
|
251
|
+
...remediationAnnotations(bomJson, scored),
|
|
252
|
+
].filter(Boolean);
|
|
253
|
+
if (freshAnnotations.length) {
|
|
254
|
+
bomJson.annotations = [...retainedAnnotations, ...freshAnnotations];
|
|
255
|
+
}
|
|
256
|
+
return bomJson;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Scored rows in ecosystem order. */
|
|
260
|
+
function sortedScoredRows(scored) {
|
|
261
|
+
return [...(Array.isArray(scored?.ecosystems) ? scored.ecosystems : [])]
|
|
262
|
+
.filter((row) => row?.ecosystem)
|
|
263
|
+
.sort((a, b) => `${a.ecosystem}`.localeCompare(`${b.ecosystem}`));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Remediations the loop can act on, in ranked order. */
|
|
267
|
+
function actionableRemediations(scored) {
|
|
268
|
+
return (
|
|
269
|
+
Array.isArray(scored?.remediations) ? scored.remediations : []
|
|
270
|
+
).filter((entry) => entry?.blocked !== true);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stamp every formulation workflow with the id of the run that reflected over
|
|
275
|
+
* the document, so a later consumer of the BOM can tell a formulation record
|
|
276
|
+
* this run generated from one that arrived with a foreign BOM. The stamp is
|
|
277
|
+
* replace-by-name, matching how the rest of the verdict mutates the
|
|
278
|
+
* document: a BOM enriched twice keeps only the latest run's id.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} bomJson CycloneDX BOM, mutated in place.
|
|
281
|
+
* @param {string} runId Run id of the introspection pass.
|
|
282
|
+
* @returns {Object} The mutated BOM.
|
|
283
|
+
*/
|
|
284
|
+
export function applyFormulationRunIdMarker(bomJson, runId) {
|
|
285
|
+
if (!bomJson || !runId) {
|
|
286
|
+
return bomJson;
|
|
287
|
+
}
|
|
288
|
+
for (const entry of Array.isArray(bomJson.formulation)
|
|
289
|
+
? bomJson.formulation
|
|
290
|
+
: []) {
|
|
291
|
+
for (const workflow of Array.isArray(entry?.workflows)
|
|
292
|
+
? entry.workflows
|
|
293
|
+
: []) {
|
|
294
|
+
const retained = (
|
|
295
|
+
Array.isArray(workflow.properties) ? workflow.properties : []
|
|
296
|
+
).filter((property) => property?.name !== FORMULATION_RUN_ID_PROPERTY);
|
|
297
|
+
workflow.properties = [
|
|
298
|
+
...retained,
|
|
299
|
+
{ name: FORMULATION_RUN_ID_PROPERTY, value: runId },
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return bomJson;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether a BOM already carries an introspection verdict.
|
|
308
|
+
*
|
|
309
|
+
* Evidence collection re-processes a finished BOM through a fresh wrapper, so
|
|
310
|
+
* the document itself is the only marker that survives between passes.
|
|
311
|
+
*
|
|
312
|
+
* @param {Object} bomJson CycloneDX BOM.
|
|
313
|
+
* @returns {boolean} True when the BOM was already introspected.
|
|
314
|
+
*/
|
|
315
|
+
export function hasIntrospectionVerdict(bomJson) {
|
|
316
|
+
return (bomJson?.metadata?.properties || []).some(
|
|
317
|
+
(property) =>
|
|
318
|
+
property?.name === `${INTROSPECTION_PROPERTY_PREFIX}:schemaVersion`,
|
|
319
|
+
);
|
|
320
|
+
}
|