@cyclonedx/cdxgen 12.3.1 → 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/bin/cdxgen.js +1 -2
- package/data/rules/ai-agent-governance.yaml +43 -0
- package/data/rules/ci-permissions.yaml +132 -0
- package/data/rules/dependency-sources.yaml +65 -5
- package/data/rules/mcp-servers.yaml +36 -2
- package/data/rules/package-integrity.yaml +22 -0
- package/lib/cli/index.js +436 -56
- package/lib/cli/index.poku.js +875 -2
- package/lib/helpers/agentFormulationParser.js +10 -3
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/aiInventory.js +262 -0
- package/lib/helpers/aiInventory.poku.js +111 -0
- package/lib/helpers/analyzer.js +413 -54
- package/lib/helpers/analyzer.poku.js +117 -0
- package/lib/helpers/auditCategories.js +76 -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 +50 -24
- package/lib/helpers/display.poku.js +70 -58
- package/lib/helpers/formulationParsers.js +26 -6
- package/lib/helpers/jsonLike.js +21 -20
- package/lib/helpers/jsonLike.poku.js +34 -0
- package/lib/helpers/mcpConfigParser.js +32 -16
- package/lib/helpers/mcpConfigParser.poku.js +104 -0
- package/lib/helpers/mcpDiscovery.js +13 -23
- package/lib/helpers/mcpDiscovery.poku.js +21 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/utils.js +953 -41
- package/lib/helpers/utils.poku.js +901 -1
- 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/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +15 -0
- package/lib/stages/postgen/auditBom.js +12 -6
- package/lib/stages/postgen/auditBom.poku.js +755 -6
- package/lib/stages/postgen/postgen.js +229 -6
- package/lib/stages/postgen/postgen.poku.js +180 -0
- package/package.json +2 -1
- package/types/lib/cli/index.d.ts +1 -0
- package/types/lib/cli/index.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 +5 -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/chromextutils.d.ts.map +1 -1
- 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.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/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/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +31 -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
- 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/lib/helpers/display.js
CHANGED
|
@@ -65,10 +65,44 @@ const formatComponentName = (component, highlight) => {
|
|
|
65
65
|
return displayName;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Builds the summary and provenance lines printed after the component table.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} bomJson CycloneDX BOM JSON object
|
|
72
|
+
* @param {string[]|undefined} filterTypes Optional list of component types to include
|
|
73
|
+
* @param {string|undefined} summaryText Optional summary message to print after the table
|
|
74
|
+
* @param {number} displayedProvenanceCount Number of displayed components with registry provenance
|
|
75
|
+
* @returns {string[]} Summary lines to print
|
|
76
|
+
*/
|
|
77
|
+
export const buildTableSummaryLines = (
|
|
78
|
+
bomJson,
|
|
79
|
+
filterTypes,
|
|
80
|
+
summaryText,
|
|
81
|
+
displayedProvenanceCount = 0,
|
|
82
|
+
) => {
|
|
83
|
+
const summaryLines = [];
|
|
84
|
+
if (summaryText) {
|
|
85
|
+
summaryLines.push(summaryText);
|
|
86
|
+
} else if (!filterTypes) {
|
|
87
|
+
summaryLines.push(
|
|
88
|
+
`BOM includes ${bomJson?.components?.length || 0} components and ${
|
|
89
|
+
bomJson?.dependencies?.length || 0
|
|
90
|
+
} dependencies`,
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
summaryLines.push(
|
|
94
|
+
`Components filtered based on type: ${filterTypes.join(", ")}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (displayedProvenanceCount > 0) {
|
|
98
|
+
summaryLines.push(
|
|
99
|
+
`Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
|
|
100
|
+
);
|
|
101
|
+
summaryLines.push(
|
|
102
|
+
`${REGISTRY_PROVENANCE_ICON} ${displayedProvenanceCount} component(s) include registry provenance or trusted publishing metadata.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return summaryLines;
|
|
72
106
|
};
|
|
73
107
|
|
|
74
108
|
/**
|
|
@@ -247,24 +281,13 @@ export function printTable(
|
|
|
247
281
|
}
|
|
248
282
|
stream.end();
|
|
249
283
|
console.log();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
bomJson?.dependencies?.length || 0,
|
|
258
|
-
"dependencies",
|
|
259
|
-
);
|
|
260
|
-
} else {
|
|
261
|
-
console.log(`Components filtered based on type: ${filterTypes.join(", ")}`);
|
|
262
|
-
}
|
|
263
|
-
if (displayedProvenanceCount > 0) {
|
|
264
|
-
printProvenanceLegend();
|
|
265
|
-
console.log(
|
|
266
|
-
`${REGISTRY_PROVENANCE_ICON} ${displayedProvenanceCount} component(s) include registry provenance or trusted publishing metadata.`,
|
|
267
|
-
);
|
|
284
|
+
for (const line of buildTableSummaryLines(
|
|
285
|
+
bomJson,
|
|
286
|
+
filterTypes,
|
|
287
|
+
summaryText,
|
|
288
|
+
displayedProvenanceCount,
|
|
289
|
+
)) {
|
|
290
|
+
console.log(line);
|
|
268
291
|
}
|
|
269
292
|
}
|
|
270
293
|
const formatProps = (props) => {
|
|
@@ -1103,7 +1126,7 @@ export function displaySelfThreatModel(
|
|
|
1103
1126
|
options,
|
|
1104
1127
|
envAuditFindings,
|
|
1105
1128
|
) {
|
|
1106
|
-
const TLP = options.tlpClassification
|
|
1129
|
+
const TLP = options.tlpClassification;
|
|
1107
1130
|
const risks = [];
|
|
1108
1131
|
let riskScore = 0;
|
|
1109
1132
|
|
|
@@ -1242,8 +1265,11 @@ export function displaySelfThreatModel(
|
|
|
1242
1265
|
AMBER_AND_STRICT: "Organisation only. No external sharing.",
|
|
1243
1266
|
RED: "Named recipients only. Do not forward or store beyond session.",
|
|
1244
1267
|
};
|
|
1268
|
+
const tlpValue = TLP
|
|
1269
|
+
? `${TLP} — ${tlpGuidance[TLP]}`
|
|
1270
|
+
: "Not set — no distribution constraints recorded.";
|
|
1245
1271
|
const headerData = [
|
|
1246
|
-
["TLP Classification",
|
|
1272
|
+
["TLP Classification", tlpValue],
|
|
1247
1273
|
["Risk Score", `${riskScore}/10`],
|
|
1248
1274
|
["Risk Level", `${riskColor[riskLevel]}${riskLevel}${reset}`],
|
|
1249
1275
|
];
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
buildActivitySummaryPayload,
|
|
9
9
|
buildDependencyTreeLegendLines,
|
|
10
10
|
buildDependencyTreeLines,
|
|
11
|
+
buildTableSummaryLines,
|
|
11
12
|
printDependencyTree,
|
|
12
13
|
serializeActivitySummary,
|
|
13
14
|
} from "./display.js";
|
|
@@ -22,74 +23,85 @@ it("print tree test", () => {
|
|
|
22
23
|
|
|
23
24
|
it("prints a provenance icon for registry-backed components", async () => {
|
|
24
25
|
const rows = [];
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const { printTable } = await esmock("./display.js", {
|
|
28
|
-
"./table.js": {
|
|
29
|
-
createStream: () => ({
|
|
30
|
-
end() {
|
|
31
|
-
// intentional no-op for stream stub
|
|
32
|
-
},
|
|
33
|
-
write(row) {
|
|
34
|
-
rows.push(row);
|
|
35
|
-
},
|
|
36
|
-
}),
|
|
37
|
-
table: sinon.stub().returns(""),
|
|
38
|
-
},
|
|
39
|
-
"./utils.js": {
|
|
40
|
-
isSecureMode: false,
|
|
41
|
-
safeExistsSync: sinon.stub(),
|
|
42
|
-
toCamel: sinon.stub(),
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
printTable(
|
|
26
|
+
const bomJson = {
|
|
27
|
+
components: [
|
|
47
28
|
{
|
|
48
|
-
|
|
29
|
+
group: "",
|
|
30
|
+
name: "left-pad",
|
|
31
|
+
properties: [
|
|
49
32
|
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
properties: [
|
|
53
|
-
{
|
|
54
|
-
name: "cdx:npm:provenanceUrl",
|
|
55
|
-
value:
|
|
56
|
-
"https://registry.npmjs.org/-/npm/v1/attestations/left-pad",
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
type: "library",
|
|
60
|
-
version: "1.3.0",
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
group: "",
|
|
64
|
-
name: "lodash",
|
|
65
|
-
properties: [],
|
|
66
|
-
type: "library",
|
|
67
|
-
version: "4.17.21",
|
|
33
|
+
name: "cdx:npm:provenanceUrl",
|
|
34
|
+
value: "https://registry.npmjs.org/-/npm/v1/attestations/left-pad",
|
|
68
35
|
},
|
|
69
36
|
],
|
|
70
|
-
|
|
37
|
+
type: "library",
|
|
38
|
+
version: "1.3.0",
|
|
71
39
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
40
|
+
{
|
|
41
|
+
group: "",
|
|
42
|
+
name: "lodash",
|
|
43
|
+
properties: [],
|
|
44
|
+
type: "library",
|
|
45
|
+
version: "4.17.21",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
dependencies: [],
|
|
49
|
+
};
|
|
50
|
+
const { printTable } = await esmock("./display.js", {
|
|
51
|
+
"./table.js": {
|
|
52
|
+
createStream: () => ({
|
|
53
|
+
end() {
|
|
54
|
+
// intentional no-op for stream stub
|
|
55
|
+
},
|
|
56
|
+
write(row) {
|
|
57
|
+
rows.push(row);
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
table: sinon.stub().returns(""),
|
|
61
|
+
},
|
|
62
|
+
"./utils.js": {
|
|
63
|
+
isSecureMode: false,
|
|
64
|
+
safeExistsSync: sinon.stub(),
|
|
65
|
+
toCamel: sinon.stub(),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
76
68
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
printTable(bomJson, undefined, undefined, "Found 1 trusted component.");
|
|
70
|
+
|
|
71
|
+
assert.strictEqual(rows[1][1], `${REGISTRY_PROVENANCE_ICON} left-pad`);
|
|
72
|
+
assert.strictEqual(rows[2][1], "lodash");
|
|
73
|
+
assert.deepStrictEqual(
|
|
74
|
+
buildTableSummaryLines(bomJson, undefined, "Found 1 trusted component.", 1),
|
|
75
|
+
[
|
|
81
76
|
"Found 1 trusted component.",
|
|
82
|
-
);
|
|
83
|
-
sinon.assert.calledWithExactly(
|
|
84
|
-
consoleLogStub,
|
|
85
77
|
`Legend: ${REGISTRY_PROVENANCE_ICON} = registry provenance or trusted publishing evidence`,
|
|
86
|
-
);
|
|
87
|
-
sinon.assert.calledWithExactly(
|
|
88
|
-
consoleLogStub,
|
|
89
78
|
`${REGISTRY_PROVENANCE_ICON} 1 component(s) include registry provenance or trusted publishing metadata.`,
|
|
90
|
-
|
|
79
|
+
],
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("displaySelfThreatModel does not assume a default TLP classification", async () => {
|
|
84
|
+
const tableStub = sinon.stub().returns("table-output");
|
|
85
|
+
try {
|
|
86
|
+
const { displaySelfThreatModel } = await esmock("./display.js", {
|
|
87
|
+
"./table.js": {
|
|
88
|
+
createStream: sinon.stub(),
|
|
89
|
+
table: tableStub,
|
|
90
|
+
},
|
|
91
|
+
"./utils.js": {
|
|
92
|
+
isSecureMode: false,
|
|
93
|
+
safeExistsSync: sinon.stub(),
|
|
94
|
+
toCamel: sinon.stub().callsFake((value) => value),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
displaySelfThreatModel("/workspace/project", {}, {}, []);
|
|
98
|
+
const [headerData] = tableStub.firstCall.args;
|
|
99
|
+
assert.deepStrictEqual(headerData[0], [
|
|
100
|
+
"TLP Classification",
|
|
101
|
+
"Not set — no distribution constraints recorded.",
|
|
102
|
+
]);
|
|
91
103
|
} finally {
|
|
92
|
-
|
|
104
|
+
sinon.restore();
|
|
93
105
|
}
|
|
94
106
|
});
|
|
95
107
|
|
|
@@ -4,14 +4,17 @@ import process from "node:process";
|
|
|
4
4
|
|
|
5
5
|
import { v4 as uuidv4 } from "uuid";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AI_INVENTORY_PROJECT_TYPES,
|
|
9
|
+
collectAiInventory,
|
|
10
|
+
optionIncludesAiInventoryProjectType,
|
|
11
|
+
} from "./aiInventory.js";
|
|
8
12
|
import { collectOSCryptoLibs } from "./cbomutils.js";
|
|
9
13
|
import { azurePipelinesParser } from "./ciParsers/azurePipelines.js";
|
|
10
14
|
import { circleCiParser } from "./ciParsers/circleCi.js";
|
|
11
15
|
import { githubActionsParser } from "./ciParsers/githubActions.js";
|
|
12
16
|
import { gitlabCiParser } from "./ciParsers/gitlabCi.js";
|
|
13
17
|
import { jenkinsParser } from "./ciParsers/jenkins.js";
|
|
14
|
-
import { communityAiConfigParser } from "./communityAiConfigParser.js";
|
|
15
18
|
import { trimComponents } from "./depsUtils.js";
|
|
16
19
|
import {
|
|
17
20
|
collectEnvInfo,
|
|
@@ -20,7 +23,6 @@ import {
|
|
|
20
23
|
gitTreeHashes,
|
|
21
24
|
listFiles,
|
|
22
25
|
} from "./envcontext.js";
|
|
23
|
-
import { mcpConfigParser } from "./mcpConfigParser.js";
|
|
24
26
|
import { rustFormulationParser } from "./rustFormulationParser.js";
|
|
25
27
|
import { scanTextForHiddenUnicode } from "./unicodeScan.js";
|
|
26
28
|
import { getAllFiles } from "./utils.js";
|
|
@@ -100,9 +102,6 @@ function buildReadmeSecurityComponents(discoveryPath, options) {
|
|
|
100
102
|
*/
|
|
101
103
|
const _parsers = [
|
|
102
104
|
rustFormulationParser,
|
|
103
|
-
agentFormulationParser,
|
|
104
|
-
mcpConfigParser,
|
|
105
|
-
communityAiConfigParser,
|
|
106
105
|
githubActionsParser,
|
|
107
106
|
gitlabCiParser,
|
|
108
107
|
jenkinsParser,
|
|
@@ -312,6 +311,12 @@ export function addFormulationSection(filePath, options, context = {}) {
|
|
|
312
311
|
const ciProperties = [];
|
|
313
312
|
|
|
314
313
|
const discoveryPath = projectPath || ".";
|
|
314
|
+
const excludedInventoryTypes = AI_INVENTORY_PROJECT_TYPES.filter((type) => {
|
|
315
|
+
return optionIncludesAiInventoryProjectType(options?.excludeType, type);
|
|
316
|
+
});
|
|
317
|
+
const includedInventoryTypes = AI_INVENTORY_PROJECT_TYPES.filter(
|
|
318
|
+
(type) => !excludedInventoryTypes.includes(type),
|
|
319
|
+
);
|
|
315
320
|
|
|
316
321
|
for (const parser of _parsers) {
|
|
317
322
|
const matchedFiles = [];
|
|
@@ -355,6 +360,21 @@ export function addFormulationSection(filePath, options, context = {}) {
|
|
|
355
360
|
}
|
|
356
361
|
}
|
|
357
362
|
|
|
363
|
+
const aiInventory = collectAiInventory(
|
|
364
|
+
discoveryPath,
|
|
365
|
+
options,
|
|
366
|
+
includedInventoryTypes,
|
|
367
|
+
);
|
|
368
|
+
if (aiInventory.components.length) {
|
|
369
|
+
ciComponents.push(...aiInventory.components);
|
|
370
|
+
}
|
|
371
|
+
if (aiInventory.services.length) {
|
|
372
|
+
ciServices.push(...aiInventory.services);
|
|
373
|
+
}
|
|
374
|
+
if (aiInventory.dependencies.length) {
|
|
375
|
+
dependencies.push(...aiInventory.dependencies);
|
|
376
|
+
}
|
|
377
|
+
|
|
358
378
|
// Merge CI components into the formulation component list
|
|
359
379
|
if (ciComponents.length) {
|
|
360
380
|
components = components.concat(ciComponents);
|
package/lib/helpers/jsonLike.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether the quote at `index` is escaped by an odd-length run of
|
|
3
|
+
* backslashes immediately preceding it.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} raw Raw JSON-like text being scanned
|
|
6
|
+
* @param {number} index Index of the quote character to evaluate
|
|
7
|
+
* @returns {boolean} `true` when the quote is escaped and should not terminate
|
|
8
|
+
* the current string literal
|
|
9
|
+
*/
|
|
10
|
+
function isEscapedQuote(raw, index) {
|
|
11
|
+
let backslashCount = 0;
|
|
12
|
+
let lookBehind = index - 1;
|
|
13
|
+
while (lookBehind >= 0 && raw[lookBehind] === "\\") {
|
|
14
|
+
backslashCount += 1;
|
|
15
|
+
lookBehind -= 1;
|
|
16
|
+
}
|
|
17
|
+
return backslashCount % 2 === 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
export function stripJsonComments(raw) {
|
|
2
21
|
let output = "";
|
|
3
22
|
let inString = false;
|
|
4
23
|
let stringQuote = "";
|
|
5
|
-
let escaped = false;
|
|
6
24
|
for (let index = 0; index < raw.length; index++) {
|
|
7
25
|
const char = raw[index];
|
|
8
26
|
const nextChar = raw[index + 1];
|
|
9
27
|
if (inString) {
|
|
10
28
|
output += char;
|
|
11
|
-
if (
|
|
12
|
-
escaped = false;
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (char === "\\") {
|
|
16
|
-
escaped = true;
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
if (char === stringQuote) {
|
|
29
|
+
if (char === stringQuote && !isEscapedQuote(raw, index)) {
|
|
20
30
|
inString = false;
|
|
21
31
|
stringQuote = "";
|
|
22
32
|
}
|
|
@@ -57,20 +67,11 @@ export function stripJsonTrailingCommas(raw) {
|
|
|
57
67
|
let output = "";
|
|
58
68
|
let inString = false;
|
|
59
69
|
let stringQuote = "";
|
|
60
|
-
let escaped = false;
|
|
61
70
|
for (let index = 0; index < raw.length; index++) {
|
|
62
71
|
const char = raw[index];
|
|
63
72
|
if (inString) {
|
|
64
73
|
output += char;
|
|
65
|
-
if (
|
|
66
|
-
escaped = false;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (char === "\\") {
|
|
70
|
-
escaped = true;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (char === stringQuote) {
|
|
74
|
+
if (char === stringQuote && !isEscapedQuote(raw, index)) {
|
|
74
75
|
inString = false;
|
|
75
76
|
stringQuote = "";
|
|
76
77
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { parseJsonLike, stripJsonComments } from "./jsonLike.js";
|
|
4
|
+
|
|
5
|
+
describe("jsonLike", () => {
|
|
6
|
+
it("preserves escaped quotes while stripping comments", () => {
|
|
7
|
+
const parsedMessage = 'escaped quote: " // not a comment';
|
|
8
|
+
const rawMessage = String.raw`escaped quote: \" // not a comment`;
|
|
9
|
+
const raw = String.raw`{
|
|
10
|
+
"message": "escaped quote: \" // not a comment",
|
|
11
|
+
// trailing comment
|
|
12
|
+
"enabled": true
|
|
13
|
+
}`;
|
|
14
|
+
const stripped = stripJsonComments(raw);
|
|
15
|
+
assert.ok(stripped.includes(rawMessage));
|
|
16
|
+
assert.ok(!stripped.includes("trailing comment"));
|
|
17
|
+
assert.deepStrictEqual(parseJsonLike(raw), {
|
|
18
|
+
enabled: true,
|
|
19
|
+
message: parsedMessage,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("preserves comment markers after escaped backslashes inside strings", () => {
|
|
24
|
+
const raw = `{
|
|
25
|
+
"path": "C:\\\\\\\\temp\\\\\\\\file // keep",
|
|
26
|
+
/* block comment */
|
|
27
|
+
"count": 1
|
|
28
|
+
}`;
|
|
29
|
+
assert.deepStrictEqual(parseJsonLike(raw), {
|
|
30
|
+
count: 1,
|
|
31
|
+
path: "C:\\\\temp\\\\file // keep",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
providerNamesForText,
|
|
10
10
|
sanitizeMcpRefToken,
|
|
11
11
|
} from "./mcpDiscovery.js";
|
|
12
|
+
import {
|
|
13
|
+
sanitizeBomPropertyValue,
|
|
14
|
+
sanitizeBomUrl,
|
|
15
|
+
} from "./propertySanitizer.js";
|
|
12
16
|
import { scanTextForHiddenUnicode } from "./unicodeScan.js";
|
|
13
17
|
|
|
14
18
|
const MCP_CONFIG_PATTERNS = [
|
|
@@ -40,13 +44,22 @@ const SECRET_FIELD_NAME_PATTERN =
|
|
|
40
44
|
const ENV_REFERENCE_PATTERN = /(?:\$\{?[A-Z0-9_]+\}?|%[A-Z0-9_]+%)/u;
|
|
41
45
|
|
|
42
46
|
function addUniqueProperty(properties, name, value) {
|
|
43
|
-
|
|
47
|
+
const sanitizedValue = sanitizeBomPropertyValue(name, value);
|
|
48
|
+
if (
|
|
49
|
+
sanitizedValue === undefined ||
|
|
50
|
+
sanitizedValue === null ||
|
|
51
|
+
sanitizedValue === ""
|
|
52
|
+
) {
|
|
44
53
|
return;
|
|
45
54
|
}
|
|
46
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
properties.some(
|
|
57
|
+
(prop) => prop.name === name && prop.value === String(sanitizedValue),
|
|
58
|
+
)
|
|
59
|
+
) {
|
|
47
60
|
return;
|
|
48
61
|
}
|
|
49
|
-
properties.push({ name, value: String(
|
|
62
|
+
properties.push({ name, value: String(sanitizedValue) });
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
function normalizeFilePath(filePath) {
|
|
@@ -214,6 +227,9 @@ function detectConfigCredentialSignals(serverConfig) {
|
|
|
214
227
|
}
|
|
215
228
|
}
|
|
216
229
|
return {
|
|
230
|
+
credentialIndicatorCount: inlineIndicators.size,
|
|
231
|
+
credentialReferenceCount: credentialRefs.size,
|
|
232
|
+
exposureFieldCount: exposureFields.size,
|
|
217
233
|
credentialRefs: Array.from(credentialRefs).sort(),
|
|
218
234
|
exposureFields: Array.from(exposureFields).sort(),
|
|
219
235
|
inlineIndicators: Array.from(inlineIndicators).sort(),
|
|
@@ -377,6 +393,9 @@ function createServiceFromConfig(
|
|
|
377
393
|
const command = normalized.command;
|
|
378
394
|
const args = normalized.args;
|
|
379
395
|
const endpoints = extractEndpoints(serverConfig);
|
|
396
|
+
const sanitizedEndpoints = endpoints.map((endpoint) =>
|
|
397
|
+
sanitizeBomUrl(endpoint),
|
|
398
|
+
);
|
|
380
399
|
const transport = inferTransport(serverConfig, endpoints);
|
|
381
400
|
const authHints = authHintsFromValue(serverConfig);
|
|
382
401
|
const authPosture = authPostureForConfig(serverConfig, endpoints, authHints);
|
|
@@ -393,7 +412,7 @@ function createServiceFromConfig(
|
|
|
393
412
|
JSON.stringify({
|
|
394
413
|
args,
|
|
395
414
|
command,
|
|
396
|
-
endpoints,
|
|
415
|
+
endpoints: sanitizedEndpoints,
|
|
397
416
|
env: serverConfig?.env || serverConfig?.environment || {},
|
|
398
417
|
}),
|
|
399
418
|
);
|
|
@@ -468,22 +487,22 @@ function createServiceFromConfig(
|
|
|
468
487
|
addUniqueProperty(properties, "cdx:mcp:credentialExposure", "true");
|
|
469
488
|
addUniqueProperty(
|
|
470
489
|
properties,
|
|
471
|
-
"cdx:mcp:
|
|
472
|
-
credentialSignals.
|
|
490
|
+
"cdx:mcp:credentialIndicatorCount",
|
|
491
|
+
String(credentialSignals.credentialIndicatorCount),
|
|
473
492
|
);
|
|
474
493
|
}
|
|
475
494
|
if (credentialSignals.exposureFields.length) {
|
|
476
495
|
addUniqueProperty(
|
|
477
496
|
properties,
|
|
478
|
-
"cdx:mcp:
|
|
479
|
-
credentialSignals.
|
|
497
|
+
"cdx:mcp:credentialExposureFieldCount",
|
|
498
|
+
String(credentialSignals.exposureFieldCount),
|
|
480
499
|
);
|
|
481
500
|
}
|
|
482
501
|
if (credentialSignals.credentialRefs.length) {
|
|
483
502
|
addUniqueProperty(
|
|
484
503
|
properties,
|
|
485
|
-
"cdx:mcp:
|
|
486
|
-
credentialSignals.
|
|
504
|
+
"cdx:mcp:credentialReferenceCount",
|
|
505
|
+
String(credentialSignals.credentialReferenceCount),
|
|
487
506
|
);
|
|
488
507
|
}
|
|
489
508
|
if (supportsDcr) {
|
|
@@ -531,7 +550,7 @@ function createServiceFromConfig(
|
|
|
531
550
|
return {
|
|
532
551
|
"bom-ref": `urn:service:mcp:${sanitizeMcpRefToken(serviceName)}:${sanitizeMcpRefToken(version)}`,
|
|
533
552
|
authenticated,
|
|
534
|
-
endpoints,
|
|
553
|
+
endpoints: sanitizedEndpoints,
|
|
535
554
|
group: "mcp",
|
|
536
555
|
name: serviceName,
|
|
537
556
|
properties,
|
|
@@ -593,11 +612,8 @@ function createConfigComponent(filePath, format, raw, services) {
|
|
|
593
612
|
addUniqueProperty(properties, "cdx:mcp:credentialExposure", "true");
|
|
594
613
|
addUniqueProperty(
|
|
595
614
|
properties,
|
|
596
|
-
"cdx:mcp:
|
|
597
|
-
credentialServices
|
|
598
|
-
.map((service) => service.name)
|
|
599
|
-
.sort()
|
|
600
|
-
.join(","),
|
|
615
|
+
"cdx:mcp:credentialExposedServiceCount",
|
|
616
|
+
String(credentialServices.length),
|
|
601
617
|
);
|
|
602
618
|
}
|
|
603
619
|
return {
|
|
@@ -56,4 +56,108 @@ describe("mcpConfigParser", () => {
|
|
|
56
56
|
syntax: "json",
|
|
57
57
|
});
|
|
58
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://user:pass@docs.example.com/mcp?access_token=secret#frag",
|
|
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
|
+
"4",
|
|
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(getProp(service, "cdx:mcp:command"), "npx");
|
|
113
|
+
assert.deepStrictEqual(service.endpoints, ["https://docs.example.com/mcp"]);
|
|
114
|
+
assert.strictEqual(
|
|
115
|
+
getProp(component, "cdx:mcp:configuredEndpoints"),
|
|
116
|
+
"https://docs.example.com/mcp",
|
|
117
|
+
);
|
|
118
|
+
assert.strictEqual(
|
|
119
|
+
getProp(service, "cdx:mcp:credentialRiskIndicators"),
|
|
120
|
+
undefined,
|
|
121
|
+
);
|
|
122
|
+
assert.strictEqual(
|
|
123
|
+
getProp(service, "cdx:mcp:credentialExposureFields"),
|
|
124
|
+
undefined,
|
|
125
|
+
);
|
|
126
|
+
assert.strictEqual(getProp(service, "cdx:mcp:credentialRefs"), undefined);
|
|
127
|
+
assert.strictEqual(
|
|
128
|
+
getProp(component, "cdx:mcp:credentialExposedServices"),
|
|
129
|
+
undefined,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("summarizes Windows executable paths with spaces safely", async () => {
|
|
134
|
+
const readFileSync = sinon.stub();
|
|
135
|
+
const scanTextForHiddenUnicode = sinon.stub().returns({
|
|
136
|
+
hasHiddenUnicode: false,
|
|
137
|
+
});
|
|
138
|
+
readFileSync.withArgs("/repo/.vscode/mcp.json", "utf-8").returns(
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
mcpServers: {
|
|
141
|
+
releaseDocs: {
|
|
142
|
+
args: ["--inspect"],
|
|
143
|
+
command: "C:\\Program Files\\nodejs\\node.exe --inspect",
|
|
144
|
+
mcp: true,
|
|
145
|
+
transport: "stdio",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
const { mcpConfigParser } = await esmock("./mcpConfigParser.js", {
|
|
151
|
+
"node:fs": { readFileSync },
|
|
152
|
+
"./unicodeScan.js": { scanTextForHiddenUnicode },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = mcpConfigParser.parse(["/repo/.vscode/mcp.json"]);
|
|
156
|
+
|
|
157
|
+
assert.strictEqual(
|
|
158
|
+
getProp(result.services[0], "cdx:mcp:command") ||
|
|
159
|
+
getProp(result.components[0], "cdx:mcp:command"),
|
|
160
|
+
"node.exe",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
59
163
|
});
|