@cyclonedx/cdxgen 12.3.0 → 12.3.2
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 +15 -5
- package/bin/audit.js +7 -0
- package/bin/cdxgen.js +241 -81
- package/bin/repl.js +138 -0
- package/data/rules/ai-agent-governance.yaml +249 -0
- package/data/rules/dependency-sources.yaml +41 -0
- package/data/rules/mcp-servers.yaml +304 -0
- package/data/rules/package-integrity.yaml +123 -0
- package/lib/audit/index.js +353 -29
- package/lib/audit/index.poku.js +247 -7
- package/lib/audit/reporters.js +26 -0
- package/lib/audit/scoring.js +262 -13
- package/lib/audit/scoring.poku.js +179 -0
- package/lib/audit/targets.js +391 -2
- package/lib/audit/targets.poku.js +416 -3
- package/lib/cli/index.js +588 -45
- package/lib/cli/index.poku.js +735 -1
- package/lib/evinser/evinser.js +8 -5
- package/lib/helpers/agentFormulationParser.js +318 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +1769 -0
- package/lib/helpers/analyzer.poku.js +284 -3
- package/lib/helpers/auditCategories.js +76 -0
- package/lib/helpers/ciParsers/githubActions.js +140 -16
- package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
- package/lib/helpers/communityAiConfigParser.js +672 -0
- package/lib/helpers/communityAiConfigParser.poku.js +63 -0
- package/lib/helpers/depsUtils.js +108 -0
- package/lib/helpers/depsUtils.poku.js +72 -1
- package/lib/helpers/display.js +325 -3
- package/lib/helpers/display.poku.js +301 -0
- package/lib/helpers/formulationParsers.js +28 -0
- package/lib/helpers/formulationParsers.poku.js +504 -1
- package/lib/helpers/jsonLike.js +102 -0
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcp.js +248 -0
- package/lib/helpers/mcp.poku.js +101 -0
- package/lib/helpers/mcpConfigParser.js +656 -0
- package/lib/helpers/mcpConfigParser.poku.js +126 -0
- package/lib/helpers/mcpDiscovery.js +84 -0
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/protobom.js +3 -3
- package/lib/helpers/provenanceUtils.js +29 -4
- package/lib/helpers/provenanceUtils.poku.js +29 -3
- package/lib/helpers/registryProvenance.js +210 -0
- package/lib/helpers/registryProvenance.poku.js +144 -0
- package/lib/helpers/rustFormulationParser.js +330 -0
- package/lib/helpers/source.js +21 -2
- package/lib/helpers/source.poku.js +38 -0
- package/lib/helpers/utils.js +1331 -83
- package/lib/helpers/utils.poku.js +599 -188
- package/lib/helpers/vsixutils.js +12 -4
- package/lib/helpers/vsixutils.poku.js +34 -0
- package/lib/managers/binary.js +36 -12
- package/lib/managers/binary.poku.js +68 -0
- package/lib/managers/docker.js +59 -9
- package/lib/managers/docker.poku.js +61 -0
- package/lib/managers/piptree.js +12 -7
- package/lib/managers/piptree.poku.js +44 -0
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +20 -6
- package/lib/stages/postgen/auditBom.poku.js +694 -1
- package/lib/stages/postgen/postgen.js +262 -11
- package/lib/stages/postgen/postgen.poku.js +306 -2
- package/lib/stages/postgen/ruleEngine.js +49 -1
- package/lib/stages/postgen/spdxConverter.poku.js +70 -0
- package/lib/stages/pregen/pregen.js +6 -4
- package/package.json +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts +12 -0
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -8
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/aiInventory.d.ts +23 -0
- package/types/lib/helpers/aiInventory.d.ts.map +1 -0
- package/types/lib/helpers/analyzer.d.ts +10 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/auditCategories.d.ts +12 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +8 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +17 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/jsonLike.d.ts +4 -0
- package/types/lib/helpers/jsonLike.d.ts.map +1 -0
- package/types/lib/helpers/mcp.d.ts +29 -0
- package/types/lib/helpers/mcp.d.ts.map +1 -0
- package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
- package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
- package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +5 -3
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
- package/types/lib/helpers/registryProvenance.d.ts +9 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
- package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
- package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +31 -1
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
package/lib/audit/scoring.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { thoughtLog } from "../helpers/logger.js";
|
|
1
2
|
import {
|
|
2
3
|
hasRegistryProvenanceEvidenceProperties,
|
|
3
4
|
hasTrustedPublishingProperties,
|
|
@@ -18,9 +19,13 @@ const BASE_FINDING_WEIGHT = {
|
|
|
18
19
|
critical: 30,
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
// Predictive scoring weighs dependency-source findings more heavily than generic
|
|
23
|
+
// CI hygiene because source mutability/local-path/git-origin issues tend to map
|
|
24
|
+
// more directly to reviewable upstream compromise exposure for package targets.
|
|
21
25
|
const CATEGORY_WEIGHT = {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
26
|
+
"ai-agent": 6,
|
|
27
|
+
"ci-permission": 4,
|
|
28
|
+
"dependency-source": 10,
|
|
24
29
|
"package-integrity": 6,
|
|
25
30
|
};
|
|
26
31
|
|
|
@@ -30,6 +35,56 @@ const RULE_SPECIFIC_WEIGHT = {
|
|
|
30
35
|
};
|
|
31
36
|
|
|
32
37
|
const PRIORITY_CORROBORATION_RULES = new Set(["CI-019", "INT-009"]);
|
|
38
|
+
// Require multiple compromise-oriented signals before escalating to critical so
|
|
39
|
+
// a single strong heuristic or one noisy category cannot dominate the final
|
|
40
|
+
// predictive severity on its own.
|
|
41
|
+
const MIN_COMPROMISE_SIGNALS_FOR_CRITICAL = 2;
|
|
42
|
+
const MIN_COMPROMISE_SIGNALS_FOR_HIGH = 1;
|
|
43
|
+
const CI_HYGIENE_RULES = new Set([
|
|
44
|
+
"CI-001",
|
|
45
|
+
"CI-002",
|
|
46
|
+
"CI-003",
|
|
47
|
+
"CI-005",
|
|
48
|
+
"CI-014",
|
|
49
|
+
]);
|
|
50
|
+
const PACKAGE_HYGIENE_RULES = new Set(["INT-001"]);
|
|
51
|
+
const SIGNAL_BUCKET_WEIGHT = {
|
|
52
|
+
"ci-compromise": 12,
|
|
53
|
+
"ci-hygiene": 2,
|
|
54
|
+
"dependency-compromise": 10,
|
|
55
|
+
"package-hygiene": 3,
|
|
56
|
+
"package-compromise": 12,
|
|
57
|
+
other: 4,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Emit a short thought-log explanation for the final package risk decision.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} target audit target descriptor
|
|
64
|
+
* @param {object} assessment final predictive risk assessment
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
function logRiskAssessmentDecision(target, assessment) {
|
|
68
|
+
const reasonPreview = Array.isArray(assessment?.reasons)
|
|
69
|
+
? assessment.reasons.slice(0, 2)
|
|
70
|
+
: [];
|
|
71
|
+
const indicators = {
|
|
72
|
+
confidence: assessment?.confidence,
|
|
73
|
+
confidenceLabel: assessment?.confidenceLabel,
|
|
74
|
+
distinctCategories: assessment?.distinctCategoryCount,
|
|
75
|
+
findings: assessment?.findingsCount,
|
|
76
|
+
purl: target?.purl,
|
|
77
|
+
reasons: reasonPreview,
|
|
78
|
+
score: assessment?.score,
|
|
79
|
+
severity: assessment?.severity,
|
|
80
|
+
strongSignals: assessment?.strongSignalCount,
|
|
81
|
+
};
|
|
82
|
+
if (["medium", "high", "critical"].includes(assessment?.severity)) {
|
|
83
|
+
thoughtLog("Predictive audit considered the package risky.", indicators);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
thoughtLog("Predictive audit kept the package at low risk.", indicators);
|
|
87
|
+
}
|
|
33
88
|
|
|
34
89
|
/**
|
|
35
90
|
* Clamp a number into a fixed range.
|
|
@@ -55,6 +110,74 @@ function getTargetProperty(target, propertyName) {
|
|
|
55
110
|
?.value;
|
|
56
111
|
}
|
|
57
112
|
|
|
113
|
+
function getTargetListProperty(target, propertyName) {
|
|
114
|
+
const propertyValue = getTargetProperty(target, propertyName);
|
|
115
|
+
if (!propertyValue || typeof propertyValue !== "string") {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return propertyValue
|
|
119
|
+
.split(",")
|
|
120
|
+
.map((entry) => entry.trim())
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getCargoPredictiveSignals(target) {
|
|
125
|
+
const buildScriptCapabilities = getTargetListProperty(
|
|
126
|
+
target,
|
|
127
|
+
"cdx:cargo:buildScriptCapabilities",
|
|
128
|
+
);
|
|
129
|
+
const nativeBuildIndicators = getTargetListProperty(
|
|
130
|
+
target,
|
|
131
|
+
"cdx:cargo:nativeBuildIndicators",
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
buildDependency:
|
|
135
|
+
getTargetProperty(target, "cdx:cargo:dependencyKind") === "build",
|
|
136
|
+
buildScript:
|
|
137
|
+
getTargetProperty(target, "cdx:cargo:hasBuildScript") === "true",
|
|
138
|
+
nativeBuild:
|
|
139
|
+
getTargetProperty(target, "cdx:cargo:hasNativeBuild") === "true",
|
|
140
|
+
networkCapableBuildScript:
|
|
141
|
+
buildScriptCapabilities.includes("network-access"),
|
|
142
|
+
processCapableBuildScript:
|
|
143
|
+
buildScriptCapabilities.includes("process-execution"),
|
|
144
|
+
runtimeFacing: Boolean(target?.runtimeFacingCargo),
|
|
145
|
+
workspaceDependency:
|
|
146
|
+
getTargetProperty(target, "cdx:cargo:workspaceDependencyResolved") ===
|
|
147
|
+
"true",
|
|
148
|
+
buildOnlyWorkspace: Boolean(target?.buildOnlyWorkspace),
|
|
149
|
+
yanked: getTargetProperty(target, "cdx:cargo:yanked") === "true",
|
|
150
|
+
buildScriptCapabilities,
|
|
151
|
+
nativeBuildIndicators,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Classify a finding into a coarse signal bucket for conservative scoring.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} finding predictive audit finding
|
|
159
|
+
* @returns {"ci-hygiene" | "ci-compromise" | "dependency-compromise" | "package-hygiene" | "package-compromise" | "other"} signal bucket
|
|
160
|
+
*/
|
|
161
|
+
function classifyFindingSignalBucket(finding) {
|
|
162
|
+
if (finding?.category === "ai-agent") {
|
|
163
|
+
return "package-compromise";
|
|
164
|
+
}
|
|
165
|
+
if (finding?.category === "ci-permission") {
|
|
166
|
+
return CI_HYGIENE_RULES.has(finding?.ruleId)
|
|
167
|
+
? "ci-hygiene"
|
|
168
|
+
: "ci-compromise";
|
|
169
|
+
}
|
|
170
|
+
if (finding?.category === "package-integrity") {
|
|
171
|
+
return PACKAGE_HYGIENE_RULES.has(finding?.ruleId)
|
|
172
|
+
? "package-hygiene"
|
|
173
|
+
: "package-compromise";
|
|
174
|
+
}
|
|
175
|
+
if (finding?.category === "dependency-source") {
|
|
176
|
+
return "dependency-compromise";
|
|
177
|
+
}
|
|
178
|
+
return "other";
|
|
179
|
+
}
|
|
180
|
+
|
|
58
181
|
/**
|
|
59
182
|
* Convert a numeric confidence score into a human readable label.
|
|
60
183
|
*
|
|
@@ -104,7 +227,7 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
104
227
|
(context?.scanError
|
|
105
228
|
? `Predictive audit for '${target.purl}' could not complete successfully.`
|
|
106
229
|
: `${target.type} package '${target.purl}' did not trigger any predictive audit rules.`);
|
|
107
|
-
|
|
230
|
+
const assessment = {
|
|
108
231
|
categoryCounts: {},
|
|
109
232
|
confidence: 0.35,
|
|
110
233
|
confidenceLabel: "low",
|
|
@@ -116,6 +239,8 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
116
239
|
severity: "none",
|
|
117
240
|
strongSignalCount: 0,
|
|
118
241
|
};
|
|
242
|
+
logRiskAssessmentDecision(target, assessment);
|
|
243
|
+
return assessment;
|
|
119
244
|
}
|
|
120
245
|
const categoryCounts = {};
|
|
121
246
|
const attackTactics = new Set();
|
|
@@ -124,28 +249,93 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
124
249
|
const matchedPriorityRules = new Set();
|
|
125
250
|
let score = 0;
|
|
126
251
|
let strongSignalCount = 0;
|
|
127
|
-
let
|
|
252
|
+
let ciCompromiseSignalCount = 0;
|
|
253
|
+
let ciHygieneSignalCount = 0;
|
|
128
254
|
let formulationSignalCount = 0;
|
|
255
|
+
let compromiseSignalCount = 0;
|
|
256
|
+
let packageIntegrityCompromiseSignalCount = 0;
|
|
257
|
+
let packageIntegrityHygieneSignalCount = 0;
|
|
129
258
|
let priorityCorroborationCount = 0;
|
|
259
|
+
let cargoBuildSignalCount = 0;
|
|
130
260
|
for (const finding of findings) {
|
|
131
261
|
const findingSeverity = finding?.severity || "low";
|
|
132
262
|
const findingCategory = finding?.category || "unknown";
|
|
263
|
+
const signalBucket = classifyFindingSignalBucket(finding);
|
|
133
264
|
let findingScore = BASE_FINDING_WEIGHT[findingSeverity] ?? 4;
|
|
134
265
|
findingScore += CATEGORY_WEIGHT[findingCategory] ?? 4;
|
|
266
|
+
findingScore +=
|
|
267
|
+
SIGNAL_BUCKET_WEIGHT[signalBucket] ?? SIGNAL_BUCKET_WEIGHT.other;
|
|
135
268
|
findingScore += RULE_SPECIFIC_WEIGHT[finding?.ruleId] ?? 0;
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
269
|
+
if (target?.type === "cargo") {
|
|
270
|
+
const cargoSignals = getCargoPredictiveSignals(target);
|
|
271
|
+
if (
|
|
272
|
+
["dependency-compromise", "package-compromise"].includes(
|
|
273
|
+
signalBucket,
|
|
274
|
+
) &&
|
|
275
|
+
cargoSignals.nativeBuild
|
|
276
|
+
) {
|
|
277
|
+
findingScore += 6;
|
|
278
|
+
cargoBuildSignalCount += 1;
|
|
279
|
+
}
|
|
280
|
+
if (
|
|
281
|
+
signalBucket === "package-compromise" &&
|
|
282
|
+
(cargoSignals.processCapableBuildScript ||
|
|
283
|
+
cargoSignals.networkCapableBuildScript)
|
|
284
|
+
) {
|
|
285
|
+
findingScore += 4;
|
|
286
|
+
cargoBuildSignalCount += 1;
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
signalBucket === "dependency-compromise" &&
|
|
290
|
+
(cargoSignals.buildDependency || cargoSignals.workspaceDependency)
|
|
291
|
+
) {
|
|
292
|
+
findingScore += 3;
|
|
293
|
+
cargoBuildSignalCount += 1;
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
["dependency-compromise", "package-compromise"].includes(
|
|
297
|
+
signalBucket,
|
|
298
|
+
) &&
|
|
299
|
+
cargoSignals.runtimeFacing
|
|
300
|
+
) {
|
|
301
|
+
findingScore += 2;
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
cargoSignals.buildOnlyWorkspace &&
|
|
305
|
+
!cargoSignals.runtimeFacing &&
|
|
306
|
+
signalBucket !== "ci-compromise"
|
|
307
|
+
) {
|
|
308
|
+
findingScore -= 2;
|
|
309
|
+
}
|
|
310
|
+
if (finding?.ruleId === "PROV-015" && cargoSignals.yanked) {
|
|
311
|
+
findingScore += cargoSignals.nativeBuild ? 8 : 4;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (signalBucket === "ci-hygiene") {
|
|
315
|
+
ciHygieneSignalCount += 1;
|
|
316
|
+
} else if (signalBucket === "ci-compromise") {
|
|
317
|
+
ciCompromiseSignalCount += 1;
|
|
318
|
+
} else if (signalBucket === "package-compromise") {
|
|
319
|
+
packageIntegrityCompromiseSignalCount += 1;
|
|
320
|
+
} else if (signalBucket === "package-hygiene") {
|
|
321
|
+
packageIntegrityHygieneSignalCount += 1;
|
|
139
322
|
}
|
|
140
323
|
if (
|
|
141
324
|
finding?.ruleId?.startsWith("CI-") ||
|
|
142
325
|
finding?.location?.file?.includes(".github/workflows")
|
|
143
326
|
) {
|
|
144
327
|
formulationSignalCount += 1;
|
|
145
|
-
findingScore +=
|
|
328
|
+
findingScore += signalBucket === "ci-hygiene" ? 1 : 4;
|
|
146
329
|
}
|
|
147
330
|
if (["high", "critical"].includes(findingSeverity)) {
|
|
148
331
|
strongSignalCount += 1;
|
|
332
|
+
if (
|
|
333
|
+
signalBucket === "ci-compromise" ||
|
|
334
|
+
signalBucket === "dependency-compromise" ||
|
|
335
|
+
signalBucket === "package-compromise"
|
|
336
|
+
) {
|
|
337
|
+
compromiseSignalCount += 1;
|
|
338
|
+
}
|
|
149
339
|
}
|
|
150
340
|
if (PRIORITY_CORROBORATION_RULES.has(finding?.ruleId)) {
|
|
151
341
|
priorityCorroborationCount += 1;
|
|
@@ -170,7 +360,17 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
170
360
|
}
|
|
171
361
|
score += Math.max(0, distinctCategories.size - 1) * 8;
|
|
172
362
|
score += Math.max(0, strongSignalCount - 1) * 10;
|
|
173
|
-
score += Math.max(0, formulationSignalCount - 1) *
|
|
363
|
+
score += Math.max(0, formulationSignalCount - 1) * 2;
|
|
364
|
+
if (target?.type === "cargo" && cargoBuildSignalCount > 0) {
|
|
365
|
+
score += Math.min(cargoBuildSignalCount, 3) * 3;
|
|
366
|
+
}
|
|
367
|
+
if (
|
|
368
|
+
target?.type === "cargo" &&
|
|
369
|
+
target?.buildOnlyWorkspace &&
|
|
370
|
+
!target?.runtimeFacingCargo
|
|
371
|
+
) {
|
|
372
|
+
score = Math.max(0, score - 3);
|
|
373
|
+
}
|
|
174
374
|
|
|
175
375
|
const hasTrustedPublishing = hasTrustedPublishingProperties(
|
|
176
376
|
target?.properties,
|
|
@@ -237,8 +437,8 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
237
437
|
if (
|
|
238
438
|
severity === "critical" &&
|
|
239
439
|
(effectiveStrongSignalCount < 3 ||
|
|
440
|
+
compromiseSignalCount < MIN_COMPROMISE_SIGNALS_FOR_CRITICAL ||
|
|
240
441
|
distinctCategories.size < 2 ||
|
|
241
|
-
ciSignalCount < 1 ||
|
|
242
442
|
confidence < 0.85)
|
|
243
443
|
) {
|
|
244
444
|
severity = "high";
|
|
@@ -246,6 +446,7 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
246
446
|
if (
|
|
247
447
|
severity === "high" &&
|
|
248
448
|
(effectiveStrongSignalCount < 2 ||
|
|
449
|
+
compromiseSignalCount < MIN_COMPROMISE_SIGNALS_FOR_HIGH ||
|
|
249
450
|
distinctCategories.size < 2 ||
|
|
250
451
|
confidence < 0.65)
|
|
251
452
|
) {
|
|
@@ -256,9 +457,51 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
256
457
|
}
|
|
257
458
|
|
|
258
459
|
const reasons = [];
|
|
259
|
-
if (
|
|
460
|
+
if (ciHygieneSignalCount > 0) {
|
|
461
|
+
reasons.push(
|
|
462
|
+
`${ciHygieneSignalCount} CI hygiene signal(s) were observed in GitHub Actions or privileged workflow configuration.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (ciCompromiseSignalCount > 0) {
|
|
466
|
+
reasons.push(
|
|
467
|
+
`${ciCompromiseSignalCount} compromise-oriented CI signal(s) increased the predictive risk score.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (target?.type === "cargo" && cargoBuildSignalCount > 0) {
|
|
471
|
+
const cargoSignals = getCargoPredictiveSignals(target);
|
|
472
|
+
const cargoReasonParts = [];
|
|
473
|
+
if (cargoSignals.nativeBuild) {
|
|
474
|
+
cargoReasonParts.push("native build tooling");
|
|
475
|
+
}
|
|
476
|
+
if (cargoSignals.processCapableBuildScript) {
|
|
477
|
+
cargoReasonParts.push("process-capable build scripts");
|
|
478
|
+
}
|
|
479
|
+
if (cargoSignals.networkCapableBuildScript) {
|
|
480
|
+
cargoReasonParts.push("network-capable build scripts");
|
|
481
|
+
}
|
|
482
|
+
if (cargoSignals.workspaceDependency) {
|
|
483
|
+
cargoReasonParts.push("workspace-resolved member dependencies");
|
|
484
|
+
}
|
|
485
|
+
if (cargoSignals.runtimeFacing) {
|
|
486
|
+
cargoReasonParts.push("runtime-facing crate exposure");
|
|
487
|
+
}
|
|
488
|
+
if (cargoSignals.buildOnlyWorkspace && !cargoSignals.runtimeFacing) {
|
|
489
|
+
cargoReasonParts.push("build-only workspace helper role");
|
|
490
|
+
}
|
|
491
|
+
if (cargoReasonParts.length) {
|
|
492
|
+
reasons.push(
|
|
493
|
+
`Cargo build-surface signals (${cargoReasonParts.join(", ")}) increased the predictive review priority.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (packageIntegrityCompromiseSignalCount > 0) {
|
|
260
498
|
reasons.push(
|
|
261
|
-
`${
|
|
499
|
+
`${packageIntegrityCompromiseSignalCount} package-integrity compromise signal(s) corroborated the package risk posture.`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (packageIntegrityHygieneSignalCount > 0) {
|
|
503
|
+
reasons.push(
|
|
504
|
+
`${packageIntegrityHygieneSignalCount} package-integrity hygiene signal(s) were recorded for review.`,
|
|
262
505
|
);
|
|
263
506
|
}
|
|
264
507
|
if (distinctCategories.size > 1) {
|
|
@@ -292,19 +535,25 @@ export function scoreTargetRisk(findings, target, context = {}) {
|
|
|
292
535
|
);
|
|
293
536
|
}
|
|
294
537
|
|
|
295
|
-
|
|
538
|
+
const assessment = {
|
|
296
539
|
categoryCounts,
|
|
297
540
|
attackTacticCount: attackTactics.size,
|
|
298
541
|
attackTechniqueCount: attackTechniques.size,
|
|
299
542
|
confidence,
|
|
300
543
|
confidenceLabel: confidenceLabel(confidence),
|
|
544
|
+
ciCompromiseSignalCount,
|
|
545
|
+
ciHygieneSignalCount,
|
|
301
546
|
distinctCategoryCount: distinctCategories.size,
|
|
302
547
|
findingsCount: findings.length,
|
|
303
548
|
formulationSignalCount,
|
|
549
|
+
packageIntegrityCompromiseSignalCount,
|
|
550
|
+
packageIntegrityHygieneSignalCount,
|
|
304
551
|
priorityCorroborationCount,
|
|
305
552
|
reasons,
|
|
306
553
|
score,
|
|
307
554
|
severity,
|
|
308
555
|
strongSignalCount,
|
|
309
556
|
};
|
|
557
|
+
logRiskAssessmentDecision(target, assessment);
|
|
558
|
+
return assessment;
|
|
310
559
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import esmock from "esmock";
|
|
1
2
|
import { assert, describe, it } from "poku";
|
|
3
|
+
import sinon from "sinon";
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
confidenceLabel,
|
|
@@ -270,6 +272,120 @@ describe("scoreTargetRisk()", () => {
|
|
|
270
272
|
assert.ok(withEvidence.score < withoutEvidence.score);
|
|
271
273
|
});
|
|
272
274
|
|
|
275
|
+
it("boosts predictive scoring for Cargo targets with risky build-surface signals", () => {
|
|
276
|
+
const findings = [
|
|
277
|
+
{
|
|
278
|
+
category: "dependency-source",
|
|
279
|
+
location: {
|
|
280
|
+
purl: "pkg:cargo/ring@0.17.8",
|
|
281
|
+
},
|
|
282
|
+
message: "Build dependency resolved from a mutable source",
|
|
283
|
+
ruleId: "PKG-001",
|
|
284
|
+
severity: "high",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
category: "package-integrity",
|
|
288
|
+
location: {
|
|
289
|
+
purl: "pkg:cargo/ring@0.17.8",
|
|
290
|
+
},
|
|
291
|
+
message: "Crate was yanked from the registry",
|
|
292
|
+
ruleId: "PROV-015",
|
|
293
|
+
severity: "high",
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
const cargoTarget = {
|
|
297
|
+
name: "ring",
|
|
298
|
+
purl: "pkg:cargo/ring@0.17.8",
|
|
299
|
+
properties: [
|
|
300
|
+
{ name: "cdx:cargo:dependencyKind", value: "build" },
|
|
301
|
+
{ name: "cdx:cargo:hasNativeBuild", value: "true" },
|
|
302
|
+
{
|
|
303
|
+
name: "cdx:cargo:buildScriptCapabilities",
|
|
304
|
+
value: "process-execution, network-access",
|
|
305
|
+
},
|
|
306
|
+
{ name: "cdx:cargo:workspaceDependencyResolved", value: "true" },
|
|
307
|
+
{ name: "cdx:cargo:yanked", value: "true" },
|
|
308
|
+
],
|
|
309
|
+
type: "cargo",
|
|
310
|
+
version: "0.17.8",
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const plainAssessment = scoreTargetRisk(findings, baseTarget, {
|
|
314
|
+
bomJson: {
|
|
315
|
+
formulation: [{}],
|
|
316
|
+
},
|
|
317
|
+
resolution: {
|
|
318
|
+
repoUrl: "https://github.com/example/repo",
|
|
319
|
+
},
|
|
320
|
+
sourceDirectoryConfidence: "high",
|
|
321
|
+
versionMatched: true,
|
|
322
|
+
});
|
|
323
|
+
const cargoAssessment = scoreTargetRisk(findings, cargoTarget, {
|
|
324
|
+
bomJson: {
|
|
325
|
+
formulation: [{}],
|
|
326
|
+
},
|
|
327
|
+
resolution: {
|
|
328
|
+
repoUrl: "https://github.com/example/ring",
|
|
329
|
+
},
|
|
330
|
+
sourceDirectoryConfidence: "high",
|
|
331
|
+
versionMatched: true,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
assert.ok(cargoAssessment.score > plainAssessment.score);
|
|
335
|
+
assert.match(
|
|
336
|
+
cargoAssessment.reasons.join(" "),
|
|
337
|
+
/Cargo build-surface signals/i,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("keeps stacked CI hygiene findings below critical without compromise corroboration", () => {
|
|
342
|
+
const findings = [
|
|
343
|
+
{
|
|
344
|
+
category: "ci-permission",
|
|
345
|
+
location: {
|
|
346
|
+
file: ".github/workflows/release.yml",
|
|
347
|
+
},
|
|
348
|
+
message: "Unpinned privileged action",
|
|
349
|
+
ruleId: "CI-001",
|
|
350
|
+
severity: "high",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
category: "ci-permission",
|
|
354
|
+
location: {
|
|
355
|
+
file: ".github/workflows/release.yml",
|
|
356
|
+
},
|
|
357
|
+
message: "Mutable action tag",
|
|
358
|
+
ruleId: "CI-003",
|
|
359
|
+
severity: "medium",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
category: "package-integrity",
|
|
363
|
+
location: {
|
|
364
|
+
purl: baseTarget.purl,
|
|
365
|
+
},
|
|
366
|
+
message: "Install hook present",
|
|
367
|
+
ruleId: "INT-001",
|
|
368
|
+
severity: "medium",
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const assessment = scoreTargetRisk(findings, baseTarget, {
|
|
373
|
+
bomJson: {
|
|
374
|
+
formulation: [{ workflows: [] }],
|
|
375
|
+
},
|
|
376
|
+
resolution: {
|
|
377
|
+
repoUrl: "https://github.com/example/repo",
|
|
378
|
+
},
|
|
379
|
+
sourceDirectoryConfidence: "high",
|
|
380
|
+
versionMatched: true,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
assert.strictEqual(assessment.severity, "medium");
|
|
384
|
+
assert.strictEqual(assessment.ciHygieneSignalCount, 2);
|
|
385
|
+
assert.strictEqual(assessment.packageIntegrityHygieneSignalCount, 1);
|
|
386
|
+
assert.strictEqual(assessment.packageIntegrityCompromiseSignalCount, 0);
|
|
387
|
+
});
|
|
388
|
+
|
|
273
389
|
it("keeps isolated CI-019 findings conservative despite the rule-specific bonus", () => {
|
|
274
390
|
const findings = [
|
|
275
391
|
{
|
|
@@ -338,4 +454,67 @@ describe("scoreTargetRisk()", () => {
|
|
|
338
454
|
/high-confidence compound rule/i,
|
|
339
455
|
);
|
|
340
456
|
});
|
|
457
|
+
|
|
458
|
+
it("emits a low-risk thought log when no findings are present", async () => {
|
|
459
|
+
const thoughtLog = sinon.spy();
|
|
460
|
+
const { scoreTargetRisk: scoreTargetRiskWithMock } = await esmock(
|
|
461
|
+
"./scoring.js",
|
|
462
|
+
{
|
|
463
|
+
"../helpers/logger.js": { thoughtLog },
|
|
464
|
+
},
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const assessment = scoreTargetRiskWithMock([], baseTarget);
|
|
468
|
+
|
|
469
|
+
assert.strictEqual(assessment.severity, "none");
|
|
470
|
+
assert.strictEqual(thoughtLog.calledOnce, true);
|
|
471
|
+
assert.match(thoughtLog.firstCall.args[0], /low risk/i);
|
|
472
|
+
assert.strictEqual(thoughtLog.firstCall.args[1].purl, baseTarget.purl);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("emits a risky thought log when corroborated findings raise the package severity", async () => {
|
|
476
|
+
const thoughtLog = sinon.spy();
|
|
477
|
+
const { scoreTargetRisk: scoreTargetRiskWithMock } = await esmock(
|
|
478
|
+
"./scoring.js",
|
|
479
|
+
{
|
|
480
|
+
"../helpers/logger.js": { thoughtLog },
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
const findings = [
|
|
484
|
+
{
|
|
485
|
+
category: "ci-permission",
|
|
486
|
+
location: {
|
|
487
|
+
file: ".github/workflows/release.yml",
|
|
488
|
+
},
|
|
489
|
+
message: "Unpinned privileged action",
|
|
490
|
+
ruleId: "CI-001",
|
|
491
|
+
severity: "high",
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
category: "dependency-source",
|
|
495
|
+
location: {
|
|
496
|
+
purl: baseTarget.purl,
|
|
497
|
+
},
|
|
498
|
+
message: "Install script from non-registry source",
|
|
499
|
+
ruleId: "PKG-001",
|
|
500
|
+
severity: "high",
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
const assessment = scoreTargetRiskWithMock(findings, baseTarget, {
|
|
505
|
+
bomJson: {
|
|
506
|
+
formulation: [{}],
|
|
507
|
+
},
|
|
508
|
+
resolution: {
|
|
509
|
+
repoUrl: "https://github.com/example/repo",
|
|
510
|
+
},
|
|
511
|
+
sourceDirectoryConfidence: "high",
|
|
512
|
+
versionMatched: true,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
assert.strictEqual(assessment.severity, "high");
|
|
516
|
+
assert.strictEqual(thoughtLog.calledOnce, true);
|
|
517
|
+
assert.match(thoughtLog.firstCall.args[0], /considered the package risky/i);
|
|
518
|
+
assert.strictEqual(thoughtLog.firstCall.args[1].severity, "high");
|
|
519
|
+
});
|
|
341
520
|
});
|