@cyclonedx/cdxgen 12.1.5 → 12.2.1
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 +51 -40
- package/bin/cdxgen.js +194 -97
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +449 -429
- package/lib/cli/index.poku.js +117 -0
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/analyzer.js +606 -3
- package/lib/helpers/analyzer.poku.js +230 -0
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +219 -0
- package/lib/helpers/depsUtils.poku.js +207 -0
- package/lib/helpers/display.js +426 -5
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/utils.js +865 -416
- package/lib/helpers/utils.poku.js +172 -265
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +34 -1
- package/lib/server/server.js +50 -13
- package/lib/server/server.poku.js +332 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +196 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -9
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +533 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +22 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- package/types/stages/pregen/pregen.d.ts.map +0 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CycloneDX annotation reporter — embeds findings as `annotations[]` entries
|
|
3
|
+
* on a copy of the input BOM. Can be reused by `bom-audit`.
|
|
4
|
+
*
|
|
5
|
+
* CycloneDX supports the annotation schema from spec version 1.5 onward.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DEBUG_MODE, getTimestamp } from "../../helpers/utils.js";
|
|
9
|
+
|
|
10
|
+
const SUPPORTED_FROM = 1.5;
|
|
11
|
+
const CODE_BLOCK = "```";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a set of findings into CycloneDX annotations.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array<object>} findings Finding objects emitted by the validator or auditBom engine.
|
|
17
|
+
* @param {object} bomJson Full CycloneDX BOM (needed for annotator/subject wiring).
|
|
18
|
+
* @returns {Array<object>} CycloneDX annotation objects.
|
|
19
|
+
*/
|
|
20
|
+
export function buildAnnotations(findings, bomJson) {
|
|
21
|
+
if (!findings?.length || !bomJson) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const specVersion = Number.parseFloat(bomJson.specVersion);
|
|
25
|
+
if (Number.isNaN(specVersion) || specVersion < SUPPORTED_FROM) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const cdxgenAnnotator =
|
|
29
|
+
bomJson?.metadata?.tools?.components?.filter((c) => c?.name === "cdxgen") ??
|
|
30
|
+
[];
|
|
31
|
+
if (!cdxgenAnnotator.length) {
|
|
32
|
+
if (DEBUG_MODE) {
|
|
33
|
+
console.warn(
|
|
34
|
+
"Cannot create audit annotations: cdxgen tool component not found in metadata",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const subjects = [bomJson.serialNumber];
|
|
40
|
+
const timestamp = getTimestamp();
|
|
41
|
+
return findings.map((f) => {
|
|
42
|
+
const properties = [
|
|
43
|
+
{ name: "cdx:validate:engine", value: f.engine || "compliance" },
|
|
44
|
+
{ name: "cdx:validate:ruleId", value: f.ruleId },
|
|
45
|
+
{ name: "cdx:validate:status", value: f.status },
|
|
46
|
+
{ name: "cdx:validate:severity", value: f.severity },
|
|
47
|
+
];
|
|
48
|
+
if (f.standard) {
|
|
49
|
+
properties.push({ name: "cdx:validate:standard", value: f.standard });
|
|
50
|
+
}
|
|
51
|
+
if (f.standardRefs?.length) {
|
|
52
|
+
properties.push({
|
|
53
|
+
name: "cdx:validate:standardRefs",
|
|
54
|
+
value: f.standardRefs.join(","),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (f.category) {
|
|
58
|
+
properties.push({ name: "cdx:validate:category", value: f.category });
|
|
59
|
+
}
|
|
60
|
+
if (f.mitigation) {
|
|
61
|
+
properties.push({ name: "cdx:validate:mitigation", value: f.mitigation });
|
|
62
|
+
}
|
|
63
|
+
if (f.scvsLevels?.length) {
|
|
64
|
+
properties.push({
|
|
65
|
+
name: "cdx:validate:scvsLevels",
|
|
66
|
+
value: f.scvsLevels.join(","),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (f.evidence && typeof f.evidence === "object") {
|
|
70
|
+
for (const [key, value] of Object.entries(f.evidence)) {
|
|
71
|
+
properties.push({
|
|
72
|
+
name: `cdx:validate:evidence:${key}`,
|
|
73
|
+
value:
|
|
74
|
+
typeof value === "object" ? JSON.stringify(value) : String(value),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
subjects,
|
|
80
|
+
annotator: {
|
|
81
|
+
component: cdxgenAnnotator[0],
|
|
82
|
+
},
|
|
83
|
+
timestamp,
|
|
84
|
+
text: `${f.message}\n${CODE_BLOCK}\n${JSON.stringify(properties)}\n${CODE_BLOCK}`,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Produce a new BOM object with findings embedded as annotations. The caller
|
|
91
|
+
* is responsible for writing the result to disk.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} bomJson
|
|
94
|
+
* @param {Array<object>} findings
|
|
95
|
+
* @returns {object}
|
|
96
|
+
*/
|
|
97
|
+
export function renderBom(bomJson, findings) {
|
|
98
|
+
if (!bomJson) {
|
|
99
|
+
return bomJson;
|
|
100
|
+
}
|
|
101
|
+
const annotations = buildAnnotations(findings, bomJson);
|
|
102
|
+
const next = { ...bomJson };
|
|
103
|
+
next.annotations = [...(bomJson.annotations || []), ...annotations];
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Convenience wrapper matching the signature of the other reporters. The
|
|
109
|
+
* second argument expects `{ bomJson }` because annotations are BOM-shaped,
|
|
110
|
+
* not report-shaped.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} report Output of validateBomAdvanced().
|
|
113
|
+
* @param {object} options
|
|
114
|
+
* @param {object} options.bomJson The BOM to annotate.
|
|
115
|
+
* @returns {string} JSON string of the annotated BOM.
|
|
116
|
+
*/
|
|
117
|
+
export function render(report, options = {}) {
|
|
118
|
+
const { bomJson } = options;
|
|
119
|
+
const annotated = renderBom(bomJson, report.findings || []);
|
|
120
|
+
return JSON.stringify(annotated, null, null);
|
|
121
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console reporter — renders findings and benchmark scorecards as tables.
|
|
3
|
+
* Uses cdxgen's internal table helper for lightweight rendering.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { table } from "../../helpers/table.js";
|
|
7
|
+
|
|
8
|
+
const SEVERITY_ICONS = {
|
|
9
|
+
critical: "⛔",
|
|
10
|
+
high: "🔴",
|
|
11
|
+
medium: "🟠",
|
|
12
|
+
low: "🟡",
|
|
13
|
+
info: "🔵",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} f Finding
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function severityIcon(f) {
|
|
21
|
+
return SEVERITY_ICONS[f.severity] || "·";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Produce a human-readable summary of findings.
|
|
26
|
+
*
|
|
27
|
+
* @param {Array<object>} findings
|
|
28
|
+
* @param {object} [options]
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function formatFindings(findings, options = {}) {
|
|
32
|
+
if (!findings || findings.length === 0) {
|
|
33
|
+
return "No findings.";
|
|
34
|
+
}
|
|
35
|
+
const data = [["", "Rule", "Status", "Severity", "Standard", "Message"]];
|
|
36
|
+
for (const f of findings) {
|
|
37
|
+
data.push([
|
|
38
|
+
severityIcon(f),
|
|
39
|
+
f.ruleId,
|
|
40
|
+
f.status,
|
|
41
|
+
f.severity,
|
|
42
|
+
(f.standardRefs || []).join(", ") || f.standard || "",
|
|
43
|
+
f.message,
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
const config = {
|
|
47
|
+
columnDefault: { wrapWord: true },
|
|
48
|
+
columns: [
|
|
49
|
+
{ width: 2 },
|
|
50
|
+
{ width: 15 },
|
|
51
|
+
{ width: 8 },
|
|
52
|
+
{ width: 10 },
|
|
53
|
+
{ width: 20 },
|
|
54
|
+
{ width: 60 },
|
|
55
|
+
],
|
|
56
|
+
header: {
|
|
57
|
+
alignment: "center",
|
|
58
|
+
content: options.title || "cdx-validate findings",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
return table(data, config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Produce a scorecard table for benchmark reports.
|
|
66
|
+
*
|
|
67
|
+
* @param {Array<object>} reports
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function formatBenchmarks(reports) {
|
|
71
|
+
if (!reports || reports.length === 0) {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
const data = [
|
|
75
|
+
["Benchmark", "Controls", "Pass", "Fail", "Manual", "Automatable score"],
|
|
76
|
+
];
|
|
77
|
+
for (const r of reports) {
|
|
78
|
+
data.push([
|
|
79
|
+
r.name,
|
|
80
|
+
String(r.totalControls),
|
|
81
|
+
String(r.pass),
|
|
82
|
+
String(r.fail),
|
|
83
|
+
String(r.manual),
|
|
84
|
+
`${r.scorePct}% (${r.pass}/${r.automatable})`,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
const config = {
|
|
88
|
+
columnDefault: { wrapWord: true },
|
|
89
|
+
columns: [
|
|
90
|
+
{ width: 40 },
|
|
91
|
+
{ width: 8 },
|
|
92
|
+
{ width: 6 },
|
|
93
|
+
{ width: 6 },
|
|
94
|
+
{ width: 8 },
|
|
95
|
+
{ width: 24 },
|
|
96
|
+
],
|
|
97
|
+
header: {
|
|
98
|
+
alignment: "center",
|
|
99
|
+
content: "cdx-validate benchmark scorecards",
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
return table(data, config);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Produce a compact one-line summary for CI logs.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} summary
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
export function formatSummary(summary) {
|
|
112
|
+
return [
|
|
113
|
+
`schemaValid=${summary.schemaValid}`,
|
|
114
|
+
`deepValid=${summary.deepValid}`,
|
|
115
|
+
`pass=${summary.pass}`,
|
|
116
|
+
`fail=${summary.fail}`,
|
|
117
|
+
`manual=${summary.manual}`,
|
|
118
|
+
`errors=${summary.errors}`,
|
|
119
|
+
].join(" ");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render the full report as a single string.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} report Output of validateBomAdvanced().
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function render(report) {
|
|
129
|
+
const pieces = [];
|
|
130
|
+
pieces.push(formatSummary(report.summary));
|
|
131
|
+
if (report.benchmarks?.length) {
|
|
132
|
+
pieces.push(formatBenchmarks(report.benchmarks));
|
|
133
|
+
}
|
|
134
|
+
const actionable = report.findings.filter(
|
|
135
|
+
(f) => f.status === "fail" || f.severity === "critical",
|
|
136
|
+
);
|
|
137
|
+
if (actionable.length) {
|
|
138
|
+
pieces.push(formatFindings(actionable, { title: "Failing controls" }));
|
|
139
|
+
}
|
|
140
|
+
const manual = report.findings.filter((f) => f.status === "manual");
|
|
141
|
+
if (manual.length) {
|
|
142
|
+
pieces.push(
|
|
143
|
+
formatFindings(manual, {
|
|
144
|
+
title: `Manual review required (${manual.length})`,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return pieces.filter(Boolean).join("\n");
|
|
149
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporter registry — dispatches to the requested reporter.
|
|
3
|
+
*
|
|
4
|
+
* Reporters are intentionally kept as independent modules so they can also be
|
|
5
|
+
* consumed by the BOM audit engine or future validators.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as annotations from "./annotations.js";
|
|
9
|
+
import * as consoleReporter from "./console.js";
|
|
10
|
+
import * as json from "./json.js";
|
|
11
|
+
import * as sarif from "./sarif.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map of reporter name → module.
|
|
15
|
+
*/
|
|
16
|
+
export const reporters = {
|
|
17
|
+
console: consoleReporter,
|
|
18
|
+
json,
|
|
19
|
+
sarif,
|
|
20
|
+
annotations,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a validation report using the named reporter.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} name Reporter identifier.
|
|
27
|
+
* @param {object} report Output of validateBomAdvanced().
|
|
28
|
+
* @param {object} [opts] Reporter-specific options.
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function render(name, report, opts) {
|
|
32
|
+
const reporter = reporters[(name || "console").toLowerCase()];
|
|
33
|
+
if (!reporter) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Unknown reporter '${name}'. Expected one of: ${Object.keys(reporters).join(", ")}.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return reporter.render(report, opts);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { annotations, consoleReporter as console, json, sarif };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON reporter — emits a stable, documented structure for programmatic use.
|
|
3
|
+
* No dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} report Output of validateBomAdvanced().
|
|
8
|
+
* @param {object} [_options] Unused
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function render(report, _options = {}) {
|
|
12
|
+
const payload = {
|
|
13
|
+
schemaValid: report.schemaValid,
|
|
14
|
+
deepValid: report.deepValid,
|
|
15
|
+
signatureVerified: report.signatureVerified ?? null,
|
|
16
|
+
summary: report.summary,
|
|
17
|
+
benchmarks: report.benchmarks || [],
|
|
18
|
+
findings: (report.findings || []).map((f) => ({
|
|
19
|
+
ruleId: f.ruleId,
|
|
20
|
+
name: f.name,
|
|
21
|
+
description: f.description,
|
|
22
|
+
engine: f.engine,
|
|
23
|
+
standard: f.standard,
|
|
24
|
+
standardRefs: f.standardRefs,
|
|
25
|
+
scvsLevels: f.scvsLevels,
|
|
26
|
+
category: f.category,
|
|
27
|
+
status: f.status,
|
|
28
|
+
severity: f.severity,
|
|
29
|
+
automatable: f.automatable,
|
|
30
|
+
message: f.message,
|
|
31
|
+
mitigation: f.mitigation,
|
|
32
|
+
locations: f.locations || [],
|
|
33
|
+
evidence: f.evidence || null,
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
return JSON.stringify(payload, null, null);
|
|
37
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF 2.1.0 reporter — renders findings as a SARIF log suitable for upload
|
|
3
|
+
* to GitHub code scanning or any other SARIF-aware consumer.
|
|
4
|
+
*
|
|
5
|
+
* No external dependencies. Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SARIF_VERSION = "2.1.0";
|
|
9
|
+
const SARIF_SCHEMA =
|
|
10
|
+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map internal severity → SARIF `level` property.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} severity
|
|
16
|
+
* @returns {"error" | "warning" | "note"}
|
|
17
|
+
*/
|
|
18
|
+
function severityToLevel(severity) {
|
|
19
|
+
switch (severity) {
|
|
20
|
+
case "critical":
|
|
21
|
+
case "high":
|
|
22
|
+
return "error";
|
|
23
|
+
case "medium":
|
|
24
|
+
return "warning";
|
|
25
|
+
default:
|
|
26
|
+
return "note";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert an internal location to a SARIF physicalLocation block. Returns null
|
|
32
|
+
* when there is no file context — the caller will fall back to a synthetic
|
|
33
|
+
* location.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} loc
|
|
36
|
+
* @returns {object | null}
|
|
37
|
+
*/
|
|
38
|
+
function toSarifLocation(loc) {
|
|
39
|
+
if (!loc) return null;
|
|
40
|
+
if (loc.file) {
|
|
41
|
+
return {
|
|
42
|
+
physicalLocation: {
|
|
43
|
+
artifactLocation: { uri: loc.file },
|
|
44
|
+
},
|
|
45
|
+
logicalLocations: loc.bomRef
|
|
46
|
+
? [{ fullyQualifiedName: loc.bomRef, kind: "package" }]
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (loc.purl || loc.bomRef) {
|
|
51
|
+
return {
|
|
52
|
+
logicalLocations: [
|
|
53
|
+
{
|
|
54
|
+
fullyQualifiedName: loc.purl || loc.bomRef,
|
|
55
|
+
kind: "package",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the SARIF `rules` array from a catalogue of rule descriptors.
|
|
65
|
+
*
|
|
66
|
+
* @param {Array<object>} findings
|
|
67
|
+
* @returns {Array<object>}
|
|
68
|
+
*/
|
|
69
|
+
function deriveRules(findings) {
|
|
70
|
+
const byId = new Map();
|
|
71
|
+
for (const f of findings) {
|
|
72
|
+
if (byId.has(f.ruleId)) continue;
|
|
73
|
+
byId.set(f.ruleId, {
|
|
74
|
+
id: f.ruleId,
|
|
75
|
+
name: f.name || f.ruleId,
|
|
76
|
+
shortDescription: { text: f.name || f.ruleId },
|
|
77
|
+
fullDescription: {
|
|
78
|
+
text: f.description || f.name || f.ruleId,
|
|
79
|
+
},
|
|
80
|
+
defaultConfiguration: { level: severityToLevel(f.severity) },
|
|
81
|
+
properties: {
|
|
82
|
+
category: f.category,
|
|
83
|
+
standard: f.standard,
|
|
84
|
+
standardRefs: f.standardRefs || [],
|
|
85
|
+
scvsLevels: f.scvsLevels || [],
|
|
86
|
+
automatable: f.automatable !== false,
|
|
87
|
+
engine: f.engine,
|
|
88
|
+
},
|
|
89
|
+
help: f.mitigation
|
|
90
|
+
? {
|
|
91
|
+
text: f.mitigation,
|
|
92
|
+
markdown: `**Remediation:** ${f.mitigation}`,
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return [...byId.values()];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert one finding into a SARIF result.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} f
|
|
104
|
+
* @returns {object}
|
|
105
|
+
*/
|
|
106
|
+
function toSarifResult(f) {
|
|
107
|
+
const locations = (f.locations || []).map(toSarifLocation).filter(Boolean);
|
|
108
|
+
if (locations.length === 0) {
|
|
109
|
+
locations.push({
|
|
110
|
+
logicalLocations: [{ fullyQualifiedName: f.ruleId, kind: "rule" }],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const result = {
|
|
114
|
+
ruleId: f.ruleId,
|
|
115
|
+
level: severityToLevel(f.severity),
|
|
116
|
+
message: { text: f.message || f.name || f.ruleId },
|
|
117
|
+
locations,
|
|
118
|
+
properties: {
|
|
119
|
+
status: f.status,
|
|
120
|
+
severity: f.severity,
|
|
121
|
+
standard: f.standard,
|
|
122
|
+
standardRefs: f.standardRefs || [],
|
|
123
|
+
scvsLevels: f.scvsLevels || [],
|
|
124
|
+
automatable: f.automatable !== false,
|
|
125
|
+
engine: f.engine,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
if (f.evidence && typeof f.evidence === "object") {
|
|
129
|
+
result.properties.evidence = f.evidence;
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render a validation report as SARIF.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} report Output of validateBomAdvanced().
|
|
138
|
+
* @param {object} [options]
|
|
139
|
+
* @param {string} [options.toolName] Override driver name.
|
|
140
|
+
* @param {string} [options.toolVersion] Driver version to embed.
|
|
141
|
+
* @param {boolean} [options.includeManual] Include manual-review findings (default false).
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
export function render(report, options = {}) {
|
|
145
|
+
const {
|
|
146
|
+
toolName = "cdx-validate",
|
|
147
|
+
toolVersion = "v12",
|
|
148
|
+
includeManual = false,
|
|
149
|
+
} = options;
|
|
150
|
+
const findings = (report.findings || []).filter((f) =>
|
|
151
|
+
includeManual ? true : f.status !== "manual",
|
|
152
|
+
);
|
|
153
|
+
const log = {
|
|
154
|
+
$schema: SARIF_SCHEMA,
|
|
155
|
+
version: SARIF_VERSION,
|
|
156
|
+
runs: [
|
|
157
|
+
{
|
|
158
|
+
tool: {
|
|
159
|
+
driver: {
|
|
160
|
+
name: toolName,
|
|
161
|
+
version: toolVersion,
|
|
162
|
+
informationUri: "https://cdxgen.github.io/cdxgen/",
|
|
163
|
+
rules: deriveRules(findings),
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
invocations: [
|
|
167
|
+
{
|
|
168
|
+
executionSuccessful:
|
|
169
|
+
report.summary?.fail === 0 && report.schemaValid !== false,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
results: findings.map(toSarifResult),
|
|
173
|
+
properties: {
|
|
174
|
+
schemaValid: report.schemaValid,
|
|
175
|
+
deepValid: report.deepValid,
|
|
176
|
+
signatureVerified: report.signatureVerified ?? null,
|
|
177
|
+
summary: report.summary,
|
|
178
|
+
benchmarks: report.benchmarks || [],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
return JSON.stringify(log, null, null);
|
|
184
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { buildAnnotations } from "./reporters/annotations.js";
|
|
4
|
+
import { render } from "./reporters/index.js";
|
|
5
|
+
|
|
6
|
+
function sampleReport() {
|
|
7
|
+
return {
|
|
8
|
+
schemaValid: true,
|
|
9
|
+
deepValid: true,
|
|
10
|
+
signatureVerified: null,
|
|
11
|
+
summary: {
|
|
12
|
+
total: 2,
|
|
13
|
+
pass: 0,
|
|
14
|
+
fail: 1,
|
|
15
|
+
manual: 1,
|
|
16
|
+
errors: 1,
|
|
17
|
+
warnings: 0,
|
|
18
|
+
schemaValid: true,
|
|
19
|
+
deepValid: true,
|
|
20
|
+
},
|
|
21
|
+
benchmarks: [
|
|
22
|
+
{
|
|
23
|
+
id: "scvs-l1",
|
|
24
|
+
name: "OWASP SCVS Level 1",
|
|
25
|
+
standard: "SCVS",
|
|
26
|
+
totalControls: 2,
|
|
27
|
+
pass: 0,
|
|
28
|
+
fail: 1,
|
|
29
|
+
manual: 1,
|
|
30
|
+
automatable: 1,
|
|
31
|
+
scorePct: 0,
|
|
32
|
+
controls: [],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
findings: [
|
|
36
|
+
{
|
|
37
|
+
engine: "compliance",
|
|
38
|
+
ruleId: "SCVS-2.4",
|
|
39
|
+
name: "SBOM is signed",
|
|
40
|
+
description: "SBOM must be signed.",
|
|
41
|
+
standard: "SCVS",
|
|
42
|
+
standardRefs: ["SCVS-2.4"],
|
|
43
|
+
scvsLevels: ["L2", "L3"],
|
|
44
|
+
category: "compliance-scvs",
|
|
45
|
+
status: "fail",
|
|
46
|
+
severity: "high",
|
|
47
|
+
automatable: true,
|
|
48
|
+
message: "BOM is not signed.",
|
|
49
|
+
mitigation: "Use cdx-sign.",
|
|
50
|
+
locations: [{ file: "bom.json" }],
|
|
51
|
+
evidence: { reason: "no-signature" },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
engine: "compliance",
|
|
55
|
+
ruleId: "SCVS-1.5",
|
|
56
|
+
name: "Manual procurement check",
|
|
57
|
+
description: "Manual.",
|
|
58
|
+
standard: "SCVS",
|
|
59
|
+
standardRefs: ["SCVS-1.5"],
|
|
60
|
+
scvsLevels: ["L2", "L3"],
|
|
61
|
+
category: "compliance-scvs",
|
|
62
|
+
status: "manual",
|
|
63
|
+
severity: "info",
|
|
64
|
+
automatable: false,
|
|
65
|
+
message: "Manual review.",
|
|
66
|
+
locations: [],
|
|
67
|
+
evidence: null,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("reporter dispatcher", () => {
|
|
74
|
+
it("throws for unknown reporter", () => {
|
|
75
|
+
assert.throws(() => render("xml", sampleReport()), /Unknown reporter/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("console reporter renders a non-empty string", () => {
|
|
79
|
+
const out = render("console", sampleReport());
|
|
80
|
+
assert.ok(typeof out === "string");
|
|
81
|
+
assert.match(out, /cdx-validate/);
|
|
82
|
+
assert.match(out, /SCVS-2\.4/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("json reporter emits stable schema", () => {
|
|
86
|
+
const out = render("json", sampleReport());
|
|
87
|
+
const parsed = JSON.parse(out);
|
|
88
|
+
assert.strictEqual(parsed.schemaValid, true);
|
|
89
|
+
assert.strictEqual(parsed.benchmarks.length, 1);
|
|
90
|
+
assert.strictEqual(parsed.findings.length, 2);
|
|
91
|
+
assert.strictEqual(parsed.findings[0].ruleId, "SCVS-2.4");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("sarif reporter emits valid 2.1.0 structure", () => {
|
|
95
|
+
const out = render("sarif", sampleReport(), {
|
|
96
|
+
toolName: "cdx-validate",
|
|
97
|
+
toolVersion: "1.2.3",
|
|
98
|
+
});
|
|
99
|
+
const parsed = JSON.parse(out);
|
|
100
|
+
assert.strictEqual(parsed.version, "2.1.0");
|
|
101
|
+
assert.strictEqual(parsed.runs[0].tool.driver.name, "cdx-validate");
|
|
102
|
+
assert.strictEqual(parsed.runs[0].tool.driver.version, "1.2.3");
|
|
103
|
+
// Manual findings are hidden by default.
|
|
104
|
+
assert.strictEqual(parsed.runs[0].results.length, 1);
|
|
105
|
+
assert.strictEqual(parsed.runs[0].results[0].ruleId, "SCVS-2.4");
|
|
106
|
+
assert.strictEqual(parsed.runs[0].results[0].level, "error");
|
|
107
|
+
// Driver rules must be unique and reference the only remaining finding.
|
|
108
|
+
assert.strictEqual(parsed.runs[0].tool.driver.rules.length, 1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("sarif reporter can include manual findings when requested", () => {
|
|
112
|
+
const out = render("sarif", sampleReport(), { includeManual: true });
|
|
113
|
+
const parsed = JSON.parse(out);
|
|
114
|
+
assert.strictEqual(parsed.runs[0].results.length, 2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("annotations reporter returns the BOM with annotations appended", () => {
|
|
118
|
+
const bomJson = {
|
|
119
|
+
bomFormat: "CycloneDX",
|
|
120
|
+
specVersion: "1.6",
|
|
121
|
+
serialNumber: "urn:uuid:1b671687-395b-41f5-a30f-a58921a69b79",
|
|
122
|
+
metadata: {
|
|
123
|
+
tools: {
|
|
124
|
+
components: [
|
|
125
|
+
{ type: "application", name: "cdxgen", version: "12.0.0" },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
component: { name: "demo", "bom-ref": "demo", type: "application" },
|
|
129
|
+
},
|
|
130
|
+
components: [{ name: "demo", "bom-ref": "demo", type: "library" }],
|
|
131
|
+
};
|
|
132
|
+
const out = render("annotations", sampleReport(), { bomJson });
|
|
133
|
+
const parsed = JSON.parse(out);
|
|
134
|
+
assert.ok(Object.keys(parsed?.annotations));
|
|
135
|
+
assert.strictEqual(parsed.annotations.length, 2);
|
|
136
|
+
const first = parsed.annotations[0];
|
|
137
|
+
assert.ok(first.subjects[0].includes(bomJson.serialNumber));
|
|
138
|
+
assert.ok(first.annotator);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("annotations reporter skips when spec version is below 1.5", () => {
|
|
142
|
+
const bomJson = {
|
|
143
|
+
bomFormat: "CycloneDX",
|
|
144
|
+
specVersion: "1.4",
|
|
145
|
+
metadata: { component: { name: "old" } },
|
|
146
|
+
};
|
|
147
|
+
const ann = buildAnnotations(sampleReport().findings, bomJson);
|
|
148
|
+
assert.strictEqual(Object.keys(ann).length, 0);
|
|
149
|
+
});
|
|
150
|
+
});
|