@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/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1733
|
-
if (!
|
|
2042
|
+
const descriptor = getGroupingDescriptor(result, sharedRepoCiGroupCounts);
|
|
2043
|
+
if (!descriptor) {
|
|
1734
2044
|
orderedEntries.push({ result, type: "single" });
|
|
1735
2045
|
continue;
|
|
1736
2046
|
}
|
|
1737
|
-
if (!
|
|
1738
|
-
|
|
1739
|
-
orderedEntries.push({
|
|
2047
|
+
if (!resultGroups.has(descriptor.key)) {
|
|
2048
|
+
resultGroups.set(descriptor.key, []);
|
|
2049
|
+
orderedEntries.push({ descriptor, type: "group" });
|
|
1740
2050
|
}
|
|
1741
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
1899
|
-
|
|
1900
|
-
result?.
|
|
1901
|
-
|
|
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,
|