@cyclonedx/cdxgen 12.2.0 → 12.3.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.
Files changed (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,310 @@
1
+ import {
2
+ hasRegistryProvenanceEvidenceProperties,
3
+ hasTrustedPublishingProperties,
4
+ } from "../helpers/provenanceUtils.js";
5
+
6
+ export const SEVERITY_ORDER = {
7
+ none: -1,
8
+ low: 0,
9
+ medium: 1,
10
+ high: 2,
11
+ critical: 3,
12
+ };
13
+
14
+ const BASE_FINDING_WEIGHT = {
15
+ low: 4,
16
+ medium: 10,
17
+ high: 18,
18
+ critical: 30,
19
+ };
20
+
21
+ const CATEGORY_WEIGHT = {
22
+ "ci-permission": 12,
23
+ "dependency-source": 8,
24
+ "package-integrity": 6,
25
+ };
26
+
27
+ const RULE_SPECIFIC_WEIGHT = {
28
+ "CI-019": 16,
29
+ "INT-009": 14,
30
+ };
31
+
32
+ const PRIORITY_CORROBORATION_RULES = new Set(["CI-019", "INT-009"]);
33
+
34
+ /**
35
+ * Clamp a number into a fixed range.
36
+ *
37
+ * @param {number} value input number
38
+ * @param {number} min minimum value
39
+ * @param {number} max maximum value
40
+ * @returns {number} clamped number
41
+ */
42
+ function clamp(value, min, max) {
43
+ return Math.max(min, Math.min(max, value));
44
+ }
45
+
46
+ /**
47
+ * Retrieve a custom property value from a target descriptor.
48
+ *
49
+ * @param {object} target audit target
50
+ * @param {string} propertyName property name
51
+ * @returns {string | undefined} property value
52
+ */
53
+ function getTargetProperty(target, propertyName) {
54
+ return target?.properties?.find((property) => property.name === propertyName)
55
+ ?.value;
56
+ }
57
+
58
+ /**
59
+ * Convert a numeric confidence score into a human readable label.
60
+ *
61
+ * @param {number} confidence confidence score
62
+ * @returns {string} confidence label
63
+ */
64
+ export function confidenceLabel(confidence) {
65
+ if (confidence >= 0.85) {
66
+ return "high";
67
+ }
68
+ if (confidence >= 0.6) {
69
+ return "medium";
70
+ }
71
+ return "low";
72
+ }
73
+
74
+ /**
75
+ * Check if a severity meets the given threshold.
76
+ *
77
+ * @param {string} severity severity to compare
78
+ * @param {string} threshold threshold severity
79
+ * @returns {boolean} true if severity is at or above threshold
80
+ */
81
+ export function severityMeetsThreshold(severity, threshold) {
82
+ const resolvedSeverity = SEVERITY_ORDER[severity] ?? SEVERITY_ORDER.none;
83
+ const resolvedThreshold = SEVERITY_ORDER[threshold] ?? SEVERITY_ORDER.low;
84
+ return resolvedSeverity >= resolvedThreshold;
85
+ }
86
+
87
+ /**
88
+ * Conservatively score predictive supply-chain risk for a single target.
89
+ *
90
+ * High and critical require corroboration across categories and strong findings,
91
+ * which keeps false positives low.
92
+ *
93
+ * @param {object[]} findings post-generation audit findings
94
+ * @param {object} target target metadata
95
+ * @param {object} context additional scan context
96
+ * @returns {object} conservative risk assessment
97
+ */
98
+ export function scoreTargetRisk(findings, target, context = {}) {
99
+ if (!Array.isArray(findings) || findings.length === 0) {
100
+ const explicitReason =
101
+ context?.skipReason ||
102
+ context?.scanErrorReason ||
103
+ context?.errorMessage ||
104
+ (context?.scanError
105
+ ? `Predictive audit for '${target.purl}' could not complete successfully.`
106
+ : `${target.type} package '${target.purl}' did not trigger any predictive audit rules.`);
107
+ return {
108
+ categoryCounts: {},
109
+ confidence: 0.35,
110
+ confidenceLabel: "low",
111
+ distinctCategoryCount: 0,
112
+ findingsCount: 0,
113
+ formulationSignalCount: 0,
114
+ reasons: [explicitReason],
115
+ score: 0,
116
+ severity: "none",
117
+ strongSignalCount: 0,
118
+ };
119
+ }
120
+ const categoryCounts = {};
121
+ const attackTactics = new Set();
122
+ const attackTechniques = new Set();
123
+ const distinctCategories = new Set();
124
+ const matchedPriorityRules = new Set();
125
+ let score = 0;
126
+ let strongSignalCount = 0;
127
+ let ciSignalCount = 0;
128
+ let formulationSignalCount = 0;
129
+ let priorityCorroborationCount = 0;
130
+ for (const finding of findings) {
131
+ const findingSeverity = finding?.severity || "low";
132
+ const findingCategory = finding?.category || "unknown";
133
+ let findingScore = BASE_FINDING_WEIGHT[findingSeverity] ?? 4;
134
+ findingScore += CATEGORY_WEIGHT[findingCategory] ?? 4;
135
+ findingScore += RULE_SPECIFIC_WEIGHT[finding?.ruleId] ?? 0;
136
+ if (findingCategory === "ci-permission") {
137
+ ciSignalCount += 1;
138
+ findingScore += 8;
139
+ }
140
+ if (
141
+ finding?.ruleId?.startsWith("CI-") ||
142
+ finding?.location?.file?.includes(".github/workflows")
143
+ ) {
144
+ formulationSignalCount += 1;
145
+ findingScore += 8;
146
+ }
147
+ if (["high", "critical"].includes(findingSeverity)) {
148
+ strongSignalCount += 1;
149
+ }
150
+ if (PRIORITY_CORROBORATION_RULES.has(finding?.ruleId)) {
151
+ priorityCorroborationCount += 1;
152
+ matchedPriorityRules.add(finding.ruleId);
153
+ }
154
+ (finding?.attackTactics || finding?.attack?.tactics || []).forEach((id) => {
155
+ if (id) {
156
+ attackTactics.add(id);
157
+ }
158
+ });
159
+ (finding?.attackTechniques || finding?.attack?.techniques || []).forEach(
160
+ (id) => {
161
+ if (id) {
162
+ attackTechniques.add(id);
163
+ }
164
+ },
165
+ );
166
+ categoryCounts[findingCategory] =
167
+ (categoryCounts[findingCategory] || 0) + 1;
168
+ distinctCategories.add(findingCategory);
169
+ score += findingScore;
170
+ }
171
+ score += Math.max(0, distinctCategories.size - 1) * 8;
172
+ score += Math.max(0, strongSignalCount - 1) * 10;
173
+ score += Math.max(0, formulationSignalCount - 1) * 6;
174
+
175
+ const hasTrustedPublishing = hasTrustedPublishingProperties(
176
+ target?.properties,
177
+ );
178
+ const hasProvenanceEvidence = hasRegistryProvenanceEvidenceProperties(
179
+ target?.properties,
180
+ );
181
+ const hasVerifiedPublisher =
182
+ getTargetProperty(target, "cdx:pypi:uploaderVerified") === "true";
183
+ let provenanceDiscount = 0;
184
+ if (hasProvenanceEvidence) {
185
+ provenanceDiscount += 4;
186
+ }
187
+ if (hasTrustedPublishing) {
188
+ provenanceDiscount += 6;
189
+ }
190
+ if (hasVerifiedPublisher) {
191
+ provenanceDiscount += 2;
192
+ }
193
+ score -= Math.min(provenanceDiscount, 10);
194
+ if (score < 0) {
195
+ score = 0;
196
+ }
197
+
198
+ const effectiveStrongSignalCount =
199
+ strongSignalCount + priorityCorroborationCount;
200
+
201
+ let confidence = 0.45;
202
+ if (context?.resolution?.repoUrl) {
203
+ confidence += 0.15;
204
+ }
205
+ if (target?.version) {
206
+ confidence += 0.1;
207
+ }
208
+ if (context?.versionMatched) {
209
+ confidence += 0.1;
210
+ }
211
+ if (context?.bomJson?.formulation?.length) {
212
+ confidence += 0.15;
213
+ }
214
+ if (context?.sourceDirectoryConfidence === "high") {
215
+ confidence += 0.05;
216
+ }
217
+ if (context?.sourceDirectoryConfidence === "low") {
218
+ confidence -= 0.1;
219
+ }
220
+ if (context?.scanError) {
221
+ confidence -= 0.35;
222
+ }
223
+ if (!context?.resolution?.repoUrl) {
224
+ confidence -= 0.2;
225
+ }
226
+ confidence = clamp(confidence, 0.05, 0.95);
227
+
228
+ let severity = "low";
229
+ if (score >= 84) {
230
+ severity = "critical";
231
+ } else if (score >= 52) {
232
+ severity = "high";
233
+ } else if (score >= 24) {
234
+ severity = "medium";
235
+ }
236
+
237
+ if (
238
+ severity === "critical" &&
239
+ (effectiveStrongSignalCount < 3 ||
240
+ distinctCategories.size < 2 ||
241
+ ciSignalCount < 1 ||
242
+ confidence < 0.85)
243
+ ) {
244
+ severity = "high";
245
+ }
246
+ if (
247
+ severity === "high" &&
248
+ (effectiveStrongSignalCount < 2 ||
249
+ distinctCategories.size < 2 ||
250
+ confidence < 0.65)
251
+ ) {
252
+ severity = "medium";
253
+ }
254
+ if (context?.scanError && severityMeetsThreshold(severity, "high")) {
255
+ severity = "medium";
256
+ }
257
+
258
+ const reasons = [];
259
+ if (ciSignalCount > 0) {
260
+ reasons.push(
261
+ `${ciSignalCount} GitHub Actions or privileged workflow signal(s) increased the predictive risk score.`,
262
+ );
263
+ }
264
+ if (distinctCategories.size > 1) {
265
+ reasons.push(
266
+ `${distinctCategories.size} distinct rule categories corroborated the package risk posture.`,
267
+ );
268
+ }
269
+ if (strongSignalCount > 0) {
270
+ reasons.push(
271
+ `${strongSignalCount} strong finding(s) were observed across the generated source SBOM.`,
272
+ );
273
+ }
274
+ if (priorityCorroborationCount > 0) {
275
+ reasons.push(
276
+ `${priorityCorroborationCount} high-confidence compound rule(s) received additional predictive weight (${Array.from(matchedPriorityRules).join(", ")}).`,
277
+ );
278
+ }
279
+ if (attackTactics.size > 0 || attackTechniques.size > 0) {
280
+ reasons.push(
281
+ `${attackTactics.size} ATT&CK tactic(s) and ${attackTechniques.size} ATT&CK technique(s) were implicated by the audit findings.`,
282
+ );
283
+ }
284
+ if (hasTrustedPublishing || hasProvenanceEvidence || hasVerifiedPublisher) {
285
+ reasons.push(
286
+ "Registry provenance or trusted-publishing evidence reduced the final predictive score.",
287
+ );
288
+ }
289
+ if (reasons.length === 0) {
290
+ reasons.push(
291
+ `Findings remained isolated, so severity stayed conservative for '${target.purl}'.`,
292
+ );
293
+ }
294
+
295
+ return {
296
+ categoryCounts,
297
+ attackTacticCount: attackTactics.size,
298
+ attackTechniqueCount: attackTechniques.size,
299
+ confidence,
300
+ confidenceLabel: confidenceLabel(confidence),
301
+ distinctCategoryCount: distinctCategories.size,
302
+ findingsCount: findings.length,
303
+ formulationSignalCount,
304
+ priorityCorroborationCount,
305
+ reasons,
306
+ score,
307
+ severity,
308
+ strongSignalCount,
309
+ };
310
+ }
@@ -0,0 +1,341 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ confidenceLabel,
5
+ scoreTargetRisk,
6
+ severityMeetsThreshold,
7
+ } from "./scoring.js";
8
+
9
+ const baseTarget = {
10
+ name: "left-pad",
11
+ purl: "pkg:npm/left-pad@1.3.0",
12
+ type: "npm",
13
+ version: "1.3.0",
14
+ };
15
+
16
+ describe("confidenceLabel()", () => {
17
+ it("maps numeric confidence to the expected buckets", () => {
18
+ assert.strictEqual(confidenceLabel(0.2), "low");
19
+ assert.strictEqual(confidenceLabel(0.7), "medium");
20
+ assert.strictEqual(confidenceLabel(0.9), "high");
21
+ });
22
+ });
23
+
24
+ describe("severityMeetsThreshold()", () => {
25
+ it("compares severities in the expected order", () => {
26
+ assert.strictEqual(severityMeetsThreshold("high", "medium"), true);
27
+ assert.strictEqual(severityMeetsThreshold("low", "high"), false);
28
+ assert.strictEqual(severityMeetsThreshold("none", "low"), false);
29
+ });
30
+ });
31
+
32
+ describe("scoreTargetRisk()", () => {
33
+ it("keeps a single strong signal at medium to reduce false positives", () => {
34
+ const findings = [
35
+ {
36
+ attackTactics: ["TA0001", "TA0004"],
37
+ attackTechniques: ["T1195.001"],
38
+ category: "ci-permission",
39
+ location: {
40
+ file: ".github/workflows/release.yml",
41
+ },
42
+ message: "Unpinned privileged action",
43
+ ruleId: "CI-001",
44
+ severity: "high",
45
+ },
46
+ ];
47
+
48
+ const assessment = scoreTargetRisk(findings, baseTarget, {
49
+ bomJson: {
50
+ formulation: [{}],
51
+ },
52
+ resolution: {
53
+ repoUrl: "https://github.com/example/repo",
54
+ },
55
+ sourceDirectoryConfidence: "high",
56
+ versionMatched: true,
57
+ });
58
+
59
+ assert.strictEqual(assessment.severity, "medium");
60
+ assert.strictEqual(assessment.attackTacticCount, 2);
61
+ assert.strictEqual(assessment.attackTechniqueCount, 1);
62
+ assert.ok(assessment.score > 0);
63
+ assert.match(assessment.reasons.join(" "), /ATT&CK tactic/i);
64
+ });
65
+
66
+ it("escalates to high only when independent signals corroborate the risk", () => {
67
+ const findings = [
68
+ {
69
+ category: "ci-permission",
70
+ location: {
71
+ file: ".github/workflows/release.yml",
72
+ },
73
+ message: "Unpinned privileged action",
74
+ ruleId: "CI-001",
75
+ severity: "high",
76
+ },
77
+ {
78
+ category: "dependency-source",
79
+ location: {
80
+ purl: baseTarget.purl,
81
+ },
82
+ message: "Install script from non-registry source",
83
+ ruleId: "PKG-001",
84
+ severity: "high",
85
+ },
86
+ ];
87
+
88
+ const assessment = scoreTargetRisk(findings, baseTarget, {
89
+ bomJson: {
90
+ formulation: [{}],
91
+ },
92
+ resolution: {
93
+ repoUrl: "https://github.com/example/repo",
94
+ },
95
+ sourceDirectoryConfidence: "high",
96
+ versionMatched: true,
97
+ });
98
+
99
+ assert.strictEqual(assessment.severity, "high");
100
+ assert.strictEqual(assessment.distinctCategoryCount, 2);
101
+ });
102
+
103
+ it("requires multiple corroborated strong signals before allowing critical", () => {
104
+ const findings = [
105
+ {
106
+ category: "ci-permission",
107
+ location: {
108
+ file: ".github/workflows/release.yml",
109
+ },
110
+ message: "Untrusted interpolation in shell step",
111
+ ruleId: "CI-007",
112
+ severity: "critical",
113
+ },
114
+ {
115
+ category: "dependency-source",
116
+ location: {
117
+ purl: baseTarget.purl,
118
+ },
119
+ message: "Install script from non-registry source",
120
+ ruleId: "PKG-001",
121
+ severity: "high",
122
+ },
123
+ {
124
+ category: "package-integrity",
125
+ location: {
126
+ purl: baseTarget.purl,
127
+ },
128
+ message: "Name mismatch",
129
+ ruleId: "INT-002",
130
+ severity: "high",
131
+ },
132
+ ];
133
+
134
+ const assessment = scoreTargetRisk(findings, baseTarget, {
135
+ bomJson: {
136
+ formulation: [{ workflows: [] }],
137
+ },
138
+ resolution: {
139
+ repoUrl: "https://github.com/example/repo",
140
+ },
141
+ sourceDirectoryConfidence: "high",
142
+ versionMatched: true,
143
+ });
144
+
145
+ assert.strictEqual(assessment.severity, "critical");
146
+ assert.ok(assessment.confidence >= 0.85);
147
+ });
148
+
149
+ it("downgrades severity when the scan encountered an error", () => {
150
+ const findings = [
151
+ {
152
+ category: "ci-permission",
153
+ location: {
154
+ file: ".github/workflows/release.yml",
155
+ },
156
+ message: "Untrusted interpolation in shell step",
157
+ ruleId: "CI-007",
158
+ severity: "critical",
159
+ },
160
+ {
161
+ category: "dependency-source",
162
+ location: {
163
+ purl: baseTarget.purl,
164
+ },
165
+ message: "Install script from non-registry source",
166
+ ruleId: "PKG-001",
167
+ severity: "high",
168
+ },
169
+ ];
170
+
171
+ const assessment = scoreTargetRisk(findings, baseTarget, {
172
+ bomJson: {
173
+ formulation: [{ workflows: [] }],
174
+ },
175
+ resolution: {
176
+ repoUrl: "https://github.com/example/repo",
177
+ },
178
+ scanError: true,
179
+ sourceDirectoryConfidence: "low",
180
+ versionMatched: false,
181
+ });
182
+
183
+ assert.strictEqual(assessment.severity, "medium");
184
+ });
185
+
186
+ it("reduces the final score when trusted publishing evidence is present", () => {
187
+ const findings = [
188
+ {
189
+ category: "dependency-source",
190
+ location: {
191
+ purl: baseTarget.purl,
192
+ },
193
+ message: "Install script from non-registry source",
194
+ ruleId: "PKG-001",
195
+ severity: "high",
196
+ },
197
+ ];
198
+
199
+ const withoutProvenance = scoreTargetRisk(findings, baseTarget, {
200
+ resolution: {
201
+ repoUrl: "https://github.com/example/repo",
202
+ },
203
+ versionMatched: true,
204
+ });
205
+ const withProvenance = scoreTargetRisk(
206
+ findings,
207
+ {
208
+ ...baseTarget,
209
+ properties: [
210
+ {
211
+ name: "cdx:npm:provenanceUrl",
212
+ value: "https://registry.npmjs.org/-/npm/v1/attestations/example",
213
+ },
214
+ { name: "cdx:npm:trustedPublishing", value: "true" },
215
+ ],
216
+ },
217
+ {
218
+ resolution: {
219
+ repoUrl: "https://github.com/example/repo",
220
+ },
221
+ versionMatched: true,
222
+ },
223
+ );
224
+
225
+ assert.ok(withProvenance.score < withoutProvenance.score);
226
+ assert.match(
227
+ withProvenance.reasons.join(" "),
228
+ /trusted-publishing evidence/i,
229
+ );
230
+ });
231
+
232
+ it("reduces the final score when direct provenance evidence properties are present", () => {
233
+ const findings = [
234
+ {
235
+ category: "dependency-source",
236
+ location: {
237
+ purl: baseTarget.purl,
238
+ },
239
+ message: "Install script from non-registry source",
240
+ ruleId: "PKG-001",
241
+ severity: "high",
242
+ },
243
+ ];
244
+
245
+ const withoutEvidence = scoreTargetRisk(findings, baseTarget, {
246
+ resolution: {
247
+ repoUrl: "https://github.com/example/repo",
248
+ },
249
+ versionMatched: true,
250
+ });
251
+ const withEvidence = scoreTargetRisk(
252
+ findings,
253
+ {
254
+ ...baseTarget,
255
+ properties: [
256
+ {
257
+ name: "cdx:npm:provenanceKeyId",
258
+ value: "sigstore-key",
259
+ },
260
+ ],
261
+ },
262
+ {
263
+ resolution: {
264
+ repoUrl: "https://github.com/example/repo",
265
+ },
266
+ versionMatched: true,
267
+ },
268
+ );
269
+
270
+ assert.ok(withEvidence.score < withoutEvidence.score);
271
+ });
272
+
273
+ it("keeps isolated CI-019 findings conservative despite the rule-specific bonus", () => {
274
+ const findings = [
275
+ {
276
+ category: "ci-permission",
277
+ location: {
278
+ file: ".github/workflows/dispatch.yml",
279
+ },
280
+ message: "Fork-aware dispatch chain",
281
+ ruleId: "CI-019",
282
+ severity: "critical",
283
+ },
284
+ ];
285
+
286
+ const assessment = scoreTargetRisk(findings, baseTarget, {
287
+ bomJson: {
288
+ formulation: [{ workflows: [] }],
289
+ },
290
+ resolution: {
291
+ repoUrl: "https://github.com/example/repo",
292
+ },
293
+ sourceDirectoryConfidence: "high",
294
+ versionMatched: true,
295
+ });
296
+
297
+ assert.strictEqual(assessment.severity, "medium");
298
+ assert.ok(assessment.priorityCorroborationCount >= 1);
299
+ });
300
+
301
+ it("escalates CI-019 plus INT-009 to critical at high confidence", () => {
302
+ const findings = [
303
+ {
304
+ category: "ci-permission",
305
+ location: {
306
+ file: ".github/workflows/dispatch.yml",
307
+ },
308
+ message: "Fork-aware dispatch chain",
309
+ ruleId: "CI-019",
310
+ severity: "critical",
311
+ },
312
+ {
313
+ category: "package-integrity",
314
+ location: {
315
+ purl: baseTarget.purl,
316
+ },
317
+ message: "Obfuscated install hook",
318
+ ruleId: "INT-009",
319
+ severity: "critical",
320
+ },
321
+ ];
322
+
323
+ const assessment = scoreTargetRisk(findings, baseTarget, {
324
+ bomJson: {
325
+ formulation: [{ workflows: [] }],
326
+ },
327
+ resolution: {
328
+ repoUrl: "https://github.com/example/repo",
329
+ },
330
+ sourceDirectoryConfidence: "high",
331
+ versionMatched: true,
332
+ });
333
+
334
+ assert.strictEqual(assessment.severity, "critical");
335
+ assert.ok(assessment.priorityCorroborationCount >= 2);
336
+ assert.match(
337
+ assessment.reasons.join(" "),
338
+ /high-confidence compound rule/i,
339
+ );
340
+ });
341
+ });