@cyclonedx/cdxgen 12.1.5 → 12.2.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 +47 -39
- package/bin/cdxgen.js +175 -96
- 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 +327 -372
- 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/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 +203 -0
- package/lib/helpers/depsUtils.poku.js +150 -0
- package/lib/helpers/display.js +423 -4
- 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/utils.js +681 -406
- package/lib/helpers/utils.poku.js +55 -255
- 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 +1 -1
- package/lib/server/server.js +40 -11
- package/lib/server/server.poku.js +123 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +197 -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 -8
- 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/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/utils.d.ts +532 -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 +21 -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,1610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal compliance rule catalog for cdx-validate.
|
|
3
|
+
*
|
|
4
|
+
* Implements OWASP SCVS (Software Component Verification Standard) controls
|
|
5
|
+
* and selected EU Cyber Resilience Act (CRA) SBOM expectations as plain
|
|
6
|
+
* JavaScript evaluators. Controls that are not automatable from a static
|
|
7
|
+
* CycloneDX BOM (for example, process or organizational controls) are still
|
|
8
|
+
* modelled so that benchmark reports can surface them as "manual review
|
|
9
|
+
* required" items with a stable identifier.
|
|
10
|
+
*
|
|
11
|
+
* Each rule exports:
|
|
12
|
+
* id - Stable short identifier (e.g. "SCVS-1.1").
|
|
13
|
+
* name - Human readable short name.
|
|
14
|
+
* description - Long description (wording taken from the source standard).
|
|
15
|
+
* standard - Source standard key: "SCVS" or "CRA".
|
|
16
|
+
* standardRefs - Array of canonical control identifiers.
|
|
17
|
+
* category - Grouping used by --categories.
|
|
18
|
+
* severity - Severity emitted for a failing automatable rule.
|
|
19
|
+
* scvsLevels - For SCVS rules, the levels (L1/L2/L3) that require the
|
|
20
|
+
* control. Non-SCVS rules use an empty array.
|
|
21
|
+
* automatable - True when evaluate() returns a deterministic pass/fail
|
|
22
|
+
* from the BOM alone. False means the rule is emitted as
|
|
23
|
+
* severity "info" / status "manual" so downstream tooling
|
|
24
|
+
* can track coverage.
|
|
25
|
+
* evaluate - Function(bomJson) => RuleResult.
|
|
26
|
+
*
|
|
27
|
+
* RuleResult shape:
|
|
28
|
+
* {
|
|
29
|
+
* status: "pass" | "fail" | "manual",
|
|
30
|
+
* message: string, // human readable summary
|
|
31
|
+
* mitigation?: string,
|
|
32
|
+
* locations?: Array<{ bomRef?, purl?, file? }>,
|
|
33
|
+
* evidence?: Record<string, any>
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { PackageURL } from "packageurl-js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract the first SPDX-ish license id from a CycloneDX component's licenses
|
|
41
|
+
* block. Returns null when no license is declared.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} comp CycloneDX component
|
|
44
|
+
* @returns {string | null}
|
|
45
|
+
*/
|
|
46
|
+
function componentLicenseId(comp) {
|
|
47
|
+
if (!comp?.licenses?.length) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of comp.licenses) {
|
|
51
|
+
if (entry?.license?.id) {
|
|
52
|
+
return entry.license.id;
|
|
53
|
+
}
|
|
54
|
+
if (entry?.expression) {
|
|
55
|
+
return entry.expression;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const entry of comp.licenses) {
|
|
59
|
+
if (entry?.license?.name) {
|
|
60
|
+
return entry.license.name;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getAllComponents(bomJson) {
|
|
67
|
+
const results = [];
|
|
68
|
+
function traverse(comps) {
|
|
69
|
+
if (!Array.isArray(comps)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const c of comps) {
|
|
73
|
+
if (c?.scope === "excluded") {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
results.push(c);
|
|
77
|
+
if (c.components) {
|
|
78
|
+
traverse(c.components);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
traverse(bomJson?.components);
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Collect libraries/frameworks/applications worth evaluating for inventory
|
|
88
|
+
* checks. Crypto-assets and data types are excluded because they are tracked
|
|
89
|
+
* with different schemas in CycloneDX.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} bomJson
|
|
92
|
+
* @returns {Array<object>}
|
|
93
|
+
*/
|
|
94
|
+
function inventoryComponents(bomJson) {
|
|
95
|
+
if (!Array.isArray(bomJson?.components)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return getAllComponents(bomJson).filter((c) =>
|
|
99
|
+
[
|
|
100
|
+
"application",
|
|
101
|
+
"framework",
|
|
102
|
+
"library",
|
|
103
|
+
"container",
|
|
104
|
+
"operating-system",
|
|
105
|
+
].includes(c?.type),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format a component identifier for console messages.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} comp
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function compLabel(comp) {
|
|
116
|
+
return comp?.purl || comp?.["bom-ref"] || comp?.name || "<unknown>";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a Set of all bom-refs declared anywhere in the BOM so that we can
|
|
121
|
+
* detect orphan components that are not reachable from the dependency tree.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} bomJson
|
|
124
|
+
* @returns {Set<string>}
|
|
125
|
+
*/
|
|
126
|
+
function collectReferencedRefs(bomJson) {
|
|
127
|
+
const refs = new Set();
|
|
128
|
+
const rootRef = bomJson?.metadata?.component?.["bom-ref"];
|
|
129
|
+
if (rootRef) {
|
|
130
|
+
refs.add(rootRef);
|
|
131
|
+
}
|
|
132
|
+
for (const dep of bomJson?.dependencies || []) {
|
|
133
|
+
if (dep?.ref) {
|
|
134
|
+
refs.add(dep.ref);
|
|
135
|
+
}
|
|
136
|
+
for (const child of dep?.dependsOn || []) {
|
|
137
|
+
refs.add(child);
|
|
138
|
+
}
|
|
139
|
+
for (const prov of dep?.provides || []) {
|
|
140
|
+
refs.add(prov);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return refs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate that a license expression is syntactically a known SPDX identifier
|
|
148
|
+
* or an expression built from SPDX operators. This is a best-effort check
|
|
149
|
+
* that tokenises the expression first — avoiding backtracking-heavy regex
|
|
150
|
+
* alternations — and then validates each token with a simple character-class
|
|
151
|
+
* pattern.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} expr
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
function looksLikeSpdx(expr) {
|
|
157
|
+
if (!expr || typeof expr !== "string") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const trimmed = expr.trim();
|
|
161
|
+
// Reject obvious "unknown" placeholders emitted by several tools.
|
|
162
|
+
const lower = trimmed.toLowerCase();
|
|
163
|
+
if (["noassertion", "unknown", "unlicensed", ""].includes(lower)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
// Strip balanced parentheses so we can focus on the identifier+operator
|
|
167
|
+
// shape. Parentheses are structural only in SPDX expressions.
|
|
168
|
+
const withoutParens = trimmed.replace(/[()]/g, " ");
|
|
169
|
+
// Identifiers: alphanumeric, dots, dashes, pluses, slashes, colons.
|
|
170
|
+
const tokenPattern = /^[A-Za-z0-9.+\-/:]+$/;
|
|
171
|
+
const operators = new Set(["AND", "OR", "WITH"]);
|
|
172
|
+
// Split on whitespace once; linear scan below validates every token. Two
|
|
173
|
+
// consecutive operators or two consecutive identifiers both fail.
|
|
174
|
+
const tokens = withoutParens.split(/\s+/).filter(Boolean);
|
|
175
|
+
if (tokens.length === 0) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
let expectIdentifier = true;
|
|
179
|
+
for (const tok of tokens) {
|
|
180
|
+
if (operators.has(tok)) {
|
|
181
|
+
if (expectIdentifier) return false; // operator cannot come first
|
|
182
|
+
expectIdentifier = true;
|
|
183
|
+
} else {
|
|
184
|
+
if (!expectIdentifier) return false; // two identifiers in a row
|
|
185
|
+
if (!tokenPattern.test(tok)) return false;
|
|
186
|
+
expectIdentifier = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Must end on an identifier, not an operator.
|
|
190
|
+
return !expectIdentifier;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Helper to build a standard "pass" rule result.
|
|
195
|
+
*/
|
|
196
|
+
function pass(message, extras = {}) {
|
|
197
|
+
return { status: "pass", message, ...extras };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Helper to build a standard "fail" rule result.
|
|
202
|
+
*/
|
|
203
|
+
function fail(message, extras = {}) {
|
|
204
|
+
return { status: "fail", message, ...extras };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Helper to build a standard "manual" rule result (non-automatable control).
|
|
209
|
+
*/
|
|
210
|
+
function manual(message, extras = {}) {
|
|
211
|
+
return { status: "manual", message, ...extras };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Factory for SCVS manual-review rules. These are emitted so that benchmark
|
|
216
|
+
* reports can accurately reflect per-level coverage even when the rule cannot
|
|
217
|
+
* be evaluated automatically.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} id
|
|
220
|
+
* @param {string} name
|
|
221
|
+
* @param {string} description
|
|
222
|
+
* @param {{ l1: boolean, l2: boolean, l3: boolean }} levels
|
|
223
|
+
* @returns {object}
|
|
224
|
+
*/
|
|
225
|
+
function scvsManual(id, name, description, levels) {
|
|
226
|
+
const required = [];
|
|
227
|
+
if (levels.l1) required.push("L1");
|
|
228
|
+
if (levels.l2) required.push("L2");
|
|
229
|
+
if (levels.l3) required.push("L3");
|
|
230
|
+
return {
|
|
231
|
+
id: `SCVS-${id}`,
|
|
232
|
+
name,
|
|
233
|
+
description,
|
|
234
|
+
standard: "SCVS",
|
|
235
|
+
standardRefs: [`SCVS-${id}`],
|
|
236
|
+
category: "compliance-scvs",
|
|
237
|
+
severity: "info",
|
|
238
|
+
scvsLevels: required,
|
|
239
|
+
automatable: false,
|
|
240
|
+
evaluate: () =>
|
|
241
|
+
manual(
|
|
242
|
+
`${name} is not automatable from the BOM and requires manual review.`,
|
|
243
|
+
{
|
|
244
|
+
mitigation: description,
|
|
245
|
+
},
|
|
246
|
+
),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// OWASP SCVS automatable rules
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
/** @type {Array<object>} */
|
|
255
|
+
const SCVS_RULES = [
|
|
256
|
+
{
|
|
257
|
+
id: "SCVS-1.1",
|
|
258
|
+
name: "Components and versions known",
|
|
259
|
+
description:
|
|
260
|
+
"All direct and transitive components and their versions are known at completion of a build.",
|
|
261
|
+
standard: "SCVS",
|
|
262
|
+
standardRefs: ["SCVS-1.1"],
|
|
263
|
+
category: "compliance-scvs",
|
|
264
|
+
severity: "high",
|
|
265
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
266
|
+
automatable: true,
|
|
267
|
+
evaluate(bomJson) {
|
|
268
|
+
const comps = inventoryComponents(bomJson);
|
|
269
|
+
if (comps.length === 0) {
|
|
270
|
+
return fail(
|
|
271
|
+
"BOM has no application, framework, or library components.",
|
|
272
|
+
{
|
|
273
|
+
mitigation:
|
|
274
|
+
"Regenerate the BOM with cdxgen so that all direct and transitive components are captured.",
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const missing = comps.filter((c) => !c.version);
|
|
279
|
+
if (missing.length) {
|
|
280
|
+
return fail(`${missing.length} component(s) are missing a version.`, {
|
|
281
|
+
mitigation:
|
|
282
|
+
"Ensure lockfiles are committed and cdxgen has access to them; set --project-version for the root component.",
|
|
283
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
284
|
+
bomRef: c["bom-ref"],
|
|
285
|
+
purl: c.purl,
|
|
286
|
+
name: c.name,
|
|
287
|
+
})),
|
|
288
|
+
evidence: { missingVersionCount: missing.length },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return pass(`All ${comps.length} components have a version.`);
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
scvsManual(
|
|
295
|
+
"1.2",
|
|
296
|
+
"Package managers used for third-party binaries",
|
|
297
|
+
"Package managers are used to manage all third-party binary components.",
|
|
298
|
+
{ l1: true, l2: true, l3: true },
|
|
299
|
+
),
|
|
300
|
+
{
|
|
301
|
+
id: "SCVS-1.3",
|
|
302
|
+
name: "Machine-readable third-party inventory",
|
|
303
|
+
description:
|
|
304
|
+
"An accurate inventory of all third-party components is available in a machine-readable format.",
|
|
305
|
+
standard: "SCVS",
|
|
306
|
+
standardRefs: ["SCVS-1.3"],
|
|
307
|
+
category: "compliance-scvs",
|
|
308
|
+
severity: "high",
|
|
309
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
310
|
+
automatable: true,
|
|
311
|
+
evaluate(bomJson) {
|
|
312
|
+
if (bomJson?.bomFormat !== "CycloneDX" || !bomJson?.specVersion) {
|
|
313
|
+
return fail(
|
|
314
|
+
"BOM is not a valid CycloneDX document (bomFormat/specVersion missing).",
|
|
315
|
+
{
|
|
316
|
+
mitigation:
|
|
317
|
+
"Produce the SBOM with cdxgen or another CycloneDX-compliant tool.",
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const comps = inventoryComponents(bomJson);
|
|
322
|
+
return pass(
|
|
323
|
+
`Machine-readable CycloneDX ${bomJson.specVersion} inventory with ${comps.length} component(s).`,
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
scvsManual(
|
|
328
|
+
"1.4",
|
|
329
|
+
"SBOMs generated for published applications",
|
|
330
|
+
"Software bill of materials are generated for publicly or commercially available applications.",
|
|
331
|
+
{ l1: true, l2: true, l3: true },
|
|
332
|
+
),
|
|
333
|
+
scvsManual(
|
|
334
|
+
"1.5",
|
|
335
|
+
"SBOMs required for new procurements",
|
|
336
|
+
"Software bill of materials are required for new procurements.",
|
|
337
|
+
{ l1: false, l2: true, l3: true },
|
|
338
|
+
),
|
|
339
|
+
scvsManual(
|
|
340
|
+
"1.6",
|
|
341
|
+
"SBOMs continuously maintained",
|
|
342
|
+
"Software bill of materials continuously maintained and current for all systems.",
|
|
343
|
+
{ l1: false, l2: false, l3: true },
|
|
344
|
+
),
|
|
345
|
+
{
|
|
346
|
+
id: "SCVS-1.7",
|
|
347
|
+
name: "Consistent machine-readable identifiers",
|
|
348
|
+
description:
|
|
349
|
+
"Components are uniquely identified in a consistent, machine-readable format.",
|
|
350
|
+
standard: "SCVS",
|
|
351
|
+
standardRefs: ["SCVS-1.7"],
|
|
352
|
+
category: "compliance-scvs",
|
|
353
|
+
severity: "high",
|
|
354
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
355
|
+
automatable: true,
|
|
356
|
+
evaluate(bomJson) {
|
|
357
|
+
const comps = inventoryComponents(bomJson);
|
|
358
|
+
const missing = comps.filter((c) => !c.purl && !(c.cpe || c.swid?.tagId));
|
|
359
|
+
if (missing.length) {
|
|
360
|
+
return fail(
|
|
361
|
+
`${missing.length} component(s) lack a purl, cpe, or swid identifier.`,
|
|
362
|
+
{
|
|
363
|
+
mitigation:
|
|
364
|
+
"Ensure component identifiers are added during generation (cdxgen emits purls automatically).",
|
|
365
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
366
|
+
bomRef: c["bom-ref"],
|
|
367
|
+
name: c.name,
|
|
368
|
+
})),
|
|
369
|
+
evidence: { missingIdentifierCount: missing.length },
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return pass(
|
|
374
|
+
`All ${comps.length} component(s) have a machine-readable identifier.`,
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: "SCVS-1.8",
|
|
380
|
+
name: "Component type is known",
|
|
381
|
+
description: "The component type is known throughout inventory.",
|
|
382
|
+
standard: "SCVS",
|
|
383
|
+
standardRefs: ["SCVS-1.8"],
|
|
384
|
+
category: "compliance-scvs",
|
|
385
|
+
severity: "medium",
|
|
386
|
+
scvsLevels: ["L3"],
|
|
387
|
+
automatable: true,
|
|
388
|
+
evaluate(bomJson) {
|
|
389
|
+
const comps = getAllComponents(bomJson);
|
|
390
|
+
const missing = comps.filter((c) => !c?.type);
|
|
391
|
+
if (missing.length) {
|
|
392
|
+
return fail(`${missing.length} component(s) are missing type.`, {
|
|
393
|
+
mitigation: "Set 'type' on each component (library, framework, …).",
|
|
394
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
395
|
+
bomRef: c["bom-ref"],
|
|
396
|
+
name: c?.name,
|
|
397
|
+
})),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return pass(`All ${comps.length} component(s) have a type.`);
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
scvsManual(
|
|
404
|
+
"1.9",
|
|
405
|
+
"Component function is known",
|
|
406
|
+
"The component function is known throughout inventory.",
|
|
407
|
+
{ l1: false, l2: false, l3: true },
|
|
408
|
+
),
|
|
409
|
+
{
|
|
410
|
+
id: "SCVS-1.10",
|
|
411
|
+
name: "Point of origin is known",
|
|
412
|
+
description: "Point of origin is known for all components.",
|
|
413
|
+
standard: "SCVS",
|
|
414
|
+
standardRefs: ["SCVS-1.10"],
|
|
415
|
+
category: "compliance-scvs",
|
|
416
|
+
severity: "medium",
|
|
417
|
+
scvsLevels: ["L3"],
|
|
418
|
+
automatable: true,
|
|
419
|
+
evaluate(bomJson) {
|
|
420
|
+
const comps = inventoryComponents(bomJson);
|
|
421
|
+
const missing = comps.filter(
|
|
422
|
+
(c) => !c.purl && !c.supplier?.name && !c.publisher,
|
|
423
|
+
);
|
|
424
|
+
if (missing.length) {
|
|
425
|
+
return fail(
|
|
426
|
+
`${missing.length} component(s) lack a point of origin (purl, supplier, or publisher).`,
|
|
427
|
+
{
|
|
428
|
+
mitigation:
|
|
429
|
+
"Populate purl, supplier, or publisher for every component.",
|
|
430
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
431
|
+
bomRef: c["bom-ref"],
|
|
432
|
+
name: c?.name,
|
|
433
|
+
})),
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return pass(
|
|
438
|
+
`All ${comps.length} component(s) have a point of origin reference.`,
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: "SCVS-2.1",
|
|
444
|
+
name: "Structured machine-readable SBOM",
|
|
445
|
+
description:
|
|
446
|
+
"A structured, machine readable software bill of materials (SBOM) format is present.",
|
|
447
|
+
standard: "SCVS",
|
|
448
|
+
standardRefs: ["SCVS-2.1"],
|
|
449
|
+
category: "compliance-scvs",
|
|
450
|
+
severity: "high",
|
|
451
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
452
|
+
automatable: true,
|
|
453
|
+
evaluate(bomJson) {
|
|
454
|
+
if (bomJson?.bomFormat === "CycloneDX" && bomJson?.specVersion) {
|
|
455
|
+
return pass(`SBOM format is CycloneDX ${bomJson.specVersion}.`);
|
|
456
|
+
}
|
|
457
|
+
return fail("bomFormat or specVersion missing from the SBOM root.", {
|
|
458
|
+
mitigation: "Use cdxgen or another CycloneDX-compliant generator.",
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
scvsManual(
|
|
463
|
+
"2.2",
|
|
464
|
+
"SBOM creation is automated and reproducible",
|
|
465
|
+
"SBOM creation is automated and reproducible.",
|
|
466
|
+
{ l1: false, l2: true, l3: true },
|
|
467
|
+
),
|
|
468
|
+
{
|
|
469
|
+
id: "SCVS-2.3",
|
|
470
|
+
name: "SBOM has unique identifier",
|
|
471
|
+
description: "Each SBOM has a unique identifier.",
|
|
472
|
+
standard: "SCVS",
|
|
473
|
+
standardRefs: ["SCVS-2.3"],
|
|
474
|
+
category: "compliance-scvs",
|
|
475
|
+
severity: "high",
|
|
476
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
477
|
+
automatable: true,
|
|
478
|
+
evaluate(bomJson) {
|
|
479
|
+
if (
|
|
480
|
+
bomJson?.serialNumber &&
|
|
481
|
+
/^urn:uuid:[0-9a-f-]{36}$/i.test(bomJson.serialNumber)
|
|
482
|
+
) {
|
|
483
|
+
return pass(`Unique serialNumber present (${bomJson.serialNumber}).`);
|
|
484
|
+
}
|
|
485
|
+
return fail("BOM serialNumber is missing or not a urn:uuid value.", {
|
|
486
|
+
mitigation:
|
|
487
|
+
"Ensure the SBOM includes a serialNumber of the form urn:uuid:<uuid>.",
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: "SCVS-2.4",
|
|
493
|
+
name: "SBOM is signed",
|
|
494
|
+
description:
|
|
495
|
+
"SBOM has been signed by publisher, supplier, or certifying authority.",
|
|
496
|
+
standard: "SCVS",
|
|
497
|
+
standardRefs: ["SCVS-2.4"],
|
|
498
|
+
category: "compliance-scvs",
|
|
499
|
+
severity: "high",
|
|
500
|
+
scvsLevels: ["L2", "L3"],
|
|
501
|
+
automatable: true,
|
|
502
|
+
evaluate(bomJson) {
|
|
503
|
+
if (bomJson?.signature) {
|
|
504
|
+
const algo =
|
|
505
|
+
bomJson.signature.algorithm ||
|
|
506
|
+
bomJson.signature.signers?.[0]?.algorithm ||
|
|
507
|
+
bomJson.signature.chain?.[0]?.algorithm;
|
|
508
|
+
return pass(`BOM is signed${algo ? ` (${algo})` : ""}.`);
|
|
509
|
+
}
|
|
510
|
+
return fail("BOM is not signed.", {
|
|
511
|
+
mitigation:
|
|
512
|
+
"Sign the SBOM with `cdx-sign -i bom.json -k private.pem` before distribution.",
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
scvsManual(
|
|
517
|
+
"2.5",
|
|
518
|
+
"SBOM signature verification exists",
|
|
519
|
+
"SBOM signature verification exists.",
|
|
520
|
+
{ l1: false, l2: true, l3: true },
|
|
521
|
+
),
|
|
522
|
+
scvsManual(
|
|
523
|
+
"2.6",
|
|
524
|
+
"SBOM signature verification is performed",
|
|
525
|
+
"SBOM signature verification is performed.",
|
|
526
|
+
{ l1: false, l2: false, l3: true },
|
|
527
|
+
),
|
|
528
|
+
{
|
|
529
|
+
id: "SCVS-2.7",
|
|
530
|
+
name: "SBOM is timestamped",
|
|
531
|
+
description: "SBOM is timestamped.",
|
|
532
|
+
standard: "SCVS",
|
|
533
|
+
standardRefs: ["SCVS-2.7"],
|
|
534
|
+
category: "compliance-scvs",
|
|
535
|
+
severity: "high",
|
|
536
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
537
|
+
automatable: true,
|
|
538
|
+
evaluate(bomJson) {
|
|
539
|
+
const ts = bomJson?.metadata?.timestamp;
|
|
540
|
+
if (typeof ts !== "string" || ts.length === 0) {
|
|
541
|
+
return fail("metadata.timestamp is missing.");
|
|
542
|
+
}
|
|
543
|
+
if (Number.isNaN(Date.parse(ts))) {
|
|
544
|
+
return fail(`metadata.timestamp is not a valid ISO-8601 date: ${ts}`);
|
|
545
|
+
}
|
|
546
|
+
return pass(`metadata.timestamp present (${ts}).`);
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
scvsManual("2.8", "SBOM is analyzed for risk", "SBOM is analyzed for risk.", {
|
|
550
|
+
l1: true,
|
|
551
|
+
l2: true,
|
|
552
|
+
l3: true,
|
|
553
|
+
}),
|
|
554
|
+
{
|
|
555
|
+
id: "SCVS-2.9",
|
|
556
|
+
name: "Complete and accurate inventory",
|
|
557
|
+
description:
|
|
558
|
+
"SBOM contains a complete and accurate inventory of all components the SBOM describes.",
|
|
559
|
+
standard: "SCVS",
|
|
560
|
+
standardRefs: ["SCVS-2.9"],
|
|
561
|
+
category: "compliance-scvs",
|
|
562
|
+
severity: "high",
|
|
563
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
564
|
+
automatable: true,
|
|
565
|
+
evaluate(bomJson) {
|
|
566
|
+
const comps = inventoryComponents(bomJson);
|
|
567
|
+
if (comps.length === 0) {
|
|
568
|
+
return fail("BOM has no inventory components.");
|
|
569
|
+
}
|
|
570
|
+
if (
|
|
571
|
+
!Array.isArray(bomJson?.dependencies) ||
|
|
572
|
+
bomJson.dependencies.length === 0
|
|
573
|
+
) {
|
|
574
|
+
return fail(
|
|
575
|
+
"BOM has components but no dependency graph — inventory is not demonstrably complete.",
|
|
576
|
+
{
|
|
577
|
+
mitigation:
|
|
578
|
+
"Ensure cdxgen is run with access to the full dependency tree so that the 'dependencies' section is populated.",
|
|
579
|
+
},
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return pass(
|
|
583
|
+
`${comps.length} component(s) with a ${bomJson.dependencies.length}-node dependency graph.`,
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
scvsManual(
|
|
588
|
+
"2.10",
|
|
589
|
+
"SBOM contains accurate test inventory",
|
|
590
|
+
"SBOM contains an accurate inventory of all test components for the asset or application it describes.",
|
|
591
|
+
{ l1: false, l2: true, l3: true },
|
|
592
|
+
),
|
|
593
|
+
{
|
|
594
|
+
id: "SCVS-2.11",
|
|
595
|
+
name: "SBOM contains asset metadata",
|
|
596
|
+
description:
|
|
597
|
+
"SBOM contains metadata about the asset or software the SBOM describes.",
|
|
598
|
+
standard: "SCVS",
|
|
599
|
+
standardRefs: ["SCVS-2.11"],
|
|
600
|
+
category: "compliance-scvs",
|
|
601
|
+
severity: "high",
|
|
602
|
+
scvsLevels: ["L2", "L3"],
|
|
603
|
+
automatable: true,
|
|
604
|
+
evaluate(bomJson) {
|
|
605
|
+
const meta = bomJson?.metadata?.component;
|
|
606
|
+
if (!meta?.name) {
|
|
607
|
+
return fail("metadata.component is missing or has no name.", {
|
|
608
|
+
mitigation:
|
|
609
|
+
"Pass --project-name (and --project-version) when running cdxgen.",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (!meta.version) {
|
|
613
|
+
return fail("metadata.component.version is missing.", {
|
|
614
|
+
mitigation: "Pass --project-version when running cdxgen.",
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return pass(
|
|
618
|
+
`Root asset metadata present (${meta.name}@${meta.version}).`,
|
|
619
|
+
);
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
id: "SCVS-2.12",
|
|
624
|
+
name: "Identifiers derived from native ecosystems",
|
|
625
|
+
description:
|
|
626
|
+
"Component identifiers are derived from their native ecosystems (if applicable).",
|
|
627
|
+
standard: "SCVS",
|
|
628
|
+
standardRefs: ["SCVS-2.12"],
|
|
629
|
+
category: "compliance-scvs",
|
|
630
|
+
severity: "high",
|
|
631
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
632
|
+
automatable: true,
|
|
633
|
+
evaluate(bomJson) {
|
|
634
|
+
const comps = inventoryComponents(bomJson);
|
|
635
|
+
const invalid = [];
|
|
636
|
+
for (const c of comps) {
|
|
637
|
+
if (!c.purl) continue;
|
|
638
|
+
try {
|
|
639
|
+
PackageURL.fromString(c.purl);
|
|
640
|
+
} catch (_err) {
|
|
641
|
+
invalid.push(c);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (invalid.length) {
|
|
645
|
+
return fail(`${invalid.length} component(s) have an invalid purl.`, {
|
|
646
|
+
mitigation:
|
|
647
|
+
"Use PackageURL.fromString to validate purls; regenerate with the latest cdxgen.",
|
|
648
|
+
locations: invalid.slice(0, 25).map((c) => ({
|
|
649
|
+
bomRef: c["bom-ref"],
|
|
650
|
+
purl: c.purl,
|
|
651
|
+
})),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return pass(
|
|
655
|
+
`All ${comps.filter((c) => c.purl).length} component purls are parseable.`,
|
|
656
|
+
);
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: "SCVS-2.13",
|
|
661
|
+
name: "Point of origin identified with PURL",
|
|
662
|
+
description:
|
|
663
|
+
"Component point of origin is identified in a consistent, machine readable format (e.g. PURL).",
|
|
664
|
+
standard: "SCVS",
|
|
665
|
+
standardRefs: ["SCVS-2.13"],
|
|
666
|
+
category: "compliance-scvs",
|
|
667
|
+
severity: "medium",
|
|
668
|
+
scvsLevels: ["L3"],
|
|
669
|
+
automatable: true,
|
|
670
|
+
evaluate(bomJson) {
|
|
671
|
+
const comps = inventoryComponents(bomJson);
|
|
672
|
+
const missing = comps.filter((c) => !c.purl);
|
|
673
|
+
if (missing.length) {
|
|
674
|
+
return fail(`${missing.length} component(s) are missing a purl.`, {
|
|
675
|
+
mitigation:
|
|
676
|
+
"Purls are the preferred SBOM identifier — regenerate with cdxgen.",
|
|
677
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
678
|
+
bomRef: c["bom-ref"],
|
|
679
|
+
name: c?.name,
|
|
680
|
+
})),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return pass(`All ${comps.length} inventory component(s) have a purl.`);
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: "SCVS-2.14",
|
|
688
|
+
name: "Components have license information",
|
|
689
|
+
description:
|
|
690
|
+
"Components defined in SBOM have accurate license information.",
|
|
691
|
+
standard: "SCVS",
|
|
692
|
+
standardRefs: ["SCVS-2.14"],
|
|
693
|
+
category: "compliance-scvs",
|
|
694
|
+
severity: "high",
|
|
695
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
696
|
+
automatable: true,
|
|
697
|
+
evaluate(bomJson) {
|
|
698
|
+
const comps = inventoryComponents(bomJson);
|
|
699
|
+
const missing = comps.filter((c) => !c.licenses?.length);
|
|
700
|
+
if (missing.length) {
|
|
701
|
+
return fail(
|
|
702
|
+
`${missing.length} component(s) are missing license information.`,
|
|
703
|
+
{
|
|
704
|
+
mitigation:
|
|
705
|
+
"Run cdxgen with FETCH_LICENSE=true, or provide a license policy.",
|
|
706
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
707
|
+
bomRef: c["bom-ref"],
|
|
708
|
+
purl: c.purl,
|
|
709
|
+
name: c?.name,
|
|
710
|
+
})),
|
|
711
|
+
evidence: { missingLicenseCount: missing.length },
|
|
712
|
+
},
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return pass(
|
|
716
|
+
`All ${comps.length} inventory component(s) declare license information.`,
|
|
717
|
+
);
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: "SCVS-2.15",
|
|
722
|
+
name: "Valid SPDX identifiers or expressions",
|
|
723
|
+
description:
|
|
724
|
+
"Components defined in SBOM have valid SPDX license IDs or expressions (if applicable).",
|
|
725
|
+
standard: "SCVS",
|
|
726
|
+
standardRefs: ["SCVS-2.15"],
|
|
727
|
+
category: "compliance-scvs",
|
|
728
|
+
severity: "medium",
|
|
729
|
+
scvsLevels: ["L2", "L3"],
|
|
730
|
+
automatable: true,
|
|
731
|
+
evaluate(bomJson) {
|
|
732
|
+
const comps = inventoryComponents(bomJson);
|
|
733
|
+
const invalid = [];
|
|
734
|
+
const evidence = new Set();
|
|
735
|
+
for (const c of comps) {
|
|
736
|
+
const lic = componentLicenseId(c);
|
|
737
|
+
if (lic && !looksLikeSpdx(lic)) {
|
|
738
|
+
invalid.push({ comp: c, lic });
|
|
739
|
+
evidence.add(lic);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (invalid.length) {
|
|
743
|
+
return fail(
|
|
744
|
+
`${invalid.length} component(s) use a non-SPDX license expression.`,
|
|
745
|
+
{
|
|
746
|
+
mitigation:
|
|
747
|
+
"Normalize license identifiers to SPDX license IDs or expressions.",
|
|
748
|
+
locations: invalid.slice(0, 25).map(({ comp, _ }) => ({
|
|
749
|
+
bomRef: comp["bom-ref"],
|
|
750
|
+
purl: comp.purl,
|
|
751
|
+
})),
|
|
752
|
+
evidence: Array.from(evidence),
|
|
753
|
+
},
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return pass(
|
|
757
|
+
"SPDX license identifiers are valid for all components with license data.",
|
|
758
|
+
);
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
id: "SCVS-2.16",
|
|
763
|
+
name: "Components have copyright statements",
|
|
764
|
+
description: "Components defined in SBOM have valid copyright statements.",
|
|
765
|
+
standard: "SCVS",
|
|
766
|
+
standardRefs: ["SCVS-2.16"],
|
|
767
|
+
category: "compliance-scvs",
|
|
768
|
+
severity: "low",
|
|
769
|
+
scvsLevels: ["L3"],
|
|
770
|
+
automatable: true,
|
|
771
|
+
evaluate(bomJson) {
|
|
772
|
+
const comps = inventoryComponents(bomJson);
|
|
773
|
+
const missing = comps.filter((c) => !c.copyright);
|
|
774
|
+
if (missing.length) {
|
|
775
|
+
return fail(
|
|
776
|
+
`${missing.length} component(s) are missing copyright statements.`,
|
|
777
|
+
{
|
|
778
|
+
mitigation:
|
|
779
|
+
"Populate copyright metadata for each component (cdxgen does this when license data is available).",
|
|
780
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
781
|
+
bomRef: c["bom-ref"],
|
|
782
|
+
purl: c.purl,
|
|
783
|
+
})),
|
|
784
|
+
},
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return pass(`All ${comps.length} component(s) declare a copyright.`);
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
scvsManual(
|
|
791
|
+
"2.17",
|
|
792
|
+
"Modified components have pedigree information",
|
|
793
|
+
"Components defined in SBOM which have been modified from the original have detailed provenance and pedigree information.",
|
|
794
|
+
{ l1: false, l2: false, l3: true },
|
|
795
|
+
),
|
|
796
|
+
{
|
|
797
|
+
id: "SCVS-2.18",
|
|
798
|
+
name: "Components have file hashes",
|
|
799
|
+
description:
|
|
800
|
+
"Components defined in SBOM have one or more file hashes (SHA-256, SHA-512, etc).",
|
|
801
|
+
standard: "SCVS",
|
|
802
|
+
standardRefs: ["SCVS-2.18"],
|
|
803
|
+
category: "compliance-scvs",
|
|
804
|
+
severity: "medium",
|
|
805
|
+
scvsLevels: ["L3"],
|
|
806
|
+
automatable: true,
|
|
807
|
+
evaluate(bomJson) {
|
|
808
|
+
const comps = inventoryComponents(bomJson);
|
|
809
|
+
const missing = comps.filter((c) => !c.hashes?.length);
|
|
810
|
+
if (missing.length) {
|
|
811
|
+
return fail(`${missing.length} component(s) are missing file hashes.`, {
|
|
812
|
+
mitigation:
|
|
813
|
+
"Run cdxgen with FETCH_LICENSE/lockfile context so tarball hashes are captured.",
|
|
814
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
815
|
+
bomRef: c["bom-ref"],
|
|
816
|
+
purl: c.purl,
|
|
817
|
+
})),
|
|
818
|
+
evidence: { missingHashesCount: missing.length },
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return pass(`All ${comps.length} component(s) have one or more hashes.`);
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
scvsManual(
|
|
825
|
+
"3.1",
|
|
826
|
+
"Application uses a repeatable build",
|
|
827
|
+
"Application uses a repeatable build.",
|
|
828
|
+
{ l1: true, l2: true, l3: true },
|
|
829
|
+
),
|
|
830
|
+
scvsManual(
|
|
831
|
+
"3.2",
|
|
832
|
+
"Build documentation exists",
|
|
833
|
+
"Documentation exists on how the application is built and instructions for repeating the build.",
|
|
834
|
+
{ l1: true, l2: true, l3: true },
|
|
835
|
+
),
|
|
836
|
+
scvsManual(
|
|
837
|
+
"3.3",
|
|
838
|
+
"Application uses CI build pipeline",
|
|
839
|
+
"Application uses a continuous integration build pipeline.",
|
|
840
|
+
{ l1: true, l2: true, l3: true },
|
|
841
|
+
),
|
|
842
|
+
scvsManual(
|
|
843
|
+
"3.4",
|
|
844
|
+
"Build outputs immutable",
|
|
845
|
+
"Application build pipeline prohibits alteration of build outside of the job performing the build.",
|
|
846
|
+
{ l1: false, l2: true, l3: true },
|
|
847
|
+
),
|
|
848
|
+
scvsManual(
|
|
849
|
+
"3.5",
|
|
850
|
+
"Package-manager settings immutable",
|
|
851
|
+
"Application build pipeline prohibits alteration of package management settings.",
|
|
852
|
+
{ l1: false, l2: true, l3: true },
|
|
853
|
+
),
|
|
854
|
+
scvsManual(
|
|
855
|
+
"3.6",
|
|
856
|
+
"No arbitrary code execution",
|
|
857
|
+
"Application build pipeline prohibits the execution of arbitrary code outside of the context of a jobs build script.",
|
|
858
|
+
{ l1: false, l2: true, l3: true },
|
|
859
|
+
),
|
|
860
|
+
scvsManual(
|
|
861
|
+
"3.7",
|
|
862
|
+
"Builds only from version control",
|
|
863
|
+
"Application build pipeline may only perform builds of source code maintained in version control systems.",
|
|
864
|
+
{ l1: true, l2: true, l3: true },
|
|
865
|
+
),
|
|
866
|
+
scvsManual(
|
|
867
|
+
"3.8",
|
|
868
|
+
"DNS/network settings immutable",
|
|
869
|
+
"Application build pipeline prohibits alteration of DNS and network settings during build.",
|
|
870
|
+
{ l1: false, l2: false, l3: true },
|
|
871
|
+
),
|
|
872
|
+
scvsManual(
|
|
873
|
+
"3.9",
|
|
874
|
+
"Certificate trust stores immutable",
|
|
875
|
+
"Application build pipeline prohibits alteration of certificate trust stores.",
|
|
876
|
+
{ l1: false, l2: false, l3: true },
|
|
877
|
+
),
|
|
878
|
+
scvsManual(
|
|
879
|
+
"3.10",
|
|
880
|
+
"Pipeline authentication enforced",
|
|
881
|
+
"Application build pipeline enforces authentication and defaults to deny.",
|
|
882
|
+
{ l1: false, l2: true, l3: true },
|
|
883
|
+
),
|
|
884
|
+
scvsManual(
|
|
885
|
+
"3.11",
|
|
886
|
+
"Pipeline authorization enforced",
|
|
887
|
+
"Application build pipeline enforces authorization and defaults to deny.",
|
|
888
|
+
{ l1: false, l2: true, l3: true },
|
|
889
|
+
),
|
|
890
|
+
scvsManual(
|
|
891
|
+
"3.12",
|
|
892
|
+
"Separation of concerns for system settings",
|
|
893
|
+
"Application build pipeline requires separation of concerns for the modification of system settings.",
|
|
894
|
+
{ l1: false, l2: false, l3: true },
|
|
895
|
+
),
|
|
896
|
+
scvsManual(
|
|
897
|
+
"3.13",
|
|
898
|
+
"Verifiable audit log of system changes",
|
|
899
|
+
"Application build pipeline maintains a verifiable audit log of all system changes.",
|
|
900
|
+
{ l1: false, l2: false, l3: true },
|
|
901
|
+
),
|
|
902
|
+
scvsManual(
|
|
903
|
+
"3.14",
|
|
904
|
+
"Verifiable audit log of build changes",
|
|
905
|
+
"Application build pipeline maintains a verifiable audit log of all build job changes.",
|
|
906
|
+
{ l1: false, l2: false, l3: true },
|
|
907
|
+
),
|
|
908
|
+
scvsManual(
|
|
909
|
+
"3.15",
|
|
910
|
+
"Build pipeline maintenance cadence",
|
|
911
|
+
"Application build pipeline has required maintenance cadence where the entire stack is updated, patched, and re-certified for use.",
|
|
912
|
+
{ l1: false, l2: true, l3: true },
|
|
913
|
+
),
|
|
914
|
+
scvsManual(
|
|
915
|
+
"3.16",
|
|
916
|
+
"Compiler/tooling tamper monitoring",
|
|
917
|
+
"Compilers, version control clients, development utilities, and software development kits are analyzed and monitored for tampering, trojans, or malicious code.",
|
|
918
|
+
{ l1: false, l2: false, l3: true },
|
|
919
|
+
),
|
|
920
|
+
scvsManual(
|
|
921
|
+
"3.17",
|
|
922
|
+
"Build-time manipulations known",
|
|
923
|
+
"All build-time manipulations to source or binaries are known and well defined.",
|
|
924
|
+
{ l1: true, l2: true, l3: true },
|
|
925
|
+
),
|
|
926
|
+
{
|
|
927
|
+
id: "SCVS-3.18",
|
|
928
|
+
name: "Checksums of components documented",
|
|
929
|
+
description:
|
|
930
|
+
"Checksums of all first-party and third-party components are documented for every build.",
|
|
931
|
+
standard: "SCVS",
|
|
932
|
+
standardRefs: ["SCVS-3.18"],
|
|
933
|
+
category: "compliance-scvs",
|
|
934
|
+
severity: "medium",
|
|
935
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
936
|
+
automatable: true,
|
|
937
|
+
evaluate(bomJson) {
|
|
938
|
+
const comps = inventoryComponents(bomJson);
|
|
939
|
+
const missing = comps.filter((c) => !c.hashes?.length);
|
|
940
|
+
if (missing.length) {
|
|
941
|
+
return fail(
|
|
942
|
+
`${missing.length} component(s) have no checksum recorded.`,
|
|
943
|
+
{
|
|
944
|
+
mitigation:
|
|
945
|
+
"Populate component 'hashes' during generation (cdxgen captures these when lockfile or tarball data is available).",
|
|
946
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
947
|
+
bomRef: c["bom-ref"],
|
|
948
|
+
purl: c.purl,
|
|
949
|
+
})),
|
|
950
|
+
},
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
return pass(`All ${comps.length} component(s) have recorded checksums.`);
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
scvsManual(
|
|
957
|
+
"3.19",
|
|
958
|
+
"Checksums delivered out-of-band",
|
|
959
|
+
"Checksums of all components are accessible and delivered out-of-band whenever those components are packaged or distributed.",
|
|
960
|
+
{ l1: false, l2: true, l3: true },
|
|
961
|
+
),
|
|
962
|
+
{
|
|
963
|
+
id: "SCVS-3.20",
|
|
964
|
+
name: "Unused components identified",
|
|
965
|
+
description:
|
|
966
|
+
"Unused direct and transitive components have been identified.",
|
|
967
|
+
standard: "SCVS",
|
|
968
|
+
standardRefs: ["SCVS-3.20"],
|
|
969
|
+
category: "compliance-scvs",
|
|
970
|
+
severity: "low",
|
|
971
|
+
scvsLevels: ["L3"],
|
|
972
|
+
automatable: true,
|
|
973
|
+
evaluate(bomJson) {
|
|
974
|
+
const comps = inventoryComponents(bomJson);
|
|
975
|
+
if (!comps.length) {
|
|
976
|
+
return fail("No components to analyse.");
|
|
977
|
+
}
|
|
978
|
+
const refs = collectReferencedRefs(bomJson);
|
|
979
|
+
const orphans = comps.filter(
|
|
980
|
+
(c) => c["bom-ref"] && !refs.has(c["bom-ref"]),
|
|
981
|
+
);
|
|
982
|
+
if (orphans.length) {
|
|
983
|
+
return fail(
|
|
984
|
+
`${orphans.length} component(s) are not referenced by the dependency graph.`,
|
|
985
|
+
{
|
|
986
|
+
mitigation:
|
|
987
|
+
"Remove unused components or ensure the dependency graph is complete.",
|
|
988
|
+
locations: orphans.slice(0, 25).map((c) => ({
|
|
989
|
+
bomRef: c["bom-ref"],
|
|
990
|
+
purl: c.purl,
|
|
991
|
+
})),
|
|
992
|
+
},
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return pass(
|
|
996
|
+
`All ${comps.length} inventory component(s) are reachable from the dependency graph.`,
|
|
997
|
+
);
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
scvsManual(
|
|
1001
|
+
"3.21",
|
|
1002
|
+
"Unused components removed",
|
|
1003
|
+
"Unused direct and transitive components have been removed from the application.",
|
|
1004
|
+
{ l1: false, l2: false, l3: true },
|
|
1005
|
+
),
|
|
1006
|
+
// V4 - Package Management (mostly process controls)
|
|
1007
|
+
scvsManual(
|
|
1008
|
+
"4.1",
|
|
1009
|
+
"Binary components from a package repository",
|
|
1010
|
+
"Binary components are retrieved from a package repository.",
|
|
1011
|
+
{ l1: true, l2: true, l3: true },
|
|
1012
|
+
),
|
|
1013
|
+
scvsManual(
|
|
1014
|
+
"4.2",
|
|
1015
|
+
"Package repository congruent with origin",
|
|
1016
|
+
"Package repository contents are congruent to an authoritative point of origin for open source components.",
|
|
1017
|
+
{ l1: true, l2: true, l3: true },
|
|
1018
|
+
),
|
|
1019
|
+
scvsManual(
|
|
1020
|
+
"4.3",
|
|
1021
|
+
"Package repository strong authentication",
|
|
1022
|
+
"Package repository requires strong authentication.",
|
|
1023
|
+
{ l1: false, l2: true, l3: true },
|
|
1024
|
+
),
|
|
1025
|
+
scvsManual(
|
|
1026
|
+
"4.4",
|
|
1027
|
+
"Package repository MFA for publishing",
|
|
1028
|
+
"Package repository supports multi-factor authentication component publishing.",
|
|
1029
|
+
{ l1: false, l2: true, l3: true },
|
|
1030
|
+
),
|
|
1031
|
+
scvsManual(
|
|
1032
|
+
"4.5",
|
|
1033
|
+
"Packages published with MFA",
|
|
1034
|
+
"Package repository components have been published with multi-factor authentication.",
|
|
1035
|
+
{ l1: false, l2: false, l3: true },
|
|
1036
|
+
),
|
|
1037
|
+
scvsManual(
|
|
1038
|
+
"4.6",
|
|
1039
|
+
"Security incident reporting supported",
|
|
1040
|
+
"Package repository supports security incident reporting.",
|
|
1041
|
+
{ l1: false, l2: true, l3: true },
|
|
1042
|
+
),
|
|
1043
|
+
scvsManual(
|
|
1044
|
+
"4.7",
|
|
1045
|
+
"Security incident reporting automated",
|
|
1046
|
+
"Package repository automates security incident reporting.",
|
|
1047
|
+
{ l1: false, l2: false, l3: true },
|
|
1048
|
+
),
|
|
1049
|
+
scvsManual(
|
|
1050
|
+
"4.8",
|
|
1051
|
+
"Publisher security notifications",
|
|
1052
|
+
"Package repository notifies publishers of security issues.",
|
|
1053
|
+
{ l1: false, l2: true, l3: true },
|
|
1054
|
+
),
|
|
1055
|
+
scvsManual(
|
|
1056
|
+
"4.9",
|
|
1057
|
+
"User security notifications",
|
|
1058
|
+
"Package repository notifies users of security issues.",
|
|
1059
|
+
{ l1: false, l2: false, l3: true },
|
|
1060
|
+
),
|
|
1061
|
+
scvsManual(
|
|
1062
|
+
"4.10",
|
|
1063
|
+
"Version-to-source correlation",
|
|
1064
|
+
"Package repository provides a verifiable way of correlating component versions to specific source codes in version control.",
|
|
1065
|
+
{ l1: false, l2: true, l3: true },
|
|
1066
|
+
),
|
|
1067
|
+
scvsManual(
|
|
1068
|
+
"4.11",
|
|
1069
|
+
"Package repository auditability",
|
|
1070
|
+
"Package repository provides auditability when components are updated.",
|
|
1071
|
+
{ l1: true, l2: true, l3: true },
|
|
1072
|
+
),
|
|
1073
|
+
scvsManual(
|
|
1074
|
+
"4.12",
|
|
1075
|
+
"Code signing for production publishing",
|
|
1076
|
+
"Package repository requires code signing to publish packages to production repositories.",
|
|
1077
|
+
{ l1: false, l2: true, l3: true },
|
|
1078
|
+
),
|
|
1079
|
+
scvsManual(
|
|
1080
|
+
"4.13",
|
|
1081
|
+
"Package manager verifies remote integrity",
|
|
1082
|
+
"Package manager verifies the integrity of packages when they are retrieved from remote repository.",
|
|
1083
|
+
{ l1: true, l2: true, l3: true },
|
|
1084
|
+
),
|
|
1085
|
+
scvsManual(
|
|
1086
|
+
"4.14",
|
|
1087
|
+
"Package manager verifies local integrity",
|
|
1088
|
+
"Package manager verifies the integrity of packages when they are retrieved from file system.",
|
|
1089
|
+
{ l1: true, l2: true, l3: true },
|
|
1090
|
+
),
|
|
1091
|
+
scvsManual(
|
|
1092
|
+
"4.15",
|
|
1093
|
+
"TLS required for package repository",
|
|
1094
|
+
"Package repository enforces use of TLS for all interactions.",
|
|
1095
|
+
{ l1: true, l2: true, l3: true },
|
|
1096
|
+
),
|
|
1097
|
+
scvsManual(
|
|
1098
|
+
"4.16",
|
|
1099
|
+
"Package manager validates TLS chain",
|
|
1100
|
+
"Package manager validates TLS certificate chain to repository and fails securely when validation fails.",
|
|
1101
|
+
{ l1: true, l2: true, l3: true },
|
|
1102
|
+
),
|
|
1103
|
+
scvsManual(
|
|
1104
|
+
"4.17",
|
|
1105
|
+
"Static analysis prior to publishing",
|
|
1106
|
+
"Package repository requires and/or performs static code analysis prior to publishing a component and makes results available for others to consume.",
|
|
1107
|
+
{ l1: false, l2: false, l3: true },
|
|
1108
|
+
),
|
|
1109
|
+
scvsManual(
|
|
1110
|
+
"4.18",
|
|
1111
|
+
"Package manager does not execute code",
|
|
1112
|
+
"Package manager does not execute component code.",
|
|
1113
|
+
{ l1: true, l2: true, l3: true },
|
|
1114
|
+
),
|
|
1115
|
+
scvsManual(
|
|
1116
|
+
"4.19",
|
|
1117
|
+
"Install documented in machine-readable form",
|
|
1118
|
+
"Package manager documents package installation in machine-readable form.",
|
|
1119
|
+
{ l1: true, l2: true, l3: true },
|
|
1120
|
+
),
|
|
1121
|
+
// V5 - Component Analysis
|
|
1122
|
+
scvsManual(
|
|
1123
|
+
"5.1",
|
|
1124
|
+
"Component analyzable by linters/SAST",
|
|
1125
|
+
"Component can be analyzed with linters and/or static analysis tools.",
|
|
1126
|
+
{ l1: true, l2: true, l3: true },
|
|
1127
|
+
),
|
|
1128
|
+
scvsManual(
|
|
1129
|
+
"5.2",
|
|
1130
|
+
"Components analyzed prior to use",
|
|
1131
|
+
"Component is analyzed using linters and/or static analysis tools prior to use.",
|
|
1132
|
+
{ l1: false, l2: true, l3: true },
|
|
1133
|
+
),
|
|
1134
|
+
scvsManual(
|
|
1135
|
+
"5.3",
|
|
1136
|
+
"Analysis repeated on upgrade",
|
|
1137
|
+
"Linting and/or static analysis is performed with every upgrade of a component.",
|
|
1138
|
+
{ l1: false, l2: true, l3: true },
|
|
1139
|
+
),
|
|
1140
|
+
scvsManual(
|
|
1141
|
+
"5.4",
|
|
1142
|
+
"Automated vulnerability identification",
|
|
1143
|
+
"An automated process of identifying all publicly disclosed vulnerabilities in third-party and open source components is used.",
|
|
1144
|
+
{ l1: true, l2: true, l3: true },
|
|
1145
|
+
),
|
|
1146
|
+
scvsManual(
|
|
1147
|
+
"5.5",
|
|
1148
|
+
"Automated dataflow exploitability",
|
|
1149
|
+
"An automated process of identifying confirmed dataflow exploitability is used.",
|
|
1150
|
+
{ l1: false, l2: false, l3: true },
|
|
1151
|
+
),
|
|
1152
|
+
scvsManual(
|
|
1153
|
+
"5.6",
|
|
1154
|
+
"Non-specified component versions identified",
|
|
1155
|
+
"An automated process of identifying non-specified component versions is used.",
|
|
1156
|
+
{ l1: true, l2: true, l3: true },
|
|
1157
|
+
),
|
|
1158
|
+
scvsManual(
|
|
1159
|
+
"5.7",
|
|
1160
|
+
"Out-of-date components identified",
|
|
1161
|
+
"An automated process of identifying out-of-date components is used.",
|
|
1162
|
+
{ l1: true, l2: true, l3: true },
|
|
1163
|
+
),
|
|
1164
|
+
scvsManual(
|
|
1165
|
+
"5.8",
|
|
1166
|
+
"End-of-life components identified",
|
|
1167
|
+
"An automated process of identifying end-of-life / end-of-support components is used.",
|
|
1168
|
+
{ l1: false, l2: false, l3: true },
|
|
1169
|
+
),
|
|
1170
|
+
scvsManual(
|
|
1171
|
+
"5.9",
|
|
1172
|
+
"Automated component type identification",
|
|
1173
|
+
"An automated process of identifying component type is used.",
|
|
1174
|
+
{ l1: false, l2: true, l3: true },
|
|
1175
|
+
),
|
|
1176
|
+
scvsManual(
|
|
1177
|
+
"5.10",
|
|
1178
|
+
"Automated component function identification",
|
|
1179
|
+
"An automated process of identifying component function is used.",
|
|
1180
|
+
{ l1: false, l2: false, l3: true },
|
|
1181
|
+
),
|
|
1182
|
+
{
|
|
1183
|
+
id: "SCVS-5.11",
|
|
1184
|
+
name: "Automated component quantity identification",
|
|
1185
|
+
description:
|
|
1186
|
+
"An automated process of identifying component quantity is used.",
|
|
1187
|
+
standard: "SCVS",
|
|
1188
|
+
standardRefs: ["SCVS-5.11"],
|
|
1189
|
+
category: "compliance-scvs",
|
|
1190
|
+
severity: "low",
|
|
1191
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
1192
|
+
automatable: true,
|
|
1193
|
+
evaluate(bomJson) {
|
|
1194
|
+
const comps = inventoryComponents(bomJson);
|
|
1195
|
+
if (comps.length === 0) {
|
|
1196
|
+
return fail("BOM has no inventory components to quantify.");
|
|
1197
|
+
}
|
|
1198
|
+
return pass(`BOM declares ${comps.length} inventory component(s).`);
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
id: "SCVS-5.12",
|
|
1203
|
+
name: "Automated component license identification",
|
|
1204
|
+
description:
|
|
1205
|
+
"An automated process of identifying component license is used.",
|
|
1206
|
+
standard: "SCVS",
|
|
1207
|
+
standardRefs: ["SCVS-5.12"],
|
|
1208
|
+
category: "compliance-scvs",
|
|
1209
|
+
severity: "medium",
|
|
1210
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
1211
|
+
automatable: true,
|
|
1212
|
+
evaluate(bomJson) {
|
|
1213
|
+
const comps = inventoryComponents(bomJson);
|
|
1214
|
+
if (comps.length === 0) {
|
|
1215
|
+
return fail("BOM has no components to analyse for licenses.");
|
|
1216
|
+
}
|
|
1217
|
+
const withLic = comps.filter((c) => c.licenses?.length);
|
|
1218
|
+
const ratio = withLic.length / comps.length;
|
|
1219
|
+
if (ratio < 0.5) {
|
|
1220
|
+
return fail(
|
|
1221
|
+
`Only ${withLic.length}/${comps.length} (${Math.round(ratio * 100)}%) components have license data.`,
|
|
1222
|
+
{
|
|
1223
|
+
mitigation: "Run cdxgen with FETCH_LICENSE=true.",
|
|
1224
|
+
},
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
return pass(
|
|
1228
|
+
`${withLic.length}/${comps.length} (${Math.round(ratio * 100)}%) components have license data.`,
|
|
1229
|
+
);
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
// V6 - Pedigree and Provenance
|
|
1233
|
+
scvsManual(
|
|
1234
|
+
"6.1",
|
|
1235
|
+
"Point of origin verifiable",
|
|
1236
|
+
"Point of origin is verifiable for source code and binary components.",
|
|
1237
|
+
{ l1: false, l2: true, l3: true },
|
|
1238
|
+
),
|
|
1239
|
+
scvsManual(
|
|
1240
|
+
"6.2",
|
|
1241
|
+
"Chain of custody auditable",
|
|
1242
|
+
"Chain of custody if auditable for source code and binary components.",
|
|
1243
|
+
{ l1: false, l2: false, l3: true },
|
|
1244
|
+
),
|
|
1245
|
+
{
|
|
1246
|
+
id: "SCVS-6.3",
|
|
1247
|
+
name: "Provenance of modified components",
|
|
1248
|
+
description: "Provenance of modified components is known and documented.",
|
|
1249
|
+
standard: "SCVS",
|
|
1250
|
+
standardRefs: ["SCVS-6.3"],
|
|
1251
|
+
category: "compliance-scvs",
|
|
1252
|
+
severity: "low",
|
|
1253
|
+
scvsLevels: ["L1", "L2", "L3"],
|
|
1254
|
+
automatable: true,
|
|
1255
|
+
evaluate(bomJson) {
|
|
1256
|
+
const comps = inventoryComponents(bomJson);
|
|
1257
|
+
const modified = comps.filter((c) => c.pedigree);
|
|
1258
|
+
if (modified.length === 0) {
|
|
1259
|
+
// No modified components is a pass — nothing to document.
|
|
1260
|
+
return pass("No modified components declared — nothing to document.");
|
|
1261
|
+
}
|
|
1262
|
+
const missing = modified.filter(
|
|
1263
|
+
(c) =>
|
|
1264
|
+
!c.pedigree?.ancestors?.length &&
|
|
1265
|
+
!c.pedigree?.descendants?.length &&
|
|
1266
|
+
!c.pedigree?.commits?.length &&
|
|
1267
|
+
!c.pedigree?.patches?.length,
|
|
1268
|
+
);
|
|
1269
|
+
if (missing.length) {
|
|
1270
|
+
return fail(
|
|
1271
|
+
`${missing.length} component(s) have an empty pedigree object.`,
|
|
1272
|
+
{
|
|
1273
|
+
mitigation:
|
|
1274
|
+
"Populate pedigree.ancestors / commits / patches for modified components.",
|
|
1275
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
1276
|
+
bomRef: c["bom-ref"],
|
|
1277
|
+
purl: c.purl,
|
|
1278
|
+
})),
|
|
1279
|
+
},
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
return pass(
|
|
1283
|
+
`${modified.length} modified component(s) have pedigree information.`,
|
|
1284
|
+
);
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
scvsManual(
|
|
1288
|
+
"6.4",
|
|
1289
|
+
"Pedigree of modifications documented",
|
|
1290
|
+
"Pedigree of component modification is documented and verifiable.",
|
|
1291
|
+
{ l1: false, l2: true, l3: true },
|
|
1292
|
+
),
|
|
1293
|
+
scvsManual(
|
|
1294
|
+
"6.5",
|
|
1295
|
+
"Modified components uniquely identified",
|
|
1296
|
+
"Modified components are uniquely identified and distinct from origin component.",
|
|
1297
|
+
{ l1: false, l2: true, l3: true },
|
|
1298
|
+
),
|
|
1299
|
+
scvsManual(
|
|
1300
|
+
"6.6",
|
|
1301
|
+
"Modified components analyzed equally",
|
|
1302
|
+
"Modified components are analyzed with the same level of precision as unmodified components.",
|
|
1303
|
+
{ l1: true, l2: true, l3: true },
|
|
1304
|
+
),
|
|
1305
|
+
scvsManual(
|
|
1306
|
+
"6.7",
|
|
1307
|
+
"Risk of modified variants analyzed",
|
|
1308
|
+
"Risk unique to modified components can be analyzed and associated specifically to modified variant.",
|
|
1309
|
+
{ l1: true, l2: true, l3: true },
|
|
1310
|
+
),
|
|
1311
|
+
];
|
|
1312
|
+
|
|
1313
|
+
// ---------------------------------------------------------------------------
|
|
1314
|
+
// EU Cyber Resilience Act (CRA) — SBOM expectations
|
|
1315
|
+
// Based on Annex I section 2 "vulnerability handling requirements" and the
|
|
1316
|
+
// ENISA SBOM guidance for CRA compliance.
|
|
1317
|
+
// ---------------------------------------------------------------------------
|
|
1318
|
+
|
|
1319
|
+
/** @type {Array<object>} */
|
|
1320
|
+
const CRA_RULES = [
|
|
1321
|
+
{
|
|
1322
|
+
id: "CRA-MIN-001",
|
|
1323
|
+
name: "SBOM supplier identified",
|
|
1324
|
+
description:
|
|
1325
|
+
"CRA Article 13(24): The manufacturer must be identifiable from the SBOM so users can reach them for vulnerability reports.",
|
|
1326
|
+
standard: "CRA",
|
|
1327
|
+
standardRefs: ["CRA-ANNEX-I-2-1"],
|
|
1328
|
+
category: "compliance-cra",
|
|
1329
|
+
severity: "high",
|
|
1330
|
+
scvsLevels: [],
|
|
1331
|
+
automatable: true,
|
|
1332
|
+
evaluate(bomJson) {
|
|
1333
|
+
const supplier =
|
|
1334
|
+
bomJson?.metadata?.supplier?.name ||
|
|
1335
|
+
bomJson?.metadata?.manufacture?.name ||
|
|
1336
|
+
bomJson?.metadata?.manufacturer?.name;
|
|
1337
|
+
if (!supplier) {
|
|
1338
|
+
return fail(
|
|
1339
|
+
"metadata.supplier / metadata.manufacturer is missing the manufacturer name.",
|
|
1340
|
+
{
|
|
1341
|
+
mitigation:
|
|
1342
|
+
"Populate metadata.supplier.name so downstream users know who to contact.",
|
|
1343
|
+
},
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
return pass(`Manufacturer declared: ${supplier}.`);
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
id: "CRA-MIN-002",
|
|
1351
|
+
name: "Manufacturer vulnerability contact",
|
|
1352
|
+
description:
|
|
1353
|
+
"CRA Annex I(2)(2): Manufacturers must provide a contact address for vulnerability reports.",
|
|
1354
|
+
standard: "CRA",
|
|
1355
|
+
standardRefs: ["CRA-ANNEX-I-2-2"],
|
|
1356
|
+
category: "compliance-cra",
|
|
1357
|
+
severity: "high",
|
|
1358
|
+
scvsLevels: [],
|
|
1359
|
+
automatable: true,
|
|
1360
|
+
evaluate(bomJson) {
|
|
1361
|
+
const supplier =
|
|
1362
|
+
bomJson?.metadata?.supplier ||
|
|
1363
|
+
bomJson?.metadata?.manufacture ||
|
|
1364
|
+
bomJson?.metadata?.manufacturer;
|
|
1365
|
+
const contacts = supplier?.contact || [];
|
|
1366
|
+
const hasContact = Array.isArray(contacts)
|
|
1367
|
+
? contacts.some((c) => c?.email || c?.phone)
|
|
1368
|
+
: Boolean(contacts?.email || contacts?.phone);
|
|
1369
|
+
const hasUrl = Array.isArray(supplier?.url)
|
|
1370
|
+
? supplier.url.some((u) => u)
|
|
1371
|
+
: typeof supplier?.url === "string" && supplier.url.length > 0;
|
|
1372
|
+
if (!hasContact && !hasUrl) {
|
|
1373
|
+
return fail(
|
|
1374
|
+
"No vulnerability contact (email / phone / URL) recorded on the manufacturer.",
|
|
1375
|
+
{
|
|
1376
|
+
mitigation:
|
|
1377
|
+
"Populate metadata.supplier.contact[].email (or .url) with your PSIRT address.",
|
|
1378
|
+
},
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
return pass("Manufacturer contact information present.");
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
id: "CRA-MIN-003",
|
|
1386
|
+
name: "Unique SBOM identifier",
|
|
1387
|
+
description:
|
|
1388
|
+
"CRA requires that each SBOM is uniquely addressable for vulnerability correlation.",
|
|
1389
|
+
standard: "CRA",
|
|
1390
|
+
standardRefs: ["CRA-ANNEX-I-2-3"],
|
|
1391
|
+
category: "compliance-cra",
|
|
1392
|
+
severity: "high",
|
|
1393
|
+
scvsLevels: [],
|
|
1394
|
+
automatable: true,
|
|
1395
|
+
evaluate(bomJson) {
|
|
1396
|
+
if (
|
|
1397
|
+
bomJson?.serialNumber &&
|
|
1398
|
+
/^urn:uuid:[0-9a-f-]{36}$/i.test(bomJson.serialNumber)
|
|
1399
|
+
) {
|
|
1400
|
+
return pass(`serialNumber present (${bomJson.serialNumber}).`);
|
|
1401
|
+
}
|
|
1402
|
+
return fail("serialNumber missing or not a urn:uuid.");
|
|
1403
|
+
},
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
id: "CRA-MIN-004",
|
|
1407
|
+
name: "Inventory has dependency relationships",
|
|
1408
|
+
description:
|
|
1409
|
+
"CRA requires a complete inventory including dependency relationships so vulnerabilities can be traced.",
|
|
1410
|
+
standard: "CRA",
|
|
1411
|
+
standardRefs: ["CRA-ANNEX-I-2-4"],
|
|
1412
|
+
category: "compliance-cra",
|
|
1413
|
+
severity: "high",
|
|
1414
|
+
scvsLevels: [],
|
|
1415
|
+
automatable: true,
|
|
1416
|
+
evaluate(bomJson) {
|
|
1417
|
+
const comps = inventoryComponents(bomJson);
|
|
1418
|
+
if (comps.length === 0) {
|
|
1419
|
+
return fail("BOM has no inventory components.");
|
|
1420
|
+
}
|
|
1421
|
+
const deps = Array.isArray(bomJson?.dependencies)
|
|
1422
|
+
? bomJson.dependencies
|
|
1423
|
+
: [];
|
|
1424
|
+
if (deps.length === 0) {
|
|
1425
|
+
return fail(
|
|
1426
|
+
"BOM has components but no dependency relationships — root-cause analysis is not possible.",
|
|
1427
|
+
{
|
|
1428
|
+
mitigation:
|
|
1429
|
+
"Ensure cdxgen runs with access to lockfiles so the dependency graph is captured.",
|
|
1430
|
+
},
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
const covered = new Set();
|
|
1434
|
+
for (const dep of deps) {
|
|
1435
|
+
if (dep?.ref) covered.add(dep.ref);
|
|
1436
|
+
for (const child of dep?.dependsOn || []) covered.add(child);
|
|
1437
|
+
}
|
|
1438
|
+
const uncovered = comps.filter(
|
|
1439
|
+
(c) => !c["bom-ref"] || !covered.has(c["bom-ref"]),
|
|
1440
|
+
);
|
|
1441
|
+
if (uncovered.length > comps.length * 0.25) {
|
|
1442
|
+
return fail(
|
|
1443
|
+
`${uncovered.length}/${comps.length} component(s) are not represented in the dependency graph.`,
|
|
1444
|
+
{
|
|
1445
|
+
mitigation:
|
|
1446
|
+
"Regenerate with deeper analysis so all components appear in the dependency graph.",
|
|
1447
|
+
locations: uncovered.slice(0, 25).map((c) => ({
|
|
1448
|
+
bomRef: c["bom-ref"],
|
|
1449
|
+
purl: c.purl,
|
|
1450
|
+
})),
|
|
1451
|
+
},
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
return pass(
|
|
1455
|
+
`${deps.length} dependency node(s) covering ${comps.length - uncovered.length}/${comps.length} component(s).`,
|
|
1456
|
+
);
|
|
1457
|
+
},
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
id: "CRA-MIN-005",
|
|
1461
|
+
name: "Timestamp",
|
|
1462
|
+
description:
|
|
1463
|
+
"CRA requires each SBOM to be timestamped so vulnerability freshness can be evaluated.",
|
|
1464
|
+
standard: "CRA",
|
|
1465
|
+
standardRefs: ["CRA-ANNEX-I-2-5"],
|
|
1466
|
+
category: "compliance-cra",
|
|
1467
|
+
severity: "high",
|
|
1468
|
+
scvsLevels: [],
|
|
1469
|
+
automatable: true,
|
|
1470
|
+
evaluate(bomJson) {
|
|
1471
|
+
const ts = bomJson?.metadata?.timestamp;
|
|
1472
|
+
if (!ts) return fail("metadata.timestamp is missing.");
|
|
1473
|
+
if (Number.isNaN(Date.parse(ts))) {
|
|
1474
|
+
return fail(`metadata.timestamp is not valid ISO-8601: ${ts}`);
|
|
1475
|
+
}
|
|
1476
|
+
return pass(`metadata.timestamp present (${ts}).`);
|
|
1477
|
+
},
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
id: "CRA-MIN-006",
|
|
1481
|
+
name: "Component identifiers resolvable",
|
|
1482
|
+
description:
|
|
1483
|
+
"CRA requires machine-readable identifiers for every component so users can cross-reference vulnerability databases.",
|
|
1484
|
+
standard: "CRA",
|
|
1485
|
+
standardRefs: ["CRA-ANNEX-I-2-6"],
|
|
1486
|
+
category: "compliance-cra",
|
|
1487
|
+
severity: "high",
|
|
1488
|
+
scvsLevels: [],
|
|
1489
|
+
automatable: true,
|
|
1490
|
+
evaluate(bomJson) {
|
|
1491
|
+
const comps = inventoryComponents(bomJson);
|
|
1492
|
+
const missing = comps.filter((c) => !c.purl && !c.cpe && !c.swid?.tagId);
|
|
1493
|
+
if (missing.length) {
|
|
1494
|
+
return fail(
|
|
1495
|
+
`${missing.length} component(s) lack purl/cpe/swid identifiers.`,
|
|
1496
|
+
{
|
|
1497
|
+
mitigation: "Regenerate with cdxgen so purls are emitted.",
|
|
1498
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
1499
|
+
bomRef: c["bom-ref"],
|
|
1500
|
+
name: c.name,
|
|
1501
|
+
})),
|
|
1502
|
+
},
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
return pass(
|
|
1506
|
+
`All ${comps.length} component(s) have a resolvable identifier.`,
|
|
1507
|
+
);
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
id: "CRA-MIN-007",
|
|
1512
|
+
name: "License information",
|
|
1513
|
+
description:
|
|
1514
|
+
"CRA and downstream guidance (ENISA) recommend recording license data so downstream users can satisfy copyright obligations.",
|
|
1515
|
+
standard: "CRA",
|
|
1516
|
+
standardRefs: ["CRA-ENISA-LICENSE"],
|
|
1517
|
+
category: "compliance-cra",
|
|
1518
|
+
severity: "medium",
|
|
1519
|
+
scvsLevels: [],
|
|
1520
|
+
automatable: true,
|
|
1521
|
+
evaluate(bomJson) {
|
|
1522
|
+
const comps = inventoryComponents(bomJson);
|
|
1523
|
+
if (comps.length === 0) {
|
|
1524
|
+
return fail("BOM has no inventory components.");
|
|
1525
|
+
}
|
|
1526
|
+
const missing = comps.filter((c) => !c.licenses?.length);
|
|
1527
|
+
if (missing.length) {
|
|
1528
|
+
return fail(
|
|
1529
|
+
`${missing.length}/${comps.length} component(s) lack license information.`,
|
|
1530
|
+
{
|
|
1531
|
+
mitigation:
|
|
1532
|
+
"Run cdxgen with FETCH_LICENSE=true or provide a license allow-list.",
|
|
1533
|
+
locations: missing.slice(0, 25).map((c) => ({
|
|
1534
|
+
bomRef: c["bom-ref"],
|
|
1535
|
+
purl: c.purl,
|
|
1536
|
+
})),
|
|
1537
|
+
},
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
return pass(
|
|
1541
|
+
`All ${comps.length} inventory component(s) declare license information.`,
|
|
1542
|
+
);
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
id: "CRA-MIN-008",
|
|
1547
|
+
name: "Tool provenance recorded",
|
|
1548
|
+
description:
|
|
1549
|
+
"CRA Annex I(2)(3): SBOM must record the tool(s) that produced it so its provenance can be audited.",
|
|
1550
|
+
standard: "CRA",
|
|
1551
|
+
standardRefs: ["CRA-ANNEX-I-2-3-TOOLS"],
|
|
1552
|
+
category: "compliance-cra",
|
|
1553
|
+
severity: "medium",
|
|
1554
|
+
scvsLevels: [],
|
|
1555
|
+
automatable: true,
|
|
1556
|
+
evaluate(bomJson) {
|
|
1557
|
+
const tools = bomJson?.metadata?.tools;
|
|
1558
|
+
// CycloneDX 1.4 uses array, 1.5+ uses object with components.
|
|
1559
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
1560
|
+
return pass(`${tools.length} tool(s) recorded.`);
|
|
1561
|
+
}
|
|
1562
|
+
if (tools?.components?.length || tools?.services?.length) {
|
|
1563
|
+
return pass(
|
|
1564
|
+
`${tools.components?.length || 0} tool component(s), ${tools.services?.length || 0} tool service(s) recorded.`,
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
return fail("metadata.tools is empty or missing.");
|
|
1568
|
+
},
|
|
1569
|
+
},
|
|
1570
|
+
];
|
|
1571
|
+
|
|
1572
|
+
// ---------------------------------------------------------------------------
|
|
1573
|
+
// Public API
|
|
1574
|
+
// ---------------------------------------------------------------------------
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Returns the full catalog of compliance rules (SCVS + CRA).
|
|
1578
|
+
*
|
|
1579
|
+
* @returns {Array<object>}
|
|
1580
|
+
*/
|
|
1581
|
+
export function getAllComplianceRules() {
|
|
1582
|
+
return [...SCVS_RULES, ...CRA_RULES];
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Returns only SCVS rules.
|
|
1587
|
+
*
|
|
1588
|
+
* @returns {Array<object>}
|
|
1589
|
+
*/
|
|
1590
|
+
export function getScvsRules() {
|
|
1591
|
+
return [...SCVS_RULES];
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Returns only CRA rules.
|
|
1596
|
+
*
|
|
1597
|
+
* @returns {Array<object>}
|
|
1598
|
+
*/
|
|
1599
|
+
export function getCraRules() {
|
|
1600
|
+
return [...CRA_RULES];
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Expose internal helpers for unit tests only.
|
|
1604
|
+
export const __test = {
|
|
1605
|
+
componentLicenseId,
|
|
1606
|
+
inventoryComponents,
|
|
1607
|
+
looksLikeSpdx,
|
|
1608
|
+
collectReferencedRefs,
|
|
1609
|
+
compLabel,
|
|
1610
|
+
};
|