@cyclonedx/cdxgen 12.3.2 → 12.3.3
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 +6 -0
- package/data/rules/ci-permissions.yaml +132 -0
- package/data/rules/dependency-sources.yaml +65 -5
- package/data/rules/package-integrity.yaml +22 -0
- package/lib/cli/index.js +141 -39
- package/lib/cli/index.poku.js +579 -1
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +38 -9
- package/lib/helpers/analyzer.poku.js +67 -0
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +45 -22
- package/lib/helpers/display.poku.js +47 -60
- package/lib/helpers/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/utils.js +951 -40
- package/lib/helpers/utils.poku.js +882 -0
- package/lib/managers/binary.js +16 -0
- package/lib/managers/binary.poku.js +1 -0
- package/lib/managers/docker.js +240 -16
- package/lib/managers/docker.poku.js +1142 -2
- package/lib/server/server.js +7 -4
- package/lib/server/server.poku.js +36 -1
- package/lib/stages/postgen/auditBom.poku.js +644 -2
- package/package.json +2 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +29 -0
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
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("agentFormulationParser", () => {
|
|
10
|
+
it("sanitizes inferred MCP URLs before emitting them", async () => {
|
|
11
|
+
const readFileSync = sinon.stub();
|
|
12
|
+
const scanTextForHiddenUnicode = sinon.stub().returns({
|
|
13
|
+
hasHiddenUnicode: false,
|
|
14
|
+
});
|
|
15
|
+
readFileSync
|
|
16
|
+
.withArgs("/repo/AGENTS.md", "utf-8")
|
|
17
|
+
.returns(
|
|
18
|
+
[
|
|
19
|
+
"Use the remote MCP endpoint at",
|
|
20
|
+
"https://user:pass@example.com/mcp?access_token=abc#frag",
|
|
21
|
+
"during release preparation.",
|
|
22
|
+
].join(" "),
|
|
23
|
+
);
|
|
24
|
+
const { agentFormulationParser } = await esmock(
|
|
25
|
+
"./agentFormulationParser.js",
|
|
26
|
+
{
|
|
27
|
+
"node:fs": { readFileSync },
|
|
28
|
+
"./unicodeScan.js": { scanTextForHiddenUnicode },
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const result = agentFormulationParser.parse(["/repo/AGENTS.md"]);
|
|
33
|
+
|
|
34
|
+
assert.strictEqual(
|
|
35
|
+
getProp(result.components[0], "cdx:agent:hiddenMcpUrls"),
|
|
36
|
+
"https://example.com/mcp",
|
|
37
|
+
);
|
|
38
|
+
assert.deepStrictEqual(result.services[0].endpoints, [
|
|
39
|
+
"https://example.com/mcp",
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/lib/helpers/analyzer.js
CHANGED
|
@@ -8,6 +8,10 @@ import traverse from "@babel/traverse";
|
|
|
8
8
|
|
|
9
9
|
import { classifyMcpReference } from "./mcp.js";
|
|
10
10
|
import { isLocalHost, sanitizeMcpRefToken } from "./mcpDiscovery.js";
|
|
11
|
+
import {
|
|
12
|
+
sanitizeBomPropertyValue,
|
|
13
|
+
sanitizeBomUrl,
|
|
14
|
+
} from "./propertySanitizer.js";
|
|
11
15
|
|
|
12
16
|
const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS
|
|
13
17
|
? process.env.ASTGEN_IGNORE_DIRS.split(",")
|
|
@@ -1509,13 +1513,26 @@ const providerFamilyFromModelName = (modelName) => {
|
|
|
1509
1513
|
};
|
|
1510
1514
|
|
|
1511
1515
|
const addUniqueProperty = (properties, name, value) => {
|
|
1512
|
-
|
|
1516
|
+
const sanitizedValue = sanitizeBomPropertyValue(name, value);
|
|
1517
|
+
if (
|
|
1518
|
+
sanitizedValue === undefined ||
|
|
1519
|
+
sanitizedValue === null ||
|
|
1520
|
+
sanitizedValue === ""
|
|
1521
|
+
) {
|
|
1513
1522
|
return;
|
|
1514
1523
|
}
|
|
1515
|
-
|
|
1524
|
+
const normalizedValue =
|
|
1525
|
+
typeof sanitizedValue === "string"
|
|
1526
|
+
? sanitizedValue
|
|
1527
|
+
: String(sanitizedValue);
|
|
1528
|
+
if (
|
|
1529
|
+
properties.some(
|
|
1530
|
+
(prop) => prop.name === name && prop.value === normalizedValue,
|
|
1531
|
+
)
|
|
1532
|
+
) {
|
|
1516
1533
|
return;
|
|
1517
1534
|
}
|
|
1518
|
-
properties.push({ name, value });
|
|
1535
|
+
properties.push({ name, value: normalizedValue });
|
|
1519
1536
|
};
|
|
1520
1537
|
|
|
1521
1538
|
const rootMemberName = (value) => String(value || "").split(".")[0];
|
|
@@ -1823,14 +1840,18 @@ const primitiveComponentForMcp = (serviceInfo, primitive) => {
|
|
|
1823
1840
|
addUniqueProperty(
|
|
1824
1841
|
properties,
|
|
1825
1842
|
"cdx:mcp:toolAnnotations",
|
|
1826
|
-
|
|
1843
|
+
primitive.annotations,
|
|
1827
1844
|
);
|
|
1828
1845
|
}
|
|
1829
1846
|
return {
|
|
1830
1847
|
"bom-ref": primitiveRef,
|
|
1831
|
-
description:
|
|
1832
|
-
|
|
1833
|
-
|
|
1848
|
+
description: String(
|
|
1849
|
+
sanitizeBomPropertyValue(
|
|
1850
|
+
"cdx:mcp:description",
|
|
1851
|
+
primitive.description ||
|
|
1852
|
+
`${primitive.role} exposed by ${serviceInfo.name || "mcp-server"}`,
|
|
1853
|
+
) || "",
|
|
1854
|
+
),
|
|
1834
1855
|
name: primitiveName,
|
|
1835
1856
|
properties,
|
|
1836
1857
|
scope: "required",
|
|
@@ -2056,8 +2077,16 @@ const serviceObjectForMcp = (serviceInfo) => {
|
|
|
2056
2077
|
return {
|
|
2057
2078
|
"bom-ref": serviceRef,
|
|
2058
2079
|
authenticated: serviceInfo.authenticated,
|
|
2059
|
-
description:
|
|
2060
|
-
|
|
2080
|
+
description: String(
|
|
2081
|
+
sanitizeBomPropertyValue(
|
|
2082
|
+
"cdx:mcp:description",
|
|
2083
|
+
serviceInfo.description || "",
|
|
2084
|
+
) || "",
|
|
2085
|
+
),
|
|
2086
|
+
endpoints: Array.from(serviceInfo.endpoints)
|
|
2087
|
+
.map((endpoint) => sanitizeBomUrl(endpoint))
|
|
2088
|
+
.filter(Boolean)
|
|
2089
|
+
.sort(),
|
|
2061
2090
|
group: "mcp",
|
|
2062
2091
|
name: serviceName,
|
|
2063
2092
|
properties,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
|
+
import { URL } from "node:url";
|
|
10
11
|
|
|
11
12
|
import { assert, describe, it } from "poku";
|
|
12
13
|
|
|
@@ -530,6 +531,72 @@ describe("detectMcpInventory()", () => {
|
|
|
530
531
|
),
|
|
531
532
|
);
|
|
532
533
|
});
|
|
534
|
+
|
|
535
|
+
it("sanitizes source-code-analysis MCP metadata before emission", () => {
|
|
536
|
+
const projectDir = createProjectFiles("mcp-sanitized-source-analysis", {
|
|
537
|
+
"src/server.ts": [
|
|
538
|
+
"import { McpServer } from '@modelcontextprotocol/server';",
|
|
539
|
+
"import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';",
|
|
540
|
+
"const server = new McpServer({",
|
|
541
|
+
" name: 'sanitized-server',",
|
|
542
|
+
" version: '0.3.0',",
|
|
543
|
+
" description: 'Use https://user:pass@example.com/mcp?token=abc#frag and Bearer sk_test_super_secret_value',",
|
|
544
|
+
"});",
|
|
545
|
+
"server.registerTool(",
|
|
546
|
+
" 'download',",
|
|
547
|
+
" {",
|
|
548
|
+
" description: 'Download from https://user:pass@example.com/tool?token=abc#frag',",
|
|
549
|
+
" annotations: {",
|
|
550
|
+
" Authorization: 'Bearer sk_test_super_secret_value',",
|
|
551
|
+
" nested: { __proto__: 'polluted', endpoint: 'https://user:pass@example.com/tool?token=abc#frag' },",
|
|
552
|
+
" },",
|
|
553
|
+
" },",
|
|
554
|
+
" async () => ({ content: [] }),",
|
|
555
|
+
");",
|
|
556
|
+
"server.registerResource(",
|
|
557
|
+
" 'private-docs',",
|
|
558
|
+
" 'https://user:pass@example.com/docs?token=abc#frag',",
|
|
559
|
+
" { description: 'Private docs' },",
|
|
560
|
+
" async () => ({ contents: [] }),",
|
|
561
|
+
");",
|
|
562
|
+
"const transport = new StreamableHTTPClientTransport(new URL('https://user:pass@example.com/mcp?access_token=secret#frag'));",
|
|
563
|
+
"void transport;",
|
|
564
|
+
].join("\n"),
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const inventory = detectMcpInventory(projectDir);
|
|
568
|
+
const service = inventory.services[0];
|
|
569
|
+
const toolComponent = inventory.components.find(
|
|
570
|
+
(component) => component.name === "download",
|
|
571
|
+
);
|
|
572
|
+
const resourceComponent = inventory.components.find(
|
|
573
|
+
(component) => component.name === "private-docs",
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
assert.strictEqual(
|
|
577
|
+
service.description,
|
|
578
|
+
"Use https://example.com/mcp and [redacted]",
|
|
579
|
+
);
|
|
580
|
+
const serviceEndpoint = new URL(service.endpoints[0]);
|
|
581
|
+
assert.strictEqual(serviceEndpoint.hostname, "example.com");
|
|
582
|
+
assert.strictEqual(serviceEndpoint.pathname, "/mcp");
|
|
583
|
+
assert.strictEqual(
|
|
584
|
+
getProp(resourceComponent, "cdx:mcp:resourceUri"),
|
|
585
|
+
"https://example.com/docs",
|
|
586
|
+
);
|
|
587
|
+
assert.strictEqual(
|
|
588
|
+
toolComponent.description,
|
|
589
|
+
"Download from https://example.com/tool",
|
|
590
|
+
);
|
|
591
|
+
const toolAnnotations = JSON.parse(
|
|
592
|
+
getProp(toolComponent, "cdx:mcp:toolAnnotations"),
|
|
593
|
+
);
|
|
594
|
+
assert.strictEqual(toolAnnotations.Authorization, "[redacted]");
|
|
595
|
+
assert.ok(
|
|
596
|
+
!JSON.stringify(toolAnnotations).includes("sk_test_super_secret_value"),
|
|
597
|
+
);
|
|
598
|
+
assert.ok(!JSON.stringify(toolAnnotations).includes("__proto__"));
|
|
599
|
+
});
|
|
533
600
|
});
|
|
534
601
|
|
|
535
602
|
describe("detectPythonMcpInventory()", () => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES,
|
|
10
10
|
detectExtensionCapabilities,
|
|
11
11
|
} from "./analyzer.js";
|
|
12
|
+
import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
|
|
12
13
|
import { isMac, isWin, safeExistsSync } from "./utils.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -850,7 +851,12 @@ export function toComponent(extInfo) {
|
|
|
850
851
|
const component = {
|
|
851
852
|
name: extensionId,
|
|
852
853
|
version: extInfo.version || "",
|
|
853
|
-
description:
|
|
854
|
+
description: String(
|
|
855
|
+
sanitizeBomPropertyValue(
|
|
856
|
+
"cdx:chrome-extension:description",
|
|
857
|
+
extInfo.displayName || extInfo.description || "",
|
|
858
|
+
) || "",
|
|
859
|
+
),
|
|
854
860
|
purl,
|
|
855
861
|
"bom-ref": decodeURIComponent(purl),
|
|
856
862
|
type: "application",
|
|
@@ -1035,8 +1041,24 @@ export function toComponent(extInfo) {
|
|
|
1035
1041
|
if (extInfo.srcPath) {
|
|
1036
1042
|
properties.push({ name: "SrcFile", value: extInfo.srcPath });
|
|
1037
1043
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1044
|
+
const sanitizedProperties = properties
|
|
1045
|
+
.map((property) => {
|
|
1046
|
+
const sanitizedValue = sanitizeBomPropertyValue(
|
|
1047
|
+
property.name,
|
|
1048
|
+
property.value,
|
|
1049
|
+
);
|
|
1050
|
+
if (
|
|
1051
|
+
sanitizedValue === undefined ||
|
|
1052
|
+
sanitizedValue === null ||
|
|
1053
|
+
sanitizedValue === ""
|
|
1054
|
+
) {
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
return { name: property.name, value: String(sanitizedValue) };
|
|
1058
|
+
})
|
|
1059
|
+
.filter(Boolean);
|
|
1060
|
+
if (sanitizedProperties.length) {
|
|
1061
|
+
component.properties = sanitizedProperties;
|
|
1040
1062
|
}
|
|
1041
1063
|
return component;
|
|
1042
1064
|
}
|
|
@@ -142,6 +142,74 @@ describe("parseChromiumExtensionManifest", () => {
|
|
|
142
142
|
assert.strictEqual(parsed.hasAutofill, false);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it("sanitizes emitted URL properties before they enter the BOM", () => {
|
|
146
|
+
const extensionRoot = join(baseTempDir, "sanitized-extension");
|
|
147
|
+
const extensionId = "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii";
|
|
148
|
+
const extensionVersion = "1.0.0";
|
|
149
|
+
const versionDir = join(extensionRoot, extensionId, extensionVersion);
|
|
150
|
+
mkdirSync(versionDir, { recursive: true });
|
|
151
|
+
writeFileSync(
|
|
152
|
+
join(versionDir, "manifest.json"),
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
manifest_version: 3,
|
|
155
|
+
name: "Sanitized URLs",
|
|
156
|
+
version: extensionVersion,
|
|
157
|
+
update_url: "https://user:pass@example.com/update.xml?token=abc#frag",
|
|
158
|
+
host_permissions: [
|
|
159
|
+
"https://user:pass@example.com/*?token=abc#frag",
|
|
160
|
+
"<all_urls>",
|
|
161
|
+
],
|
|
162
|
+
externally_connectable: {
|
|
163
|
+
matches: ["https://user:pass@example.com/*?token=abc#frag"],
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
"utf-8",
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const result = collectChromeExtensionsFromPath(versionDir);
|
|
170
|
+
|
|
171
|
+
assert.strictEqual(
|
|
172
|
+
getProp(result.components[0], "cdx:chrome-extension:updateUrl"),
|
|
173
|
+
"https://example.com/update.xml",
|
|
174
|
+
);
|
|
175
|
+
assert.strictEqual(
|
|
176
|
+
getProp(result.components[0], "cdx:chrome-extension:hostPermissions"),
|
|
177
|
+
"https://example.com/*, <all_urls>",
|
|
178
|
+
);
|
|
179
|
+
assert.strictEqual(
|
|
180
|
+
getProp(
|
|
181
|
+
result.components[0],
|
|
182
|
+
"cdx:chrome-extension:externallyConnectableMatches",
|
|
183
|
+
),
|
|
184
|
+
"https://example.com/*",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("sanitizes emitted extension descriptions before they enter the BOM", () => {
|
|
189
|
+
const extensionRoot = join(baseTempDir, "sanitized-description-extension");
|
|
190
|
+
const extensionId = "jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj";
|
|
191
|
+
const extensionVersion = "1.0.0";
|
|
192
|
+
const versionDir = join(extensionRoot, extensionId, extensionVersion);
|
|
193
|
+
mkdirSync(versionDir, { recursive: true });
|
|
194
|
+
writeFileSync(
|
|
195
|
+
join(versionDir, "manifest.json"),
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
manifest_version: 3,
|
|
198
|
+
description:
|
|
199
|
+
"Connect with Bearer sk_test_super_secret_value at https://user:pass@example.com/path?token=abc#frag",
|
|
200
|
+
version: extensionVersion,
|
|
201
|
+
}),
|
|
202
|
+
"utf-8",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const result = collectChromeExtensionsFromPath(versionDir);
|
|
206
|
+
|
|
207
|
+
assert.strictEqual(
|
|
208
|
+
result.components[0].description,
|
|
209
|
+
"Connect with [redacted] at https://example.com/path",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
145
213
|
it("should parse real manifest fixtures from Chrome, Chromium and Edge extensions", () => {
|
|
146
214
|
const fixtureCases = [
|
|
147
215
|
{
|
|
@@ -112,6 +112,34 @@ const CARGO_CACHE_ACTION_PATTERNS = [/^swatinem\/rust-cache(?:@|$)/i];
|
|
|
112
112
|
|
|
113
113
|
const CARGO_TOOL_INSTALL_ACTION_PATTERNS = [/^taiki-e\/install-action(?:@|$)/i];
|
|
114
114
|
|
|
115
|
+
const DEPENDENCY_CACHE_SETUP_ACTIONS = [
|
|
116
|
+
{
|
|
117
|
+
pattern: /^actions\/setup-node(?:@|$)/i,
|
|
118
|
+
ecosystem: "npm",
|
|
119
|
+
inputNames: ["package-manager-cache", "cache"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
pattern: /^actions\/setup-python(?:@|$)/i,
|
|
123
|
+
ecosystem: "pypi",
|
|
124
|
+
inputNames: ["cache"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
pattern: /^actions\/setup-go(?:@|$)/i,
|
|
128
|
+
ecosystem: "go",
|
|
129
|
+
inputNames: ["cache"],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
pattern: /^actions\/setup-java(?:@|$)/i,
|
|
133
|
+
ecosystem: "java",
|
|
134
|
+
inputNames: ["cache"],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
pattern: /^moonrepo\/setup-rust(?:@|$)/i,
|
|
138
|
+
ecosystem: "cargo",
|
|
139
|
+
inputNames: ["cache"],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
115
143
|
const FORK_CONTEXT_PATTERNS = [
|
|
116
144
|
[
|
|
117
145
|
"github.event.pull_request.head.repo.fork",
|
|
@@ -479,6 +507,56 @@ function analyzeCargoActionStep(step) {
|
|
|
479
507
|
return props;
|
|
480
508
|
}
|
|
481
509
|
|
|
510
|
+
function isExplicitFalseLikeValue(value) {
|
|
511
|
+
if (value === false) {
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
if (typeof value !== "string") {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return ["0", "false", "no", "off", "disabled"].includes(
|
|
518
|
+
value.trim().toLowerCase(),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function analyzeSetupActionCacheStep(step) {
|
|
523
|
+
const props = [];
|
|
524
|
+
if (!step?.uses || typeof step.uses !== "string") {
|
|
525
|
+
return props;
|
|
526
|
+
}
|
|
527
|
+
const setupAction = DEPENDENCY_CACHE_SETUP_ACTIONS.find((candidate) =>
|
|
528
|
+
candidate.pattern.test(step.uses),
|
|
529
|
+
);
|
|
530
|
+
if (!setupAction || !step.with || typeof step.with !== "object") {
|
|
531
|
+
return props;
|
|
532
|
+
}
|
|
533
|
+
const disableInputName = setupAction.inputNames.find(
|
|
534
|
+
(inputName) =>
|
|
535
|
+
Object.hasOwn(step.with, inputName) &&
|
|
536
|
+
isExplicitFalseLikeValue(step.with[inputName]),
|
|
537
|
+
);
|
|
538
|
+
if (!disableInputName) {
|
|
539
|
+
return props;
|
|
540
|
+
}
|
|
541
|
+
props.push({
|
|
542
|
+
name: "cdx:github:action:disablesBuildCache",
|
|
543
|
+
value: "true",
|
|
544
|
+
});
|
|
545
|
+
props.push({
|
|
546
|
+
name: "cdx:github:action:buildCacheEcosystem",
|
|
547
|
+
value: setupAction.ecosystem,
|
|
548
|
+
});
|
|
549
|
+
props.push({
|
|
550
|
+
name: "cdx:github:action:buildCacheDisableInput",
|
|
551
|
+
value: disableInputName,
|
|
552
|
+
});
|
|
553
|
+
props.push({
|
|
554
|
+
name: "cdx:github:action:buildCacheDisableValue",
|
|
555
|
+
value: String(step.with[disableInputName]),
|
|
556
|
+
});
|
|
557
|
+
return props;
|
|
558
|
+
}
|
|
559
|
+
|
|
482
560
|
function analyzeCargoRunStep(normalizedRun) {
|
|
483
561
|
const props = [];
|
|
484
562
|
if (!normalizedRun || typeof normalizedRun !== "string") {
|
|
@@ -2018,6 +2096,7 @@ export function parseWorkflowFile(f, options) {
|
|
|
2018
2096
|
actionProperties.push(...analyzeCheckoutStep(step));
|
|
2019
2097
|
actionProperties.push(...analyzeCacheStep(step));
|
|
2020
2098
|
actionProperties.push(...analyzeCargoActionStep(step));
|
|
2099
|
+
actionProperties.push(...analyzeSetupActionCacheStep(step));
|
|
2021
2100
|
actionProperties.push(...analyzeDispatchActionStep(step));
|
|
2022
2101
|
if (
|
|
2023
2102
|
step.uses?.includes("actions/github-script") &&
|
|
@@ -505,6 +505,109 @@ describe("githubActionsParser", () => {
|
|
|
505
505
|
});
|
|
506
506
|
});
|
|
507
507
|
|
|
508
|
+
describe("setup action cache disable property emission", () => {
|
|
509
|
+
it("emits cache disable properties for setup-node, setup-python, and setup-rust", () => {
|
|
510
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-gha-cache-"));
|
|
511
|
+
const workflowFile = path.join(tmpDir, "cache-disable.yml");
|
|
512
|
+
writeFileSync(
|
|
513
|
+
workflowFile,
|
|
514
|
+
[
|
|
515
|
+
"name: Cache disable",
|
|
516
|
+
"on: push",
|
|
517
|
+
"jobs:",
|
|
518
|
+
" build:",
|
|
519
|
+
" runs-on: ubuntu-latest",
|
|
520
|
+
" steps:",
|
|
521
|
+
" - uses: actions/setup-node@v4",
|
|
522
|
+
" with:",
|
|
523
|
+
" node-version: 20",
|
|
524
|
+
" package-manager-cache: false",
|
|
525
|
+
" - uses: actions/setup-python@v5",
|
|
526
|
+
" with:",
|
|
527
|
+
" python-version: '3.12'",
|
|
528
|
+
" cache: false",
|
|
529
|
+
" - uses: moonrepo/setup-rust@v1",
|
|
530
|
+
" with:",
|
|
531
|
+
" cache: false",
|
|
532
|
+
].join("\n"),
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const result = parseWorkflowFile(workflowFile, { specVersion: 1.7 });
|
|
537
|
+
const setupNodeComp = result.components.find(
|
|
538
|
+
(component) =>
|
|
539
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
540
|
+
"actions/setup-node@v4",
|
|
541
|
+
);
|
|
542
|
+
const setupPythonComp = result.components.find(
|
|
543
|
+
(component) =>
|
|
544
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
545
|
+
"actions/setup-python@v5",
|
|
546
|
+
);
|
|
547
|
+
const setupRustComp = result.components.find(
|
|
548
|
+
(component) =>
|
|
549
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
550
|
+
"moonrepo/setup-rust@v1",
|
|
551
|
+
);
|
|
552
|
+
assert.ok(setupNodeComp, "expected setup-node component");
|
|
553
|
+
assert.ok(setupPythonComp, "expected setup-python component");
|
|
554
|
+
assert.ok(setupRustComp, "expected setup-rust component");
|
|
555
|
+
assert.strictEqual(
|
|
556
|
+
getProp(setupNodeComp, "cdx:github:action:disablesBuildCache"),
|
|
557
|
+
"true",
|
|
558
|
+
);
|
|
559
|
+
assert.strictEqual(
|
|
560
|
+
getProp(setupNodeComp, "cdx:github:action:buildCacheEcosystem"),
|
|
561
|
+
"npm",
|
|
562
|
+
);
|
|
563
|
+
assert.strictEqual(
|
|
564
|
+
getProp(setupNodeComp, "cdx:github:action:buildCacheDisableInput"),
|
|
565
|
+
"package-manager-cache",
|
|
566
|
+
);
|
|
567
|
+
assert.strictEqual(
|
|
568
|
+
getProp(setupPythonComp, "cdx:github:action:disablesBuildCache"),
|
|
569
|
+
"true",
|
|
570
|
+
);
|
|
571
|
+
assert.strictEqual(
|
|
572
|
+
getProp(setupPythonComp, "cdx:github:action:buildCacheEcosystem"),
|
|
573
|
+
"pypi",
|
|
574
|
+
);
|
|
575
|
+
assert.strictEqual(
|
|
576
|
+
getProp(setupPythonComp, "cdx:github:action:buildCacheDisableInput"),
|
|
577
|
+
"cache",
|
|
578
|
+
);
|
|
579
|
+
assert.strictEqual(
|
|
580
|
+
getProp(setupRustComp, "cdx:github:action:disablesBuildCache"),
|
|
581
|
+
"true",
|
|
582
|
+
);
|
|
583
|
+
assert.strictEqual(
|
|
584
|
+
getProp(setupRustComp, "cdx:github:action:buildCacheEcosystem"),
|
|
585
|
+
"cargo",
|
|
586
|
+
);
|
|
587
|
+
assert.strictEqual(
|
|
588
|
+
getProp(setupRustComp, "cdx:github:action:buildCacheDisableInput"),
|
|
589
|
+
"cache",
|
|
590
|
+
);
|
|
591
|
+
} finally {
|
|
592
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("does not emit cache disable properties when cache is not explicitly disabled", () => {
|
|
597
|
+
const result = parseWorkflow("simple-build.yml");
|
|
598
|
+
const setupNodeComp = result.components.find(
|
|
599
|
+
(component) =>
|
|
600
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
601
|
+
"actions/setup-node@v4",
|
|
602
|
+
);
|
|
603
|
+
assert.ok(setupNodeComp, "expected setup-node component");
|
|
604
|
+
assert.strictEqual(
|
|
605
|
+
getProp(setupNodeComp, "cdx:github:action:disablesBuildCache"),
|
|
606
|
+
undefined,
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
508
611
|
describe("script injection interpolation detection", () => {
|
|
509
612
|
it("detects github.event.pull_request interpolation", () => {
|
|
510
613
|
const result = parseWorkflow("injection-pull-request-title.yml");
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
credentialIndicatorsForText,
|
|
9
9
|
sanitizeMcpRefToken,
|
|
10
10
|
} from "./mcpDiscovery.js";
|
|
11
|
+
import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
|
|
11
12
|
import { scanTextForHiddenUnicode } from "./unicodeScan.js";
|
|
12
13
|
|
|
13
14
|
const COMMUNITY_AI_PATTERNS = [
|
|
@@ -46,13 +47,22 @@ const COMMUNITY_AI_PATTERNS = [
|
|
|
46
47
|
];
|
|
47
48
|
|
|
48
49
|
function addUniqueProperty(properties, name, value) {
|
|
49
|
-
|
|
50
|
+
const sanitizedValue = sanitizeBomPropertyValue(name, value);
|
|
51
|
+
if (
|
|
52
|
+
sanitizedValue === undefined ||
|
|
53
|
+
sanitizedValue === null ||
|
|
54
|
+
sanitizedValue === ""
|
|
55
|
+
) {
|
|
50
56
|
return;
|
|
51
57
|
}
|
|
52
|
-
if (
|
|
58
|
+
if (
|
|
59
|
+
properties.some(
|
|
60
|
+
(prop) => prop.name === name && prop.value === String(sanitizedValue),
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
55
|
-
properties.push({ name, value: String(
|
|
65
|
+
properties.push({ name, value: String(sanitizedValue) });
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
function normalizeFilePath(filePath) {
|
|
@@ -217,7 +227,7 @@ function parseSkillFile(filePath, raw) {
|
|
|
217
227
|
addUniqueProperty(
|
|
218
228
|
component.properties,
|
|
219
229
|
"cdx:skill:metadata",
|
|
220
|
-
|
|
230
|
+
metadata.metadata,
|
|
221
231
|
);
|
|
222
232
|
}
|
|
223
233
|
maybeAddFileSignals(component.properties, filePath, raw);
|
|
@@ -399,7 +409,7 @@ function parseOpencodeConfig(filePath, raw) {
|
|
|
399
409
|
addUniqueProperty(
|
|
400
410
|
component.properties,
|
|
401
411
|
"cdx:agent:permission",
|
|
402
|
-
|
|
412
|
+
agentConfig.permission,
|
|
403
413
|
);
|
|
404
414
|
}
|
|
405
415
|
components.push(component);
|
|
@@ -60,4 +60,75 @@ describe("communityAiConfigParser", () => {
|
|
|
60
60
|
),
|
|
61
61
|
);
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
it("sanitizes secret-bearing AI inventory properties before emission", async () => {
|
|
65
|
+
const readFileSync = sinon.stub();
|
|
66
|
+
readFileSync.withArgs("/repo/opencode.json", "utf-8").returns(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
agent: {
|
|
69
|
+
release: {
|
|
70
|
+
description:
|
|
71
|
+
"Deploy with https://user:pass@example.com/release?access_token=abc#frag and sk_test_super_secret_value",
|
|
72
|
+
permission: {
|
|
73
|
+
endpoints: [
|
|
74
|
+
"https://user:pass@example.com/private?token=abc#frag",
|
|
75
|
+
],
|
|
76
|
+
__proto__: {
|
|
77
|
+
polluted: true,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
readFileSync
|
|
85
|
+
.withArgs("/repo/.claude/skills/release/SKILL.md", "utf-8")
|
|
86
|
+
.returns(
|
|
87
|
+
[
|
|
88
|
+
"---",
|
|
89
|
+
"name: release",
|
|
90
|
+
"description: Publish release notes",
|
|
91
|
+
"metadata:",
|
|
92
|
+
" endpoint: https://user:pass@example.com/skill?token=abc#frag",
|
|
93
|
+
" apiKey: sk_test_skill_secret_value",
|
|
94
|
+
"---",
|
|
95
|
+
"Use the release workflow.",
|
|
96
|
+
].join("\n"),
|
|
97
|
+
);
|
|
98
|
+
const { communityAiConfigParser } = await esmock(
|
|
99
|
+
"./communityAiConfigParser.js",
|
|
100
|
+
{
|
|
101
|
+
"node:fs": { readFileSync },
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const result = communityAiConfigParser.parse([
|
|
106
|
+
"/repo/opencode.json",
|
|
107
|
+
"/repo/.claude/skills/release/SKILL.md",
|
|
108
|
+
]);
|
|
109
|
+
const agent = result.components.find(
|
|
110
|
+
(component) => getProp(component, "cdx:file:kind") === "agent-config",
|
|
111
|
+
);
|
|
112
|
+
const skill = result.components.find(
|
|
113
|
+
(component) => getProp(component, "cdx:file:kind") === "skill-file",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
assert.strictEqual(
|
|
117
|
+
getProp(agent, "cdx:agent:description"),
|
|
118
|
+
"Deploy with https://example.com/release and [redacted]",
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(
|
|
121
|
+
getProp(agent, "cdx:agent:permission"),
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
endpoints: ["https://example.com/private"],
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
assert.strictEqual(
|
|
127
|
+
getProp(skill, "cdx:skill:metadata"),
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
endpoint: "https://example.com/skill",
|
|
130
|
+
apiKey: "[redacted]",
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
63
134
|
});
|
package/lib/helpers/depsUtils.js
CHANGED
|
@@ -272,6 +272,11 @@ export function trimComponents(components) {
|
|
|
272
272
|
if (!existIdent.methods) {
|
|
273
273
|
existIdent.methods = [];
|
|
274
274
|
}
|
|
275
|
+
if (aident.tools?.length) {
|
|
276
|
+
existIdent.tools = Array.from(
|
|
277
|
+
new Set([...(existIdent.tools || []), ...aident.tools]),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
275
280
|
let isDup = false;
|
|
276
281
|
for (const emethod of existIdent.methods) {
|
|
277
282
|
if (emethod?.value === amethod?.value) {
|