@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.
Files changed (121) hide show
  1. package/README.md +15 -5
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +241 -81
  4. package/bin/repl.js +138 -0
  5. package/data/rules/ai-agent-governance.yaml +249 -0
  6. package/data/rules/dependency-sources.yaml +41 -0
  7. package/data/rules/mcp-servers.yaml +304 -0
  8. package/data/rules/package-integrity.yaml +123 -0
  9. package/lib/audit/index.js +353 -29
  10. package/lib/audit/index.poku.js +247 -7
  11. package/lib/audit/reporters.js +26 -0
  12. package/lib/audit/scoring.js +262 -13
  13. package/lib/audit/scoring.poku.js +179 -0
  14. package/lib/audit/targets.js +391 -2
  15. package/lib/audit/targets.poku.js +416 -3
  16. package/lib/cli/index.js +588 -45
  17. package/lib/cli/index.poku.js +735 -1
  18. package/lib/evinser/evinser.js +8 -5
  19. package/lib/helpers/agentFormulationParser.js +318 -0
  20. package/lib/helpers/aiInventory.js +262 -0
  21. package/lib/helpers/aiInventory.poku.js +111 -0
  22. package/lib/helpers/analyzer.js +1769 -0
  23. package/lib/helpers/analyzer.poku.js +284 -3
  24. package/lib/helpers/auditCategories.js +76 -0
  25. package/lib/helpers/ciParsers/githubActions.js +140 -16
  26. package/lib/helpers/ciParsers/githubActions.poku.js +110 -0
  27. package/lib/helpers/communityAiConfigParser.js +672 -0
  28. package/lib/helpers/communityAiConfigParser.poku.js +63 -0
  29. package/lib/helpers/depsUtils.js +108 -0
  30. package/lib/helpers/depsUtils.poku.js +72 -1
  31. package/lib/helpers/display.js +325 -3
  32. package/lib/helpers/display.poku.js +301 -0
  33. package/lib/helpers/formulationParsers.js +28 -0
  34. package/lib/helpers/formulationParsers.poku.js +504 -1
  35. package/lib/helpers/jsonLike.js +102 -0
  36. package/lib/helpers/jsonLike.poku.js +34 -0
  37. package/lib/helpers/mcp.js +248 -0
  38. package/lib/helpers/mcp.poku.js +101 -0
  39. package/lib/helpers/mcpConfigParser.js +656 -0
  40. package/lib/helpers/mcpConfigParser.poku.js +126 -0
  41. package/lib/helpers/mcpDiscovery.js +84 -0
  42. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  43. package/lib/helpers/protobom.js +3 -3
  44. package/lib/helpers/provenanceUtils.js +29 -4
  45. package/lib/helpers/provenanceUtils.poku.js +29 -3
  46. package/lib/helpers/registryProvenance.js +210 -0
  47. package/lib/helpers/registryProvenance.poku.js +144 -0
  48. package/lib/helpers/rustFormulationParser.js +330 -0
  49. package/lib/helpers/source.js +21 -2
  50. package/lib/helpers/source.poku.js +38 -0
  51. package/lib/helpers/utils.js +1331 -83
  52. package/lib/helpers/utils.poku.js +599 -188
  53. package/lib/helpers/vsixutils.js +12 -4
  54. package/lib/helpers/vsixutils.poku.js +34 -0
  55. package/lib/managers/binary.js +36 -12
  56. package/lib/managers/binary.poku.js +68 -0
  57. package/lib/managers/docker.js +59 -9
  58. package/lib/managers/docker.poku.js +61 -0
  59. package/lib/managers/piptree.js +12 -7
  60. package/lib/managers/piptree.poku.js +44 -0
  61. package/lib/stages/postgen/annotator.js +2 -1
  62. package/lib/stages/postgen/annotator.poku.js +15 -0
  63. package/lib/stages/postgen/auditBom.js +20 -6
  64. package/lib/stages/postgen/auditBom.poku.js +694 -1
  65. package/lib/stages/postgen/postgen.js +262 -11
  66. package/lib/stages/postgen/postgen.poku.js +306 -2
  67. package/lib/stages/postgen/ruleEngine.js +49 -1
  68. package/lib/stages/postgen/spdxConverter.poku.js +70 -0
  69. package/lib/stages/pregen/pregen.js +6 -4
  70. package/package.json +1 -1
  71. package/types/bin/repl.d.ts.map +1 -1
  72. package/types/lib/audit/index.d.ts.map +1 -1
  73. package/types/lib/audit/reporters.d.ts.map +1 -1
  74. package/types/lib/audit/scoring.d.ts.map +1 -1
  75. package/types/lib/audit/targets.d.ts +12 -0
  76. package/types/lib/audit/targets.d.ts.map +1 -1
  77. package/types/lib/cli/index.d.ts +2 -8
  78. package/types/lib/cli/index.d.ts.map +1 -1
  79. package/types/lib/evinser/evinser.d.ts.map +1 -1
  80. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  81. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  82. package/types/lib/helpers/aiInventory.d.ts +23 -0
  83. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  84. package/types/lib/helpers/analyzer.d.ts +10 -0
  85. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  86. package/types/lib/helpers/auditCategories.d.ts +12 -0
  87. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  88. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  89. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  90. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  91. package/types/lib/helpers/depsUtils.d.ts +8 -0
  92. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  93. package/types/lib/helpers/display.d.ts +17 -1
  94. package/types/lib/helpers/display.d.ts.map +1 -1
  95. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  96. package/types/lib/helpers/jsonLike.d.ts +4 -0
  97. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  98. package/types/lib/helpers/mcp.d.ts +29 -0
  99. package/types/lib/helpers/mcp.d.ts.map +1 -0
  100. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  101. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  102. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  103. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  104. package/types/lib/helpers/provenanceUtils.d.ts +5 -3
  105. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -1
  106. package/types/lib/helpers/registryProvenance.d.ts +9 -0
  107. package/types/lib/helpers/registryProvenance.d.ts.map +1 -1
  108. package/types/lib/helpers/rustFormulationParser.d.ts +17 -0
  109. package/types/lib/helpers/rustFormulationParser.d.ts.map +1 -0
  110. package/types/lib/helpers/source.d.ts.map +1 -1
  111. package/types/lib/helpers/utils.d.ts +31 -1
  112. package/types/lib/helpers/utils.d.ts.map +1 -1
  113. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  114. package/types/lib/managers/binary.d.ts.map +1 -1
  115. package/types/lib/managers/docker.d.ts.map +1 -1
  116. package/types/lib/managers/piptree.d.ts.map +1 -1
  117. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  118. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  119. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  120. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  121. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -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
- "ci-permission": 12,
23
- "dependency-source": 8,
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
- return {
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 ciSignalCount = 0;
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 (findingCategory === "ci-permission") {
137
- ciSignalCount += 1;
138
- findingScore += 8;
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 += 8;
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) * 6;
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 (ciSignalCount > 0) {
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
- `${ciSignalCount} GitHub Actions or privileged workflow signal(s) increased the predictive risk score.`,
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
- return {
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
  });