@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,13 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import {
3
- mkdtempSync,
4
- readdirSync,
5
- readFileSync,
6
- realpathSync,
7
- rmSync,
8
- statSync,
9
- writeFileSync,
10
- } from "node:fs";
2
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
11
3
  import { dirname, join, relative, resolve } from "node:path";
12
4
  import process from "node:process";
13
5
 
@@ -34,6 +26,9 @@ import {
34
26
  getTmpDir,
35
27
  safeExistsSync,
36
28
  safeMkdirSync,
29
+ safeMkdtempSync,
30
+ safeRmSync,
31
+ safeWriteSync,
37
32
  } from "../helpers/utils.js";
38
33
  import { auditBom } from "../stages/postgen/auditBom.js";
39
34
  import { postProcess } from "../stages/postgen/postgen.js";
@@ -44,9 +39,14 @@ import {
44
39
  scoreTargetRisk,
45
40
  severityMeetsThreshold,
46
41
  } from "./scoring.js";
47
- import { collectAuditTargets, normalizePackageName } from "./targets.js";
42
+ import {
43
+ collectAuditTargets,
44
+ enrichInputBomsWithRegistryMetadata,
45
+ normalizePackageName,
46
+ } from "./targets.js";
48
47
 
49
48
  export const DEFAULT_AUDIT_CATEGORIES = [
49
+ "ai-agent",
50
50
  "ci-permission",
51
51
  "dependency-source",
52
52
  "package-integrity",
@@ -211,7 +211,7 @@ function writeTextFile(filePath, content) {
211
211
  if (!safeExistsSync(parentDir)) {
212
212
  safeMkdirSync(parentDir, { recursive: true });
213
213
  }
214
- writeFileSync(filePath, content);
214
+ safeWriteSync(filePath, content);
215
215
  }
216
216
 
217
217
  /**
@@ -979,6 +979,99 @@ export function buildTargetContextFindings(target) {
979
979
  });
980
980
  }
981
981
  }
982
+ if (target.type === "cargo") {
983
+ const yanked = getTargetProperty(target, "cdx:cargo:yanked") === "true";
984
+ const establishedPackage = isEstablishedPackage(target, "cdx:cargo");
985
+ const recentRelease = isRecentRelease(target, "cdx:cargo");
986
+ const publisherDrift = hasPublisherDrift(target, "cdx:cargo");
987
+ const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
988
+ target,
989
+ "cdx:cargo",
990
+ );
991
+ const compressedCadence = hasCompressedCadence(target, "cdx:cargo");
992
+ if (target.version && yanked) {
993
+ findings.push({
994
+ category: "package-integrity",
995
+ description:
996
+ "Yanked crates are removed from normal Cargo resolution and usually indicate a correctness, security, or publisher-action issue that deserves review before further adoption.",
997
+ location: {
998
+ bomRef: target.bomRefs?.[0],
999
+ purl: target.purl,
1000
+ },
1001
+ message: `Cargo crate '${target.name}@${target.version}' has been yanked from crates.io.`,
1002
+ mitigation:
1003
+ "Prefer a non-yanked release and review the crate's publisher history and changelog before upgrading.",
1004
+ ruleId: "PROV-015",
1005
+ severity: "high",
1006
+ });
1007
+ }
1008
+ if (
1009
+ target.version &&
1010
+ establishedPackage &&
1011
+ recentRelease &&
1012
+ !hasTrustedPublishing &&
1013
+ !hasProvenanceEvidence
1014
+ ) {
1015
+ findings.push({
1016
+ category: "package-integrity",
1017
+ description:
1018
+ "Very recent releases on established crates benefit from a short review window when trusted publishing and provenance remain weak.",
1019
+ location: {
1020
+ bomRef: target.bomRefs?.[0],
1021
+ purl: target.purl,
1022
+ },
1023
+ message: `Cargo crate '${target.name}@${target.version}' is a very recent release on an established package without registry-visible provenance signals.`,
1024
+ mitigation:
1025
+ "Delay adoption briefly, compare the release to the prior version, and prefer trusted-publishing-backed releases for sensitive crates.",
1026
+ ruleId: "PROV-016",
1027
+ severity: "low",
1028
+ });
1029
+ }
1030
+ if (
1031
+ target.version &&
1032
+ establishedPackage &&
1033
+ publisherDrift &&
1034
+ !hasTrustedPublishing &&
1035
+ !hasProvenanceEvidence
1036
+ ) {
1037
+ findings.push({
1038
+ category: "package-integrity",
1039
+ description:
1040
+ "Publisher drift on established crates is often benign, but becomes more meaningful when provenance and trusted publishing are absent.",
1041
+ location: {
1042
+ bomRef: target.bomRefs?.[0],
1043
+ purl: target.purl,
1044
+ },
1045
+ message: `Cargo crate '${target.name}@${target.version}' was published by a different identity than the prior release and lacks registry-visible provenance signals.`,
1046
+ mitigation:
1047
+ "Review the publisher transition, compare the prior release metadata, and validate ownership before upgrading sensitive crates.",
1048
+ ruleId: "PROV-017",
1049
+ severity: "medium",
1050
+ });
1051
+ }
1052
+ if (
1053
+ target.version &&
1054
+ establishedPackage &&
1055
+ (dormantReleaseGapAnomaly || compressedCadence) &&
1056
+ !hasTrustedPublishing &&
1057
+ !hasProvenanceEvidence
1058
+ ) {
1059
+ findings.push({
1060
+ category: "package-integrity",
1061
+ description:
1062
+ "Release timing anomalies on established crates are low-noise triage signals when provenance remains weak.",
1063
+ location: {
1064
+ bomRef: target.bomRefs?.[0],
1065
+ purl: target.purl,
1066
+ },
1067
+ message: `Cargo crate '${target.name}@${target.version}' shows unusual release timing and lacks registry-visible provenance signals.`,
1068
+ mitigation:
1069
+ "Review the release diff and timing versus prior versions before rapidly adopting the new crate release.",
1070
+ ruleId: "PROV-018",
1071
+ severity: "low",
1072
+ });
1073
+ }
1074
+ }
982
1075
  return findings;
983
1076
  }
984
1077
 
@@ -1034,7 +1127,7 @@ async function cloneRepositoryToDirWithRetry(repoUrl, cloneDir, gitRef) {
1034
1127
  return;
1035
1128
  } catch (error) {
1036
1129
  lastError = error;
1037
- rmSync(cloneDir, { force: true, recursive: true });
1130
+ safeRmSync(cloneDir, { force: true, recursive: true });
1038
1131
  if (!error?.retryable || attempt >= CLONE_RETRY_DELAYS_MS.length) {
1039
1132
  break;
1040
1133
  }
@@ -1062,7 +1155,9 @@ async function cloneRepositoryToDirWithRetry(repoUrl, cloneDir, gitRef) {
1062
1155
  */
1063
1156
  async function ensureCheckout(target, resolution, workspaceDir, gitRef) {
1064
1157
  if (!workspaceDir) {
1065
- const cloneDir = mkdtempSync(join(getTmpDir(), `${targetSlug(target)}-`));
1158
+ const cloneDir = safeMkdtempSync(
1159
+ join(getTmpDir(), `${targetSlug(target)}-`),
1160
+ );
1066
1161
  await cloneRepositoryToDirWithRetry(resolution.repoUrl, cloneDir, gitRef);
1067
1162
  return {
1068
1163
  cleanup: true,
@@ -1083,7 +1178,7 @@ async function ensureCheckout(target, resolution, workspaceDir, gitRef) {
1083
1178
  };
1084
1179
  }
1085
1180
  if (safeExistsSync(cloneDir)) {
1086
- rmSync(cloneDir, { force: true, recursive: true });
1181
+ safeRmSync(cloneDir, { force: true, recursive: true });
1087
1182
  }
1088
1183
  await cloneRepositoryToDirWithRetry(resolution.repoUrl, cloneDir, gitRef);
1089
1184
  return {
@@ -1354,6 +1449,14 @@ export function buildPythonSourceHeuristicFindings(scanDir, target) {
1354
1449
  * @returns {object} createBom options
1355
1450
  */
1356
1451
  function buildChildOptions(options, target) {
1452
+ const projectType =
1453
+ target.type === "npm"
1454
+ ? ["js"]
1455
+ : target.type === "pypi"
1456
+ ? ["py"]
1457
+ : target.type === "cargo"
1458
+ ? ["cargo", "github"]
1459
+ : [target.type];
1357
1460
  return {
1358
1461
  deep: true,
1359
1462
  failOnError: false,
@@ -1362,7 +1465,7 @@ function buildChildOptions(options, target) {
1362
1465
  installDeps: false,
1363
1466
  multiProject: true,
1364
1467
  profile: "threat-modeling",
1365
- projectType: [target.type === "npm" ? "js" : "py"],
1468
+ projectType,
1366
1469
  specVersion: 1.7,
1367
1470
  };
1368
1471
  }
@@ -1591,6 +1694,7 @@ export async function auditTarget(target, options) {
1591
1694
  * @returns {object} summary object
1592
1695
  */
1593
1696
  function summarizeAudit(inputBoms, results, skipped) {
1697
+ const analysisErrorCounts = {};
1594
1698
  const severityCounts = {
1595
1699
  critical: 0,
1596
1700
  high: 0,
@@ -1608,9 +1712,13 @@ function summarizeAudit(inputBoms, results, skipped) {
1608
1712
  }
1609
1713
  if (result.status === "error") {
1610
1714
  erroredTargets += 1;
1715
+ const errorType = result?.errorType || "runtime";
1716
+ analysisErrorCounts[errorType] =
1717
+ (analysisErrorCounts[errorType] || 0) + 1;
1611
1718
  }
1612
1719
  }
1613
1720
  return {
1721
+ analysisErrorCounts,
1614
1722
  erroredTargets,
1615
1723
  inputBomCount: inputBoms.length,
1616
1724
  scannedTargets,
@@ -1622,6 +1730,16 @@ function summarizeAudit(inputBoms, results, skipped) {
1622
1730
  };
1623
1731
  }
1624
1732
 
1733
+ function normalizeRepoGroupingValue(repoUrl) {
1734
+ if (!repoUrl || typeof repoUrl !== "string") {
1735
+ return undefined;
1736
+ }
1737
+ return repoUrl
1738
+ .trim()
1739
+ .replace(/\.git$/i, "")
1740
+ .toLowerCase();
1741
+ }
1742
+
1625
1743
  function preferredResult(left, right) {
1626
1744
  const leftSeverity = SEVERITY_ORDER[left?.assessment?.severity] ?? -1;
1627
1745
  const rightSeverity = SEVERITY_ORDER[right?.assessment?.severity] ?? -1;
@@ -1674,6 +1792,187 @@ function getNamespaceGroupingKey(result) {
1674
1792
  return `${result.target.namespace}|${categories.join(",")}|${ruleIds.join(",")}`;
1675
1793
  }
1676
1794
 
1795
+ function getCargoRepositoryGroupingKey(result) {
1796
+ const normalizedRepoUrl = normalizeRepoGroupingValue(result?.repoUrl);
1797
+ if (
1798
+ result?.status !== "audited" ||
1799
+ result?.target?.type !== "cargo" ||
1800
+ !normalizedRepoUrl ||
1801
+ (result?.assessment?.severity || "none") === "none" ||
1802
+ !Array.isArray(result?.findings) ||
1803
+ !result.findings.length
1804
+ ) {
1805
+ return undefined;
1806
+ }
1807
+ const categories = new Set();
1808
+ const ruleIds = new Set();
1809
+ for (const finding of result.findings) {
1810
+ if (!finding?.category || !finding?.ruleId) {
1811
+ return undefined;
1812
+ }
1813
+ if (finding.category === "ci-permission") {
1814
+ return undefined;
1815
+ }
1816
+ categories.add(finding.category);
1817
+ ruleIds.add(finding.ruleId);
1818
+ }
1819
+ if (!categories.size || !ruleIds.size) {
1820
+ return undefined;
1821
+ }
1822
+ return `${normalizedRepoUrl}|cargo|${[...categories].sort().join(",")}|${[...ruleIds].sort().join(",")}`;
1823
+ }
1824
+
1825
+ function getSharedRepoCiGroupingKey(result) {
1826
+ const normalizedRepoUrl = normalizeRepoGroupingValue(result?.repoUrl);
1827
+ if (
1828
+ result?.status !== "audited" ||
1829
+ !normalizedRepoUrl ||
1830
+ !Array.isArray(result?.findings) ||
1831
+ !result.findings.length
1832
+ ) {
1833
+ return undefined;
1834
+ }
1835
+ const categories = new Set();
1836
+ const ruleIds = new Set();
1837
+ for (const finding of result.findings) {
1838
+ const category = finding?.category;
1839
+ const ruleId = finding?.ruleId;
1840
+ const findingFile = finding?.location?.file || "";
1841
+ if (
1842
+ category !== "ci-permission" ||
1843
+ !ruleId?.startsWith("CI-") ||
1844
+ (findingFile && !findingFile.includes(".github/workflows"))
1845
+ ) {
1846
+ return undefined;
1847
+ }
1848
+ categories.add(category);
1849
+ ruleIds.add(ruleId);
1850
+ }
1851
+ return `${normalizedRepoUrl}|${[...categories].sort().join(",")}|${[...ruleIds].sort().join(",")}`;
1852
+ }
1853
+
1854
+ function getGroupingDescriptor(result, sharedRepoCiGroupCounts) {
1855
+ const sharedRepoCiKey = getSharedRepoCiGroupingKey(result);
1856
+ if (sharedRepoCiKey) {
1857
+ const groupSize = sharedRepoCiGroupCounts?.get(sharedRepoCiKey) || 0;
1858
+ if (groupSize > 1) {
1859
+ return {
1860
+ key: sharedRepoCiKey,
1861
+ kind: "shared-repo-ci",
1862
+ };
1863
+ }
1864
+ }
1865
+ const cargoRepositoryKey = getCargoRepositoryGroupingKey(result);
1866
+ if (cargoRepositoryKey) {
1867
+ return {
1868
+ key: cargoRepositoryKey,
1869
+ kind: "cargo-repository",
1870
+ };
1871
+ }
1872
+ const namespaceKey = getNamespaceGroupingKey(result);
1873
+ if (!namespaceKey) {
1874
+ return undefined;
1875
+ }
1876
+ return {
1877
+ key: namespaceKey,
1878
+ kind: "npm-namespace",
1879
+ };
1880
+ }
1881
+
1882
+ function consolidateCargoRepositoryResult(group) {
1883
+ const representative = group.reduce((best, result) =>
1884
+ preferredResult(best, result),
1885
+ );
1886
+ const allBomRefs = [
1887
+ ...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
1888
+ ];
1889
+ const groupedPurls = [
1890
+ ...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
1891
+ ];
1892
+ const mergedFindings = dedupeFindings(
1893
+ group.flatMap((result) => result.findings || []),
1894
+ );
1895
+ const categoryCounts = {};
1896
+ for (const result of group) {
1897
+ for (const [category, count] of Object.entries(
1898
+ result.assessment?.categoryCounts || {},
1899
+ )) {
1900
+ categoryCounts[category] = (categoryCounts[category] || 0) + count;
1901
+ }
1902
+ }
1903
+ const reasons = [
1904
+ `${group.length} Cargo packages resolved to the same repository and shared the same predictive pattern, so cdx-audit consolidated them into one alert.`,
1905
+ ...(representative.assessment?.reasons || []),
1906
+ ];
1907
+ return {
1908
+ ...representative,
1909
+ assessment: {
1910
+ ...representative.assessment,
1911
+ categoryCounts,
1912
+ findingsCount: mergedFindings.length,
1913
+ reasons: [...new Set(reasons)],
1914
+ },
1915
+ findings: mergedFindings,
1916
+ grouping: {
1917
+ kind: "cargo-repository",
1918
+ label: `cargo:${representative.repoUrl}`,
1919
+ memberCount: group.length,
1920
+ repoUrl: representative.repoUrl,
1921
+ groupedPurls,
1922
+ },
1923
+ target: {
1924
+ ...representative.target,
1925
+ bomRefs: allBomRefs,
1926
+ name: "*",
1927
+ version: undefined,
1928
+ },
1929
+ };
1930
+ }
1931
+
1932
+ function consolidateSharedRepoCiResult(group) {
1933
+ const representative = group.reduce((best, result) =>
1934
+ preferredResult(best, result),
1935
+ );
1936
+ const allBomRefs = [
1937
+ ...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
1938
+ ];
1939
+ const groupedPurls = [
1940
+ ...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
1941
+ ];
1942
+ const mergedFindings = dedupeFindings(
1943
+ group.flatMap((result) => result.findings || []),
1944
+ );
1945
+ const reasons = [
1946
+ `${group.length} packages resolved to the same repository and shared the same CI findings, so cdx-audit consolidated them into one alert.`,
1947
+ ...(representative.assessment?.reasons || []),
1948
+ ];
1949
+ return {
1950
+ ...representative,
1951
+ assessment: {
1952
+ ...representative.assessment,
1953
+ categoryCounts: {
1954
+ "ci-permission": mergedFindings.length,
1955
+ },
1956
+ findingsCount: mergedFindings.length,
1957
+ reasons: [...new Set(reasons)],
1958
+ },
1959
+ findings: mergedFindings,
1960
+ grouping: {
1961
+ kind: "shared-repo-ci",
1962
+ label: representative.repoUrl,
1963
+ memberCount: group.length,
1964
+ repoUrl: representative.repoUrl,
1965
+ groupedPurls,
1966
+ },
1967
+ target: {
1968
+ ...representative.target,
1969
+ bomRefs: allBomRefs,
1970
+ name: "*",
1971
+ version: undefined,
1972
+ },
1973
+ };
1974
+ }
1975
+
1677
1976
  function consolidateNamespaceResult(group) {
1678
1977
  const representative = group.reduce((best, result) =>
1679
1978
  preferredResult(best, result),
@@ -1727,27 +2026,46 @@ function consolidateNamespaceResult(group) {
1727
2026
  export function groupAuditResults(results) {
1728
2027
  const groupedResults = [];
1729
2028
  const orderedEntries = [];
1730
- const namespaceGroups = new Map();
2029
+ const resultGroups = new Map();
2030
+ const sharedRepoCiGroupCounts = new Map();
2031
+ for (const result of results) {
2032
+ const sharedRepoCiKey = getSharedRepoCiGroupingKey(result);
2033
+ if (!sharedRepoCiKey) {
2034
+ continue;
2035
+ }
2036
+ sharedRepoCiGroupCounts.set(
2037
+ sharedRepoCiKey,
2038
+ (sharedRepoCiGroupCounts.get(sharedRepoCiKey) || 0) + 1,
2039
+ );
2040
+ }
1731
2041
  for (const result of results) {
1732
- const groupKey = getNamespaceGroupingKey(result);
1733
- if (!groupKey) {
2042
+ const descriptor = getGroupingDescriptor(result, sharedRepoCiGroupCounts);
2043
+ if (!descriptor) {
1734
2044
  orderedEntries.push({ result, type: "single" });
1735
2045
  continue;
1736
2046
  }
1737
- if (!namespaceGroups.has(groupKey)) {
1738
- namespaceGroups.set(groupKey, []);
1739
- orderedEntries.push({ groupKey, type: "group" });
2047
+ if (!resultGroups.has(descriptor.key)) {
2048
+ resultGroups.set(descriptor.key, []);
2049
+ orderedEntries.push({ descriptor, type: "group" });
1740
2050
  }
1741
- namespaceGroups.get(groupKey).push(result);
2051
+ resultGroups.get(descriptor.key).push(result);
1742
2052
  }
1743
2053
  for (const entry of orderedEntries) {
1744
2054
  if (entry.type === "single") {
1745
2055
  groupedResults.push(entry.result);
1746
2056
  continue;
1747
2057
  }
1748
- const group = namespaceGroups.get(entry.groupKey) || [];
2058
+ const group = resultGroups.get(entry.descriptor.key) || [];
2059
+ if (group.length <= 1) {
2060
+ groupedResults.push(group[0]);
2061
+ continue;
2062
+ }
1749
2063
  groupedResults.push(
1750
- group.length > 1 ? consolidateNamespaceResult(group) : group[0],
2064
+ entry.descriptor.kind === "shared-repo-ci"
2065
+ ? consolidateSharedRepoCiResult(group)
2066
+ : entry.descriptor.kind === "cargo-repository"
2067
+ ? consolidateCargoRepositoryResult(group)
2068
+ : consolidateNamespaceResult(group),
1751
2069
  );
1752
2070
  }
1753
2071
  return groupedResults;
@@ -1782,8 +2100,12 @@ export async function runAuditFromBoms(inputBoms, options) {
1782
2100
  if (!inputBoms.length) {
1783
2101
  throw new Error("No CycloneDX BOM inputs were found.");
1784
2102
  }
2103
+ if (options.trusted !== "include") {
2104
+ await enrichInputBomsWithRegistryMetadata(inputBoms);
2105
+ }
1785
2106
  const extractedTargets = collectAuditTargets(inputBoms, {
1786
2107
  maxTargets: options.maxTargets,
2108
+ prioritizeDirectRuntime: options.prioritizeDirectRuntime,
1787
2109
  scope: options.scope,
1788
2110
  trusted: options.trusted,
1789
2111
  });
@@ -1895,11 +2217,13 @@ export function finalizeAuditReport(report, options) {
1895
2217
  const effectiveResults = report.groupedResults?.length
1896
2218
  ? report.groupedResults
1897
2219
  : report.results;
1898
- const shouldFail = effectiveResults.some((result) =>
1899
- severityMeetsThreshold(
1900
- result?.assessment?.severity || "none",
1901
- options.failSeverity || "high",
1902
- ),
2220
+ const shouldFail = effectiveResults.some(
2221
+ (result) =>
2222
+ !result?.error &&
2223
+ severityMeetsThreshold(
2224
+ result?.assessment?.severity || "none",
2225
+ options.failSeverity || "high",
2226
+ ),
1903
2227
  );
1904
2228
  return {
1905
2229
  exitCode: shouldFail ? 3 : 0,