@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import esmock from "esmock";
|
|
2
|
+
import { assert, describe, it } from "poku";
|
|
3
|
+
import sinon from "sinon";
|
|
4
|
+
|
|
5
|
+
function getProp(obj, name) {
|
|
6
|
+
return obj?.properties?.find((property) => property.name === name)?.value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("mcpConfigParser", () => {
|
|
10
|
+
it("normalizes Windows paths for config format detection and treats jsonc as json", async () => {
|
|
11
|
+
const readFileSync = sinon.stub();
|
|
12
|
+
const scanTextForHiddenUnicode = sinon.stub().returns({
|
|
13
|
+
hasHiddenUnicode: false,
|
|
14
|
+
});
|
|
15
|
+
readFileSync.withArgs("C:\\repo\\.vscode\\mcp.json", "utf-8").returns(
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
mcpServers: {
|
|
18
|
+
localDocs: {
|
|
19
|
+
transport: "streamable-http",
|
|
20
|
+
url: "https://docs.example.com/mcp",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
readFileSync.withArgs("C:\\repo\\opencode.jsonc", "utf-8").returns(`{
|
|
26
|
+
// JSONC config
|
|
27
|
+
"mcp": {
|
|
28
|
+
"remoteDocs": {
|
|
29
|
+
"type": "remote",
|
|
30
|
+
"url": "https://example.com/mcp"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}`);
|
|
34
|
+
const { mcpConfigParser } = await esmock("./mcpConfigParser.js", {
|
|
35
|
+
"node:fs": { readFileSync },
|
|
36
|
+
"./unicodeScan.js": { scanTextForHiddenUnicode },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = mcpConfigParser.parse([
|
|
40
|
+
"C:\\repo\\.vscode\\mcp.json",
|
|
41
|
+
"C:\\repo\\opencode.jsonc",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
assert.ok(
|
|
45
|
+
result.components.some(
|
|
46
|
+
(component) => getProp(component, "cdx:mcp:configFormat") === "vscode",
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
assert.ok(
|
|
50
|
+
result.components.some(
|
|
51
|
+
(component) =>
|
|
52
|
+
getProp(component, "cdx:mcp:configFormat") === "opencode",
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
sinon.assert.calledWithMatch(scanTextForHiddenUnicode, sinon.match.string, {
|
|
56
|
+
syntax: "json",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("records credential exposure without embedding raw secret metadata", async () => {
|
|
61
|
+
const readFileSync = sinon.stub();
|
|
62
|
+
const scanTextForHiddenUnicode = sinon.stub().returns({
|
|
63
|
+
hasHiddenUnicode: false,
|
|
64
|
+
});
|
|
65
|
+
readFileSync.withArgs("/repo/.vscode/mcp.json", "utf-8").returns(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
mcpServers: {
|
|
68
|
+
releaseDocs: {
|
|
69
|
+
args: [
|
|
70
|
+
"--token",
|
|
71
|
+
"sk_test_super_secret_value",
|
|
72
|
+
"https://docs.example.com/mcp",
|
|
73
|
+
],
|
|
74
|
+
command: "npx",
|
|
75
|
+
env: {
|
|
76
|
+
API_KEY: "$" + "{API_KEY}",
|
|
77
|
+
},
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: "Bearer sk_test_another_secret_value",
|
|
80
|
+
},
|
|
81
|
+
transport: "http",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
const { mcpConfigParser } = await esmock("./mcpConfigParser.js", {
|
|
87
|
+
"node:fs": { readFileSync },
|
|
88
|
+
"./unicodeScan.js": { scanTextForHiddenUnicode },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = mcpConfigParser.parse(["/repo/.vscode/mcp.json"]);
|
|
92
|
+
const service = result.services[0];
|
|
93
|
+
const component = result.components[0];
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(getProp(service, "cdx:mcp:credentialExposure"), "true");
|
|
96
|
+
assert.strictEqual(
|
|
97
|
+
getProp(service, "cdx:mcp:credentialIndicatorCount"),
|
|
98
|
+
"3",
|
|
99
|
+
);
|
|
100
|
+
assert.strictEqual(
|
|
101
|
+
getProp(service, "cdx:mcp:credentialExposureFieldCount"),
|
|
102
|
+
"3",
|
|
103
|
+
);
|
|
104
|
+
assert.strictEqual(
|
|
105
|
+
getProp(service, "cdx:mcp:credentialReferenceCount"),
|
|
106
|
+
"1",
|
|
107
|
+
);
|
|
108
|
+
assert.strictEqual(
|
|
109
|
+
getProp(component, "cdx:mcp:credentialExposedServiceCount"),
|
|
110
|
+
"1",
|
|
111
|
+
);
|
|
112
|
+
assert.strictEqual(
|
|
113
|
+
getProp(service, "cdx:mcp:credentialRiskIndicators"),
|
|
114
|
+
undefined,
|
|
115
|
+
);
|
|
116
|
+
assert.strictEqual(
|
|
117
|
+
getProp(service, "cdx:mcp:credentialExposureFields"),
|
|
118
|
+
undefined,
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(getProp(service, "cdx:mcp:credentialRefs"), undefined);
|
|
121
|
+
assert.strictEqual(
|
|
122
|
+
getProp(component, "cdx:mcp:credentialExposedServices"),
|
|
123
|
+
undefined,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const PROVIDER_TEXT_PATTERNS = [
|
|
2
|
+
["anthropic", /\banthropic\b|claude/i],
|
|
3
|
+
["openai", /\bopenai\b|\bgpt-[a-z0-9-]+\b|\bo[13]\b/i],
|
|
4
|
+
["google", /\bgemini\b|google(?:\s+ai)?/i],
|
|
5
|
+
["mistral", /\bmistral\b/i],
|
|
6
|
+
["deepseek", /\bdeepseek\b/i],
|
|
7
|
+
["ollama", /\bollama\b/i],
|
|
8
|
+
["groq", /\bgroq\b/i],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const INLINE_CREDENTIAL_PATTERNS = [
|
|
12
|
+
["aws-access-key", /\bAKIA[0-9A-Z]{16}\b/u],
|
|
13
|
+
["bearer-token", /\bbearer\s+[a-z0-9._-]{16,}\b/iu],
|
|
14
|
+
["generic-secret", /\b(?:sk|rk|pk)_[a-z0-9_-]{8,}\b/iu],
|
|
15
|
+
["github-token", /\bgh[pousr]_[a-z0-9]{20,}\b/iu],
|
|
16
|
+
["google-api-key", /\bAIza[0-9A-Za-z_-]{20,}\b/u],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function sanitizeMcpRefToken(value) {
|
|
20
|
+
const input = String(value || "")
|
|
21
|
+
.normalize("NFKC")
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
const normalized = input
|
|
25
|
+
.replaceAll(/[/\\:]/gu, "-")
|
|
26
|
+
.replaceAll(/[^a-z0-9._-]+/gu, "-")
|
|
27
|
+
.replaceAll(/[._-]{2,}/gu, "-")
|
|
28
|
+
.replaceAll(/^\.+|\.+$/gu, "")
|
|
29
|
+
.replaceAll(/^[._-]+|[._-]+$/gu, "");
|
|
30
|
+
if (!normalized || normalized === "." || normalized === "..") {
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
33
|
+
return normalized.slice(0, 128);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isLocalHost(hostname) {
|
|
37
|
+
const normalized = String(hostname || "").toLowerCase();
|
|
38
|
+
if (
|
|
39
|
+
!normalized ||
|
|
40
|
+
normalized === "localhost" ||
|
|
41
|
+
normalized === "127.0.0.1" ||
|
|
42
|
+
normalized === "::1"
|
|
43
|
+
) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
normalized.startsWith("10.") ||
|
|
48
|
+
normalized.startsWith("127.") ||
|
|
49
|
+
normalized.startsWith("169.254.") ||
|
|
50
|
+
normalized.startsWith("192.168.")
|
|
51
|
+
) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const octets = normalized.split(".");
|
|
55
|
+
if (
|
|
56
|
+
octets.length === 4 &&
|
|
57
|
+
octets[0] === "172" &&
|
|
58
|
+
Number(octets[1]) >= 16 &&
|
|
59
|
+
Number(octets[1]) <= 31
|
|
60
|
+
) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function providerNamesForText(text) {
|
|
67
|
+
return [
|
|
68
|
+
...new Set(
|
|
69
|
+
PROVIDER_TEXT_PATTERNS.flatMap(([name, pattern]) =>
|
|
70
|
+
pattern.test(text) ? [name] : [],
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function credentialIndicatorsForText(text) {
|
|
77
|
+
return [
|
|
78
|
+
...new Set(
|
|
79
|
+
INLINE_CREDENTIAL_PATTERNS.flatMap(([name, pattern]) =>
|
|
80
|
+
pattern.test(text) ? [name] : [],
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { sanitizeMcpRefToken } from "./mcpDiscovery.js";
|
|
4
|
+
|
|
5
|
+
describe("sanitizeMcpRefToken()", () => {
|
|
6
|
+
it("normalizes path traversal and punctuation-heavy input into safe tokens", () => {
|
|
7
|
+
assert.strictEqual(
|
|
8
|
+
sanitizeMcpRefToken("../Secrets/Prod Token"),
|
|
9
|
+
"secrets-prod-token",
|
|
10
|
+
);
|
|
11
|
+
assert.strictEqual(
|
|
12
|
+
sanitizeMcpRefToken("..\\..\\etc\\passwd"),
|
|
13
|
+
"etc-passwd",
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns unknown for empty or separator-only input", () => {
|
|
18
|
+
assert.strictEqual(sanitizeMcpRefToken("..."), "unknown");
|
|
19
|
+
assert.strictEqual(sanitizeMcpRefToken("///"), "unknown");
|
|
20
|
+
});
|
|
21
|
+
});
|
package/lib/helpers/protobom.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
3
|
import { cdx_16, cdx_17 } from "@appthreat/cdx-proto";
|
|
4
4
|
import {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
toJson,
|
|
9
9
|
} from "@bufbuild/protobuf";
|
|
10
10
|
|
|
11
|
-
import { safeExistsSync } from "./utils.js";
|
|
11
|
+
import { safeExistsSync, safeWriteSync } from "./utils.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Stringify the given bom json based on the type.
|
|
@@ -37,7 +37,7 @@ export const writeBinary = (bomJson, binFile) => {
|
|
|
37
37
|
} else {
|
|
38
38
|
bomSchema = cdx_16.BomSchema;
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
safeWriteSync(
|
|
41
41
|
binFile,
|
|
42
42
|
toBinary(
|
|
43
43
|
bomSchema,
|
|
@@ -2,6 +2,8 @@ const NPM_PROVENANCE_URL_PROPERTY = "cdx:npm:provenanceUrl";
|
|
|
2
2
|
const NPM_TRUSTED_PUBLISHING_PROPERTY = "cdx:npm:trustedPublishing";
|
|
3
3
|
const PYPI_PROVENANCE_URL_PROPERTY = "cdx:pypi:provenanceUrl";
|
|
4
4
|
const PYPI_TRUSTED_PUBLISHING_PROPERTY = "cdx:pypi:trustedPublishing";
|
|
5
|
+
const CARGO_PROVENANCE_URL_PROPERTY = "cdx:cargo:provenanceUrl";
|
|
6
|
+
const CARGO_TRUSTED_PUBLISHING_PROPERTY = "cdx:cargo:trustedPublishing";
|
|
5
7
|
|
|
6
8
|
export const NPM_PROVENANCE_EVIDENCE_PROPERTIES = [
|
|
7
9
|
NPM_PROVENANCE_URL_PROPERTY,
|
|
@@ -22,13 +24,23 @@ export const PYPI_PROVENANCE_EVIDENCE_PROPERTIES = [
|
|
|
22
24
|
"cdx:pypi:artifactDigestBlake2b256",
|
|
23
25
|
"cdx:pypi:artifactDigestMd5",
|
|
24
26
|
];
|
|
27
|
+
export const CARGO_PROVENANCE_EVIDENCE_PROPERTIES = [
|
|
28
|
+
CARGO_PROVENANCE_URL_PROPERTY,
|
|
29
|
+
"cdx:cargo:provenanceDigest",
|
|
30
|
+
"cdx:cargo:provenanceKeyId",
|
|
31
|
+
"cdx:cargo:provenancePredicateType",
|
|
32
|
+
"cdx:cargo:provenanceSignature",
|
|
33
|
+
"cdx:cargo:artifactDigestSha256",
|
|
34
|
+
];
|
|
25
35
|
export const REGISTRY_PROVENANCE_EVIDENCE_PROPERTIES = [
|
|
26
36
|
...NPM_PROVENANCE_EVIDENCE_PROPERTIES,
|
|
27
37
|
...PYPI_PROVENANCE_EVIDENCE_PROPERTIES,
|
|
38
|
+
...CARGO_PROVENANCE_EVIDENCE_PROPERTIES,
|
|
28
39
|
];
|
|
29
40
|
export const TRUSTED_PUBLISHING_PROPERTIES = [
|
|
30
41
|
NPM_TRUSTED_PUBLISHING_PROPERTY,
|
|
31
42
|
PYPI_TRUSTED_PUBLISHING_PROPERTY,
|
|
43
|
+
CARGO_TRUSTED_PUBLISHING_PROPERTY,
|
|
32
44
|
];
|
|
33
45
|
|
|
34
46
|
export const REGISTRY_PROVENANCE_ICON = "🛡";
|
|
@@ -73,7 +85,7 @@ export function hasAnyPropertyValue(properties, propertyNames) {
|
|
|
73
85
|
* Determine whether a raw properties array includes trusted publishing metadata.
|
|
74
86
|
*
|
|
75
87
|
* @param {object[]} properties CycloneDX properties array
|
|
76
|
-
* @returns {boolean} True when trusted publishing is recorded for npm or
|
|
88
|
+
* @returns {boolean} True when trusted publishing is recorded for npm, PyPI, or Cargo
|
|
77
89
|
*/
|
|
78
90
|
export function hasTrustedPublishingProperties(properties) {
|
|
79
91
|
return TRUSTED_PUBLISHING_PROPERTIES.some(
|
|
@@ -98,7 +110,7 @@ export function hasRegistryProvenanceEvidenceProperties(properties) {
|
|
|
98
110
|
* Determine whether a component includes trusted publishing metadata.
|
|
99
111
|
*
|
|
100
112
|
* @param {object} component CycloneDX component
|
|
101
|
-
* @returns {boolean} True when trusted publishing is recorded for npm or
|
|
113
|
+
* @returns {boolean} True when trusted publishing is recorded for npm, PyPI, or Cargo
|
|
102
114
|
*/
|
|
103
115
|
export function hasComponentTrustedPublishing(component) {
|
|
104
116
|
return hasTrustedPublishingProperties(component?.properties);
|
|
@@ -161,10 +173,11 @@ export function getProvenanceComponents(components) {
|
|
|
161
173
|
* Count components with trusted publishing metadata by registry ecosystem.
|
|
162
174
|
*
|
|
163
175
|
* @param {object[]} components BOM components
|
|
164
|
-
* @returns {{npm: number, pypi: number, total: number}} Trusted publishing counts
|
|
176
|
+
* @returns {{cargo: number, npm: number, pypi: number, total: number}} Trusted publishing counts
|
|
165
177
|
*/
|
|
166
178
|
export function getTrustedPublishingComponentCounts(components) {
|
|
167
179
|
const counts = {
|
|
180
|
+
cargo: 0,
|
|
168
181
|
npm: 0,
|
|
169
182
|
pypi: 0,
|
|
170
183
|
total: 0,
|
|
@@ -179,13 +192,25 @@ export function getTrustedPublishingComponentCounts(components) {
|
|
|
179
192
|
const pypiTrustedPublishing =
|
|
180
193
|
getComponentPropertyValue(component, PYPI_TRUSTED_PUBLISHING_PROPERTY) ===
|
|
181
194
|
"true";
|
|
195
|
+
const cargoTrustedPublishing =
|
|
196
|
+
getComponentPropertyValue(
|
|
197
|
+
component,
|
|
198
|
+
CARGO_TRUSTED_PUBLISHING_PROPERTY,
|
|
199
|
+
) === "true";
|
|
182
200
|
if (npmTrustedPublishing) {
|
|
183
201
|
counts.npm += 1;
|
|
184
202
|
}
|
|
185
203
|
if (pypiTrustedPublishing) {
|
|
186
204
|
counts.pypi += 1;
|
|
187
205
|
}
|
|
188
|
-
if (
|
|
206
|
+
if (cargoTrustedPublishing) {
|
|
207
|
+
counts.cargo += 1;
|
|
208
|
+
}
|
|
209
|
+
if (
|
|
210
|
+
npmTrustedPublishing ||
|
|
211
|
+
pypiTrustedPublishing ||
|
|
212
|
+
cargoTrustedPublishing
|
|
213
|
+
) {
|
|
189
214
|
counts.total += 1;
|
|
190
215
|
}
|
|
191
216
|
}
|
|
@@ -32,6 +32,19 @@ describe("provenanceUtils", () => {
|
|
|
32
32
|
},
|
|
33
33
|
],
|
|
34
34
|
};
|
|
35
|
+
const cargoTrustedComponent = {
|
|
36
|
+
name: "serde",
|
|
37
|
+
properties: [
|
|
38
|
+
{
|
|
39
|
+
name: "cdx:cargo:trustedPublishing",
|
|
40
|
+
value: "true",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "cdx:cargo:provenanceUrl",
|
|
44
|
+
value: "https://crates.io/provenance/serde/1.0.0",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
35
48
|
const plainComponent = {
|
|
36
49
|
name: "lodash",
|
|
37
50
|
properties: [],
|
|
@@ -58,17 +71,19 @@ describe("provenanceUtils", () => {
|
|
|
58
71
|
getTrustedComponents([
|
|
59
72
|
plainComponent,
|
|
60
73
|
npmTrustedComponent,
|
|
74
|
+
cargoTrustedComponent,
|
|
61
75
|
pypiProvenanceComponent,
|
|
62
76
|
]).map((component) => component.name),
|
|
63
|
-
["left-pad"],
|
|
77
|
+
["left-pad", "serde"],
|
|
64
78
|
);
|
|
65
79
|
assert.deepStrictEqual(
|
|
66
80
|
getProvenanceComponents([
|
|
67
81
|
plainComponent,
|
|
68
82
|
npmTrustedComponent,
|
|
83
|
+
cargoTrustedComponent,
|
|
69
84
|
pypiProvenanceComponent,
|
|
70
85
|
]).map((component) => component.name),
|
|
71
|
-
["requests"],
|
|
86
|
+
["serde", "requests"],
|
|
72
87
|
);
|
|
73
88
|
assert.deepStrictEqual(
|
|
74
89
|
getTrustedPublishingComponentCounts([
|
|
@@ -82,12 +97,14 @@ describe("provenanceUtils", () => {
|
|
|
82
97
|
},
|
|
83
98
|
],
|
|
84
99
|
},
|
|
100
|
+
cargoTrustedComponent,
|
|
85
101
|
plainComponent,
|
|
86
102
|
]),
|
|
87
103
|
{
|
|
104
|
+
cargo: 1,
|
|
88
105
|
npm: 1,
|
|
89
106
|
pypi: 1,
|
|
90
|
-
total:
|
|
107
|
+
total: 3,
|
|
91
108
|
},
|
|
92
109
|
);
|
|
93
110
|
});
|
|
@@ -102,6 +119,10 @@ describe("provenanceUtils", () => {
|
|
|
102
119
|
name: "cdx:npm:trustedPublishing",
|
|
103
120
|
value: "true",
|
|
104
121
|
},
|
|
122
|
+
{
|
|
123
|
+
name: "cdx:cargo:artifactDigestSha256",
|
|
124
|
+
value: "deadbeef",
|
|
125
|
+
},
|
|
105
126
|
];
|
|
106
127
|
assert.strictEqual(
|
|
107
128
|
getPropertyValue(properties, "cdx:npm:provenanceKeyId"),
|
|
@@ -132,10 +153,15 @@ describe("provenanceUtils", () => {
|
|
|
132
153
|
name: "cdx:pypi:trustedPublishing",
|
|
133
154
|
value: "true",
|
|
134
155
|
},
|
|
156
|
+
{
|
|
157
|
+
name: "cdx:cargo:trustedPublishing",
|
|
158
|
+
value: "true",
|
|
159
|
+
},
|
|
135
160
|
],
|
|
136
161
|
},
|
|
137
162
|
]),
|
|
138
163
|
{
|
|
164
|
+
cargo: 1,
|
|
139
165
|
npm: 1,
|
|
140
166
|
pypi: 1,
|
|
141
167
|
total: 1,
|
|
@@ -791,3 +791,213 @@ export function collectPypiRegistryProvenanceProperties(projectBody, version) {
|
|
|
791
791
|
);
|
|
792
792
|
return properties;
|
|
793
793
|
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Extract Cargo/crates.io release, publisher, and provenance-adjacent properties.
|
|
797
|
+
*
|
|
798
|
+
* @param {object} crateBody crates.io `/api/v1/crates/{name}` response body
|
|
799
|
+
* @param {string | undefined} version crate version
|
|
800
|
+
* @param {object} [ownersBody] crates.io `/api/v1/crates/{name}/owners` response body
|
|
801
|
+
* @returns {object[]} custom properties
|
|
802
|
+
*/
|
|
803
|
+
export function collectCargoRegistryProvenanceProperties(
|
|
804
|
+
crateBody,
|
|
805
|
+
version,
|
|
806
|
+
ownersBody,
|
|
807
|
+
) {
|
|
808
|
+
const properties = [];
|
|
809
|
+
const versions = Array.isArray(crateBody?.versions) ? crateBody.versions : [];
|
|
810
|
+
const currentVersionBody =
|
|
811
|
+
versions.find((entry) => entry?.num === version) || versions[0];
|
|
812
|
+
if (!currentVersionBody) {
|
|
813
|
+
return properties;
|
|
814
|
+
}
|
|
815
|
+
const releaseEntries = versions
|
|
816
|
+
.map((entry) => ({
|
|
817
|
+
publishers: uniqueStrings([
|
|
818
|
+
entry?.published_by?.login,
|
|
819
|
+
entry?.published_by?.name,
|
|
820
|
+
]),
|
|
821
|
+
timestamp: parseTimestamp(entry?.created_at || entry?.updated_at),
|
|
822
|
+
version: entry?.num,
|
|
823
|
+
rawTime: entry?.created_at || entry?.updated_at,
|
|
824
|
+
}))
|
|
825
|
+
.filter((entry) => entry.version && entry.timestamp !== undefined);
|
|
826
|
+
const currentPublishTimestamp = parseTimestamp(
|
|
827
|
+
currentVersionBody?.created_at || currentVersionBody?.updated_at,
|
|
828
|
+
);
|
|
829
|
+
const priorReleaseEntry = sortReleaseEntries(
|
|
830
|
+
releaseEntries.filter(
|
|
831
|
+
(entry) =>
|
|
832
|
+
entry.version !== currentVersionBody?.num &&
|
|
833
|
+
currentPublishTimestamp !== undefined &&
|
|
834
|
+
entry.timestamp < currentPublishTimestamp,
|
|
835
|
+
),
|
|
836
|
+
).pop();
|
|
837
|
+
const gapMetrics = releaseGapMetrics(releaseEntries, currentVersionBody?.num);
|
|
838
|
+
const cadenceMetrics = compressedCadenceMetrics(gapMetrics);
|
|
839
|
+
const currentPublisherSet = uniqueIdentities([
|
|
840
|
+
currentVersionBody?.published_by?.login,
|
|
841
|
+
currentVersionBody?.published_by?.name,
|
|
842
|
+
]);
|
|
843
|
+
const priorPublisherSet = uniqueIdentities(
|
|
844
|
+
priorReleaseEntry?.publishers || [],
|
|
845
|
+
);
|
|
846
|
+
const overlapMetrics = identityOverlapMetrics(
|
|
847
|
+
currentPublisherSet,
|
|
848
|
+
priorPublisherSet,
|
|
849
|
+
);
|
|
850
|
+
const publisherDrift = isDisjointIdentitySet(
|
|
851
|
+
currentPublisherSet,
|
|
852
|
+
priorPublisherSet,
|
|
853
|
+
);
|
|
854
|
+
const ownerSet = uniqueIdentities(
|
|
855
|
+
(ownersBody?.users || []).flatMap((owner) => [owner?.login, owner?.name]),
|
|
856
|
+
);
|
|
857
|
+
const trustpubCandidate =
|
|
858
|
+
currentVersionBody?.trustpub_data ||
|
|
859
|
+
currentVersionBody?.trustpubData ||
|
|
860
|
+
crateBody?.crate?.trustpub_data ||
|
|
861
|
+
crateBody?.crate?.trustpubData ||
|
|
862
|
+
crateBody?.versions?.find((entry) => entry?.trustpub_data)?.trustpub_data;
|
|
863
|
+
const provenanceUrl = normalizeProvenanceUrl(trustpubCandidate);
|
|
864
|
+
const provenanceDigests = collectProvenanceDigests(trustpubCandidate);
|
|
865
|
+
const provenanceKeyIds = collectProvenanceKeyIds(trustpubCandidate);
|
|
866
|
+
const provenanceSignatures = collectProvenanceSignatures(trustpubCandidate);
|
|
867
|
+
const provenancePredicateTypes =
|
|
868
|
+
collectProvenancePredicateTypes(trustpubCandidate);
|
|
869
|
+
|
|
870
|
+
appendProperty(
|
|
871
|
+
properties,
|
|
872
|
+
"cdx:cargo:packageCreatedTime",
|
|
873
|
+
sortReleaseEntries([...releaseEntries])[0]?.rawTime,
|
|
874
|
+
);
|
|
875
|
+
appendProperty(
|
|
876
|
+
properties,
|
|
877
|
+
"cdx:cargo:publishTime",
|
|
878
|
+
currentVersionBody?.created_at || currentVersionBody?.updated_at,
|
|
879
|
+
);
|
|
880
|
+
appendProperty(properties, "cdx:cargo:versionCount", releaseEntries.length);
|
|
881
|
+
appendProperty(
|
|
882
|
+
properties,
|
|
883
|
+
"cdx:cargo:publisher",
|
|
884
|
+
currentVersionBody?.published_by?.login ||
|
|
885
|
+
currentVersionBody?.published_by?.name,
|
|
886
|
+
);
|
|
887
|
+
appendProperty(
|
|
888
|
+
properties,
|
|
889
|
+
"cdx:cargo:priorPublisher",
|
|
890
|
+
priorReleaseEntry?.publishers?.join(", "),
|
|
891
|
+
);
|
|
892
|
+
appendProperty(
|
|
893
|
+
properties,
|
|
894
|
+
"cdx:cargo:publisherSet",
|
|
895
|
+
currentPublisherSet.join(", "),
|
|
896
|
+
);
|
|
897
|
+
appendProperty(
|
|
898
|
+
properties,
|
|
899
|
+
"cdx:cargo:publisherSetCount",
|
|
900
|
+
currentPublisherSet.length,
|
|
901
|
+
);
|
|
902
|
+
appendProperty(properties, "cdx:cargo:ownerSet", ownerSet.join(", "));
|
|
903
|
+
appendProperty(properties, "cdx:cargo:ownerSetCount", ownerSet.length);
|
|
904
|
+
appendProperty(
|
|
905
|
+
properties,
|
|
906
|
+
"cdx:cargo:publisherOverlapCount",
|
|
907
|
+
overlapMetrics.overlapCount,
|
|
908
|
+
);
|
|
909
|
+
appendProperty(
|
|
910
|
+
properties,
|
|
911
|
+
"cdx:cargo:publisherOverlapRatio",
|
|
912
|
+
overlapMetrics.overlapRatio?.toFixed(2),
|
|
913
|
+
);
|
|
914
|
+
appendProperty(
|
|
915
|
+
properties,
|
|
916
|
+
"cdx:cargo:priorVersion",
|
|
917
|
+
priorReleaseEntry?.version,
|
|
918
|
+
);
|
|
919
|
+
appendProperty(
|
|
920
|
+
properties,
|
|
921
|
+
"cdx:cargo:priorPublishTime",
|
|
922
|
+
priorReleaseEntry?.rawTime,
|
|
923
|
+
);
|
|
924
|
+
appendProperty(
|
|
925
|
+
properties,
|
|
926
|
+
"cdx:cargo:releaseGapDays",
|
|
927
|
+
gapMetrics.currentGapDays?.toFixed(2),
|
|
928
|
+
);
|
|
929
|
+
appendProperty(
|
|
930
|
+
properties,
|
|
931
|
+
"cdx:cargo:releaseGapBaselineDays",
|
|
932
|
+
gapMetrics.baselineDays?.toFixed(2),
|
|
933
|
+
);
|
|
934
|
+
appendProperty(
|
|
935
|
+
properties,
|
|
936
|
+
"cdx:cargo:releaseGapSampleSize",
|
|
937
|
+
gapMetrics.sampleSize,
|
|
938
|
+
);
|
|
939
|
+
appendProperty(
|
|
940
|
+
properties,
|
|
941
|
+
"cdx:cargo:releaseCadenceCompressionRatio",
|
|
942
|
+
cadenceMetrics.compressionRatio?.toFixed(2),
|
|
943
|
+
);
|
|
944
|
+
appendProperty(
|
|
945
|
+
properties,
|
|
946
|
+
"cdx:cargo:artifactDigestSha256",
|
|
947
|
+
currentVersionBody?.checksum,
|
|
948
|
+
);
|
|
949
|
+
appendProperty(properties, "cdx:cargo:edition", currentVersionBody?.edition);
|
|
950
|
+
appendProperty(properties, "cdx:cargo:hasLib", currentVersionBody?.has_lib);
|
|
951
|
+
appendJoinedProperty(
|
|
952
|
+
properties,
|
|
953
|
+
"cdx:cargo:binNames",
|
|
954
|
+
Array.isArray(currentVersionBody?.bin_names)
|
|
955
|
+
? currentVersionBody.bin_names
|
|
956
|
+
: [],
|
|
957
|
+
);
|
|
958
|
+
appendProperty(
|
|
959
|
+
properties,
|
|
960
|
+
"cdx:cargo:crateSize",
|
|
961
|
+
currentVersionBody?.crate_size,
|
|
962
|
+
);
|
|
963
|
+
if (currentVersionBody?.yanked === true) {
|
|
964
|
+
appendProperty(properties, "cdx:cargo:yanked", "true");
|
|
965
|
+
}
|
|
966
|
+
if (publisherDrift) {
|
|
967
|
+
appendProperty(properties, "cdx:cargo:publisherDrift", "true");
|
|
968
|
+
}
|
|
969
|
+
if (overlapMetrics.partialDrift) {
|
|
970
|
+
appendProperty(properties, "cdx:cargo:publisherSetPartialDrift", "true");
|
|
971
|
+
}
|
|
972
|
+
if (cadenceMetrics.compressedCadence) {
|
|
973
|
+
appendProperty(properties, "cdx:cargo:compressedCadence", "true");
|
|
974
|
+
}
|
|
975
|
+
if (
|
|
976
|
+
crateBody?.crate?.trustpub_only === true ||
|
|
977
|
+
hasTrustedPublishingEvidence(trustpubCandidate)
|
|
978
|
+
) {
|
|
979
|
+
appendProperty(properties, "cdx:cargo:trustedPublishing", "true");
|
|
980
|
+
}
|
|
981
|
+
appendProperty(properties, "cdx:cargo:provenanceUrl", provenanceUrl);
|
|
982
|
+
appendJoinedProperty(
|
|
983
|
+
properties,
|
|
984
|
+
"cdx:cargo:provenanceDigest",
|
|
985
|
+
provenanceDigests,
|
|
986
|
+
);
|
|
987
|
+
appendJoinedProperty(
|
|
988
|
+
properties,
|
|
989
|
+
"cdx:cargo:provenanceKeyId",
|
|
990
|
+
provenanceKeyIds,
|
|
991
|
+
);
|
|
992
|
+
appendJoinedProperty(
|
|
993
|
+
properties,
|
|
994
|
+
"cdx:cargo:provenanceSignature",
|
|
995
|
+
provenanceSignatures,
|
|
996
|
+
);
|
|
997
|
+
appendJoinedProperty(
|
|
998
|
+
properties,
|
|
999
|
+
"cdx:cargo:provenancePredicateType",
|
|
1000
|
+
provenancePredicateTypes,
|
|
1001
|
+
);
|
|
1002
|
+
return properties;
|
|
1003
|
+
}
|