@bryan-thompson/inspector-assessment-cli 1.32.2 ā 1.32.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/build/__tests__/assessment-runner/server-config.test.js +8 -5
- package/build/__tests__/jsonl-events.test.js +195 -1
- package/build/__tests__/lib/server-configSchemas.test.js +314 -0
- package/build/__tests__/lib/zodErrorFormatter.test.js +721 -0
- package/build/__tests__/security/security-pattern-count.test.js +245 -0
- package/build/assess-security.js +11 -77
- package/build/lib/assessment-runner/__tests__/server-config.test.js +116 -0
- package/build/lib/assessment-runner/assessment-executor.js +18 -1
- package/build/lib/assessment-runner/config-builder.js +10 -0
- package/build/lib/assessment-runner/server-config.js +43 -35
- package/build/lib/assessment-runner/server-configSchemas.js +4 -1
- package/build/lib/jsonl-events.js +59 -0
- package/build/lib/zodErrorFormatter.js +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Pattern Count Consistency Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that all references to security pattern count are consistent across:
|
|
5
|
+
* - CLI help messages
|
|
6
|
+
* - Console output
|
|
7
|
+
* - Configuration defaults
|
|
8
|
+
* - Documentation comments
|
|
9
|
+
*
|
|
10
|
+
* Addresses QA requirement: verify all references to security pattern count are consistent (30 patterns).
|
|
11
|
+
*
|
|
12
|
+
* NOTE: The DEFAULT_ASSESSMENT_CONFIG uses 8 patterns (for Anthropic's basic security testing).
|
|
13
|
+
* CLI tools override this to 30 patterns for comprehensive security assessment.
|
|
14
|
+
* This test verifies consistency within each context.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from "@jest/globals";
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
// Get the root directory of the project (one level up from cli/)
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
const projectRoot = path.resolve(__dirname, "../../../.."); // From cli/src/__tests__/security to root
|
|
24
|
+
describe("Security Pattern Count Consistency", () => {
|
|
25
|
+
describe("CLI assess-security references", () => {
|
|
26
|
+
it("should use consistent pattern count in assess-security.ts", () => {
|
|
27
|
+
const filePath = path.join(projectRoot, "cli/src/assess-security.ts");
|
|
28
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
29
|
+
// Find all references to pattern count
|
|
30
|
+
const references = [
|
|
31
|
+
{ match: /securityPatternsToTest:\s*(\d+)/, name: "config value" },
|
|
32
|
+
{
|
|
33
|
+
match: /Running security assessment with (\d+) attack patterns/,
|
|
34
|
+
name: "console log",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
match: /Run security assessment.*with (\d+) attack patterns/,
|
|
38
|
+
name: "help text header",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
match: /Attack Patterns Tested \((\d+) total\)/,
|
|
42
|
+
name: "help text section",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const expectedCount = 30;
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const ref of references) {
|
|
48
|
+
const match = content.match(ref.match);
|
|
49
|
+
if (match && match[1]) {
|
|
50
|
+
const count = parseInt(match[1], 10);
|
|
51
|
+
findings.push({ name: ref.name, value: count });
|
|
52
|
+
expect(count).toBe(expectedCount);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Ensure we found at least 3 references (config + 2 messages)
|
|
56
|
+
expect(findings.length).toBeGreaterThanOrEqual(3);
|
|
57
|
+
// Verify all found references match
|
|
58
|
+
const allMatch = findings.every((f) => f.value === expectedCount);
|
|
59
|
+
expect(allMatch).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it("should document 30 patterns in help text", () => {
|
|
62
|
+
const filePath = path.join(projectRoot, "cli/src/assess-security.ts");
|
|
63
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
64
|
+
// Verify help text explicitly mentions 30 patterns
|
|
65
|
+
expect(content).toContain("30 attack patterns");
|
|
66
|
+
expect(content).toContain("Attack Patterns Tested (30 total)");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("Configuration consistency", () => {
|
|
70
|
+
it("should use 8 patterns for DEFAULT_ASSESSMENT_CONFIG (Anthropic basic)", () => {
|
|
71
|
+
const filePath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
72
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
73
|
+
// DEFAULT_ASSESSMENT_CONFIG should use 8 patterns (Anthropic's basic requirement)
|
|
74
|
+
const defaultConfigMatch = content.match(/DEFAULT_ASSESSMENT_CONFIG[\s\S]*?securityPatternsToTest:\s*(\d+)/);
|
|
75
|
+
expect(defaultConfigMatch).toBeTruthy();
|
|
76
|
+
if (defaultConfigMatch) {
|
|
77
|
+
const count = parseInt(defaultConfigMatch[1], 10);
|
|
78
|
+
expect(count).toBe(8);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it("should use 3 patterns for REVIEWER_MODE_CONFIG (fast review)", () => {
|
|
82
|
+
const filePath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
83
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
84
|
+
// REVIEWER_MODE_CONFIG should use 3 patterns (optimized for speed)
|
|
85
|
+
const reviewerConfigMatch = content.match(/REVIEWER_MODE_CONFIG[\s\S]*?securityPatternsToTest:\s*(\d+)/);
|
|
86
|
+
expect(reviewerConfigMatch).toBeTruthy();
|
|
87
|
+
if (reviewerConfigMatch) {
|
|
88
|
+
const count = parseInt(reviewerConfigMatch[1], 10);
|
|
89
|
+
expect(count).toBe(3);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
it("should use 8 patterns for DEVELOPER_MODE_CONFIG (comprehensive)", () => {
|
|
93
|
+
const filePath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
94
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
95
|
+
// DEVELOPER_MODE_CONFIG should use 8 patterns (comprehensive testing)
|
|
96
|
+
const developerConfigMatch = content.match(/DEVELOPER_MODE_CONFIG[\s\S]*?securityPatternsToTest:\s*(\d+)/);
|
|
97
|
+
expect(developerConfigMatch).toBeTruthy();
|
|
98
|
+
if (developerConfigMatch) {
|
|
99
|
+
const count = parseInt(developerConfigMatch[1], 10);
|
|
100
|
+
expect(count).toBe(8);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it("should use 8 patterns for AUDIT_MODE_CONFIG (compliance)", () => {
|
|
104
|
+
const filePath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
105
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
106
|
+
// AUDIT_MODE_CONFIG should use 8 patterns (compliance validation)
|
|
107
|
+
const auditConfigMatch = content.match(/AUDIT_MODE_CONFIG[\s\S]*?securityPatternsToTest:\s*(\d+)/);
|
|
108
|
+
expect(auditConfigMatch).toBeTruthy();
|
|
109
|
+
if (auditConfigMatch) {
|
|
110
|
+
const count = parseInt(auditConfigMatch[1], 10);
|
|
111
|
+
expect(count).toBe(8);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
it("should use 8 patterns for CLAUDE_ENHANCED_AUDIT_CONFIG", () => {
|
|
115
|
+
const filePath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
116
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
117
|
+
// CLAUDE_ENHANCED_AUDIT_CONFIG should use 8 patterns (enhanced validation)
|
|
118
|
+
const claudeConfigMatch = content.match(/CLAUDE_ENHANCED_AUDIT_CONFIG[\s\S]*?securityPatternsToTest:\s*(\d+)/);
|
|
119
|
+
expect(claudeConfigMatch).toBeTruthy();
|
|
120
|
+
if (claudeConfigMatch) {
|
|
121
|
+
const count = parseInt(claudeConfigMatch[1], 10);
|
|
122
|
+
expect(count).toBe(8);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("Documentation consistency", () => {
|
|
127
|
+
it("should reference correct pattern counts in config comments", () => {
|
|
128
|
+
const filePath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
129
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
130
|
+
// Check inline comments for pattern counts
|
|
131
|
+
// Default: "default all 8" or "default 8"
|
|
132
|
+
expect(content).toMatch(/securityPatternsToTest\?\s*:\s*number;.*default.*8/i);
|
|
133
|
+
// Reviewer mode: "Test only 3 critical"
|
|
134
|
+
expect(content).toMatch(/securityPatternsToTest:\s*3;.*3 critical/i);
|
|
135
|
+
// Developer/audit modes: "all security patterns" or "all 8"
|
|
136
|
+
expect(content).toMatch(/securityPatternsToTest:\s*8;.*all.*8|8.*patterns/i);
|
|
137
|
+
});
|
|
138
|
+
it("should document pattern count in help text comments", () => {
|
|
139
|
+
const filePath = path.join(projectRoot, "cli/src/assess-security.ts");
|
|
140
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
141
|
+
// CLI uses 30 patterns (expanded from base 8)
|
|
142
|
+
expect(content).toContain("30 attack patterns");
|
|
143
|
+
expect(content).toContain("(30 total)");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe("Cross-file consistency", () => {
|
|
147
|
+
it("should have matching pattern counts between related files", () => {
|
|
148
|
+
// assess-security.ts: 30 patterns (CLI override)
|
|
149
|
+
const assessSecurityPath = path.join(projectRoot, "cli/src/assess-security.ts");
|
|
150
|
+
const assessSecurityContent = fs.readFileSync(assessSecurityPath, "utf-8");
|
|
151
|
+
const assessSecurityMatch = assessSecurityContent.match(/securityPatternsToTest:\s*(\d+)/);
|
|
152
|
+
expect(assessSecurityMatch).toBeTruthy();
|
|
153
|
+
if (assessSecurityMatch) {
|
|
154
|
+
const cliCount = parseInt(assessSecurityMatch[1], 10);
|
|
155
|
+
expect(cliCount).toBe(30); // CLI uses 30 patterns
|
|
156
|
+
}
|
|
157
|
+
// configTypes.ts: 8 patterns default (Anthropic basic)
|
|
158
|
+
const configTypesPath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
159
|
+
const configTypesContent = fs.readFileSync(configTypesPath, "utf-8");
|
|
160
|
+
const configMatch = configTypesContent.match(/DEFAULT_ASSESSMENT_CONFIG[\s\S]*?securityPatternsToTest:\s*(\d+)/);
|
|
161
|
+
expect(configMatch).toBeTruthy();
|
|
162
|
+
if (configMatch) {
|
|
163
|
+
const defaultCount = parseInt(configMatch[1], 10);
|
|
164
|
+
expect(defaultCount).toBe(8); // Default uses 8 patterns
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
it("should match pattern count in estimators comment", () => {
|
|
168
|
+
const estimatorsPath = path.join(projectRoot, "client/src/services/assessment/registry/estimators.ts");
|
|
169
|
+
if (fs.existsSync(estimatorsPath)) {
|
|
170
|
+
const content = fs.readFileSync(estimatorsPath, "utf-8");
|
|
171
|
+
// Verify comment mentions "default 8" for security patterns
|
|
172
|
+
expect(content).toMatch(/Security assessor:.*securityPatternsToTest.*default 8/i);
|
|
173
|
+
// Verify fallback value is 8
|
|
174
|
+
const fallbackMatch = content.match(/securityPatternsToTest.*\?\?.*(\d+)/);
|
|
175
|
+
if (fallbackMatch) {
|
|
176
|
+
const fallback = parseInt(fallbackMatch[1], 10);
|
|
177
|
+
expect(fallback).toBe(8);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe("Pattern count validation rules", () => {
|
|
183
|
+
it("should enforce consistent pattern counts across contexts", () => {
|
|
184
|
+
// Rule: CLI tools use 30 patterns (comprehensive security testing)
|
|
185
|
+
// Rule: Default config uses 8 patterns (Anthropic basic requirement)
|
|
186
|
+
// Rule: Reviewer mode uses 3 patterns (fast reviews)
|
|
187
|
+
// Rule: Developer/Audit modes use 8 patterns (comprehensive validation)
|
|
188
|
+
const rules = {
|
|
189
|
+
cli: 30,
|
|
190
|
+
default: 8,
|
|
191
|
+
reviewer: 3,
|
|
192
|
+
developer: 8,
|
|
193
|
+
audit: 8,
|
|
194
|
+
};
|
|
195
|
+
// This test documents the expected pattern counts for each context
|
|
196
|
+
expect(rules.cli).toBe(30);
|
|
197
|
+
expect(rules.default).toBe(8);
|
|
198
|
+
expect(rules.reviewer).toBe(3);
|
|
199
|
+
expect(rules.developer).toBe(8);
|
|
200
|
+
expect(rules.audit).toBe(8);
|
|
201
|
+
});
|
|
202
|
+
it("should validate CLI pattern count matches help text", () => {
|
|
203
|
+
const filePath = path.join(projectRoot, "cli/src/assess-security.ts");
|
|
204
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
205
|
+
// Extract config value
|
|
206
|
+
const configMatch = content.match(/securityPatternsToTest:\s*(\d+)/);
|
|
207
|
+
expect(configMatch).toBeTruthy();
|
|
208
|
+
// Extract help text values
|
|
209
|
+
const helpMatches = [
|
|
210
|
+
...content.matchAll(/(\d+) attack patterns/gi),
|
|
211
|
+
...content.matchAll(/\((\d+) total\)/gi),
|
|
212
|
+
];
|
|
213
|
+
// All should reference 30
|
|
214
|
+
const expectedCount = 30;
|
|
215
|
+
if (configMatch) {
|
|
216
|
+
expect(parseInt(configMatch[1], 10)).toBe(expectedCount);
|
|
217
|
+
}
|
|
218
|
+
for (const match of helpMatches) {
|
|
219
|
+
if (match[1]) {
|
|
220
|
+
expect(parseInt(match[1], 10)).toBe(expectedCount);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe("Edge cases and error scenarios", () => {
|
|
226
|
+
it("should handle missing pattern count gracefully", () => {
|
|
227
|
+
// This test verifies that even if config is missing securityPatternsToTest,
|
|
228
|
+
// the system has documented defaults
|
|
229
|
+
const configTypesPath = path.join(projectRoot, "client/src/lib/assessment/configTypes.ts");
|
|
230
|
+
const content = fs.readFileSync(configTypesPath, "utf-8");
|
|
231
|
+
// Verify comment documents the default value
|
|
232
|
+
expect(content).toMatch(/securityPatternsToTest.*default.*8/i);
|
|
233
|
+
});
|
|
234
|
+
it("should document pattern count semantics clearly", () => {
|
|
235
|
+
const filePath = path.join(projectRoot, "cli/src/assess-security.ts");
|
|
236
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
237
|
+
// Help text should clearly state what "30 patterns" means
|
|
238
|
+
expect(content).toContain("Attack Patterns Tested");
|
|
239
|
+
expect(content).toContain("30 total");
|
|
240
|
+
// Should list pattern categories for clarity
|
|
241
|
+
expect(content).toContain("Command Injection");
|
|
242
|
+
expect(content).toContain("SQL Injection");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
package/build/assess-security.js
CHANGED
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "fs";
|
|
13
13
|
import * as path from "path";
|
|
14
|
-
import * as os from "os";
|
|
15
14
|
import { execSync } from "child_process";
|
|
15
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
17
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
18
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
19
|
+
// Import shared server config loading (Issue #84 - Zod validation)
|
|
20
|
+
import { loadServerConfig } from "./lib/assessment-runner/server-config.js";
|
|
16
21
|
/**
|
|
17
22
|
* Validate that a command is safe to execute
|
|
18
23
|
* - Must be an absolute path or resolvable via PATH
|
|
@@ -69,81 +74,10 @@ function validateEnvVars(env) {
|
|
|
69
74
|
}
|
|
70
75
|
return validatedEnv;
|
|
71
76
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Safely parse JSON with error handling
|
|
74
|
-
*/
|
|
75
|
-
function safeJsonParse(content, filePath) {
|
|
76
|
-
try {
|
|
77
|
-
return JSON.parse(content);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
throw new Error(`Failed to parse JSON from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
84
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
85
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
86
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
87
77
|
// Import from local client lib (will use package exports when published)
|
|
88
78
|
import { SecurityAssessor } from "../../client/lib/services/assessment/modules/SecurityAssessor.js";
|
|
89
79
|
import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
|
|
90
80
|
import { loadPerformanceConfig } from "../../client/lib/services/assessment/config/performanceConfig.js";
|
|
91
|
-
/**
|
|
92
|
-
* Load server configuration from Claude Code's MCP settings
|
|
93
|
-
*/
|
|
94
|
-
function loadServerConfig(serverName, configPath) {
|
|
95
|
-
const possiblePaths = [
|
|
96
|
-
configPath,
|
|
97
|
-
path.join(os.homedir(), ".config", "mcp", "servers", `${serverName}.json`),
|
|
98
|
-
path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json"),
|
|
99
|
-
].filter(Boolean);
|
|
100
|
-
for (const tryPath of possiblePaths) {
|
|
101
|
-
if (!fs.existsSync(tryPath))
|
|
102
|
-
continue;
|
|
103
|
-
const rawConfig = safeJsonParse(fs.readFileSync(tryPath, "utf-8"), tryPath);
|
|
104
|
-
// Type guard: check if it's a Claude Desktop config with mcpServers
|
|
105
|
-
if (rawConfig &&
|
|
106
|
-
typeof rawConfig === "object" &&
|
|
107
|
-
"mcpServers" in rawConfig) {
|
|
108
|
-
const desktopConfig = rawConfig;
|
|
109
|
-
const serverConfig = desktopConfig.mcpServers?.[serverName];
|
|
110
|
-
if (serverConfig) {
|
|
111
|
-
return {
|
|
112
|
-
transport: "stdio",
|
|
113
|
-
command: serverConfig.command,
|
|
114
|
-
args: serverConfig.args || [],
|
|
115
|
-
env: serverConfig.env || {},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// Type guard: check if it's a direct config file
|
|
120
|
-
if (rawConfig && typeof rawConfig === "object") {
|
|
121
|
-
const directConfig = rawConfig;
|
|
122
|
-
// Check for HTTP/SSE transport
|
|
123
|
-
if (directConfig.url ||
|
|
124
|
-
directConfig.transport === "http" ||
|
|
125
|
-
directConfig.transport === "sse") {
|
|
126
|
-
if (!directConfig.url) {
|
|
127
|
-
throw new Error(`Invalid server config: transport is '${directConfig.transport}' but 'url' is missing`);
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
transport: directConfig.transport || "http",
|
|
131
|
-
url: directConfig.url,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
// Check for stdio transport
|
|
135
|
-
if (directConfig.command) {
|
|
136
|
-
return {
|
|
137
|
-
transport: "stdio",
|
|
138
|
-
command: directConfig.command,
|
|
139
|
-
args: directConfig.args || [],
|
|
140
|
-
env: directConfig.env || {},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
|
|
146
|
-
}
|
|
147
81
|
/**
|
|
148
82
|
* Connect to MCP server via configured transport
|
|
149
83
|
*/
|
|
@@ -263,7 +197,7 @@ async function runSecurityAssessment(options) {
|
|
|
263
197
|
}
|
|
264
198
|
const config = {
|
|
265
199
|
...DEFAULT_ASSESSMENT_CONFIG,
|
|
266
|
-
securityPatternsToTest:
|
|
200
|
+
securityPatternsToTest: 30,
|
|
267
201
|
reviewerMode: false,
|
|
268
202
|
testTimeout: 30000,
|
|
269
203
|
};
|
|
@@ -273,7 +207,7 @@ async function runSecurityAssessment(options) {
|
|
|
273
207
|
callTool: createCallToolWrapper(client),
|
|
274
208
|
config,
|
|
275
209
|
};
|
|
276
|
-
console.log(`š”ļø Running security assessment with
|
|
210
|
+
console.log(`š”ļø Running security assessment with 30 attack patterns...`);
|
|
277
211
|
const assessor = new SecurityAssessor(config);
|
|
278
212
|
const results = await assessor.assess(context);
|
|
279
213
|
await client.close();
|
|
@@ -392,7 +326,7 @@ function printHelp() {
|
|
|
392
326
|
console.log(`
|
|
393
327
|
Usage: mcp-assess-security [options] [server-name]
|
|
394
328
|
|
|
395
|
-
Run security assessment against an MCP server with
|
|
329
|
+
Run security assessment against an MCP server with 30 attack patterns.
|
|
396
330
|
|
|
397
331
|
Options:
|
|
398
332
|
--server, -s <name> Server name (required, or pass as first positional arg)
|
|
@@ -403,12 +337,12 @@ Options:
|
|
|
403
337
|
--verbose, -v Enable verbose logging
|
|
404
338
|
--help, -h Show this help message
|
|
405
339
|
|
|
406
|
-
Attack Patterns Tested (
|
|
340
|
+
Attack Patterns Tested (30 total):
|
|
407
341
|
⢠Command Injection, SQL Injection, Path Traversal
|
|
408
342
|
⢠Calculator Injection, Code Execution, XXE
|
|
409
343
|
⢠Data Exfiltration, Token Theft, NoSQL Injection
|
|
410
344
|
⢠Unicode Bypass, Nested Injection, Package Squatting
|
|
411
|
-
ā¢
|
|
345
|
+
⢠Session Management, Auth Bypass, and more...
|
|
412
346
|
|
|
413
347
|
Examples:
|
|
414
348
|
mcp-assess-security my-server
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for server-config.ts Zod validation integration (Issue #84)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that loadServerConfig() correctly validates config files
|
|
5
|
+
* using Zod schemas and provides helpful error messages.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import { loadServerConfig } from "../server-config.js";
|
|
11
|
+
describe("loadServerConfig with Zod validation", () => {
|
|
12
|
+
const tmpDir = os.tmpdir();
|
|
13
|
+
let testConfigPath;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
testConfigPath = path.join(tmpDir, `test-config-${Date.now()}.json`);
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Clean up test config files
|
|
19
|
+
if (fs.existsSync(testConfigPath)) {
|
|
20
|
+
fs.unlinkSync(testConfigPath);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
describe("valid configurations", () => {
|
|
24
|
+
it("should load valid HTTP config", () => {
|
|
25
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
26
|
+
transport: "http",
|
|
27
|
+
url: "http://localhost:8080/mcp",
|
|
28
|
+
}));
|
|
29
|
+
const config = loadServerConfig("test-server", testConfigPath);
|
|
30
|
+
expect(config.transport).toBe("http");
|
|
31
|
+
expect(config.url).toBe("http://localhost:8080/mcp");
|
|
32
|
+
});
|
|
33
|
+
it("should load valid SSE config", () => {
|
|
34
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
35
|
+
transport: "sse",
|
|
36
|
+
url: "http://localhost:8080/sse",
|
|
37
|
+
}));
|
|
38
|
+
const config = loadServerConfig("test-server", testConfigPath);
|
|
39
|
+
expect(config.transport).toBe("sse");
|
|
40
|
+
expect(config.url).toBe("http://localhost:8080/sse");
|
|
41
|
+
});
|
|
42
|
+
it("should load valid stdio config", () => {
|
|
43
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
44
|
+
command: "/usr/bin/node",
|
|
45
|
+
args: ["server.js"],
|
|
46
|
+
env: { NODE_ENV: "production" },
|
|
47
|
+
}));
|
|
48
|
+
const config = loadServerConfig("test-server", testConfigPath);
|
|
49
|
+
expect(config.transport).toBe("stdio");
|
|
50
|
+
expect(config.command).toBe("/usr/bin/node");
|
|
51
|
+
expect(config.args).toEqual(["server.js"]);
|
|
52
|
+
expect(config.env).toEqual({ NODE_ENV: "production" });
|
|
53
|
+
});
|
|
54
|
+
it("should load config from Claude Desktop format", () => {
|
|
55
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
56
|
+
mcpServers: {
|
|
57
|
+
"my-server": {
|
|
58
|
+
command: "/usr/bin/python",
|
|
59
|
+
args: ["-m", "mcp_server"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
const config = loadServerConfig("my-server", testConfigPath);
|
|
64
|
+
expect(config.transport).toBe("stdio");
|
|
65
|
+
expect(config.command).toBe("/usr/bin/python");
|
|
66
|
+
expect(config.args).toEqual(["-m", "mcp_server"]);
|
|
67
|
+
});
|
|
68
|
+
it("should default HTTP transport when url present without transport field", () => {
|
|
69
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
70
|
+
url: "http://localhost:8080/mcp",
|
|
71
|
+
}));
|
|
72
|
+
const config = loadServerConfig("test-server", testConfigPath);
|
|
73
|
+
expect(config.transport).toBe("http");
|
|
74
|
+
expect(config.url).toBe("http://localhost:8080/mcp");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("invalid configurations with Zod validation errors", () => {
|
|
78
|
+
it("should throw Zod error for invalid URL", () => {
|
|
79
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
80
|
+
transport: "http",
|
|
81
|
+
url: "not-a-valid-url",
|
|
82
|
+
}));
|
|
83
|
+
expect(() => loadServerConfig("test-server", testConfigPath)).toThrow(/url must be a valid URL/);
|
|
84
|
+
});
|
|
85
|
+
it("should throw Zod error for empty command in stdio config", () => {
|
|
86
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
87
|
+
command: "",
|
|
88
|
+
}));
|
|
89
|
+
expect(() => loadServerConfig("test-server", testConfigPath)).toThrow(/command is required/);
|
|
90
|
+
});
|
|
91
|
+
it("should include config source in error message", () => {
|
|
92
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
93
|
+
transport: "http",
|
|
94
|
+
url: "invalid",
|
|
95
|
+
}));
|
|
96
|
+
expect(() => loadServerConfig("test-server", testConfigPath)).toThrow(new RegExp(testConfigPath));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe("error handling", () => {
|
|
100
|
+
it("should throw for invalid JSON", () => {
|
|
101
|
+
fs.writeFileSync(testConfigPath, "not valid json {");
|
|
102
|
+
expect(() => loadServerConfig("test-server", testConfigPath)).toThrow(/Invalid JSON/);
|
|
103
|
+
});
|
|
104
|
+
it("should throw for missing config file", () => {
|
|
105
|
+
expect(() => loadServerConfig("nonexistent-server", "/nonexistent/path.json")).toThrow(/Server config not found/);
|
|
106
|
+
});
|
|
107
|
+
it("should throw for server not in Claude Desktop config", () => {
|
|
108
|
+
fs.writeFileSync(testConfigPath, JSON.stringify({
|
|
109
|
+
mcpServers: {
|
|
110
|
+
"other-server": { command: "node" },
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
expect(() => loadServerConfig("missing-server", testConfigPath)).toThrow(/Server config not found/);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -9,7 +9,7 @@ import * as fs from "fs";
|
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import { AssessmentOrchestrator, } from "../../../../client/lib/services/assessment/AssessmentOrchestrator.js";
|
|
11
11
|
import { AssessmentStateManager } from "../../assessmentState.js";
|
|
12
|
-
import { emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, } from "../jsonl-events.js";
|
|
12
|
+
import { emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, emitPhaseStarted, emitPhaseComplete, emitToolTestComplete, emitValidationSummary, } from "../jsonl-events.js";
|
|
13
13
|
import { loadServerConfig } from "./server-config.js";
|
|
14
14
|
import { loadSourceFiles } from "./source-loader.js";
|
|
15
15
|
import { connectToServer } from "./server-connection.js";
|
|
@@ -29,6 +29,9 @@ export async function runFullAssessment(options) {
|
|
|
29
29
|
if (!options.jsonOnly) {
|
|
30
30
|
console.log("ā
Server config loaded");
|
|
31
31
|
}
|
|
32
|
+
// Phase 1: Discovery
|
|
33
|
+
const discoveryStart = Date.now();
|
|
34
|
+
emitPhaseStarted("discovery");
|
|
32
35
|
const client = await connectToServer(serverConfig);
|
|
33
36
|
emitServerConnected(options.serverName, serverConfig.transport || "stdio");
|
|
34
37
|
if (!options.jsonOnly) {
|
|
@@ -117,6 +120,8 @@ export async function runFullAssessment(options) {
|
|
|
117
120
|
console.log("š¬ Prompts not supported by server");
|
|
118
121
|
}
|
|
119
122
|
}
|
|
123
|
+
// End of discovery phase
|
|
124
|
+
emitPhaseComplete("discovery", Date.now() - discoveryStart);
|
|
120
125
|
// State management for resumable assessments
|
|
121
126
|
const stateManager = new AssessmentStateManager(options.serverName);
|
|
122
127
|
if (stateManager.exists() && !options.noResume) {
|
|
@@ -239,7 +244,14 @@ export async function runFullAssessment(options) {
|
|
|
239
244
|
else if (event.type === "annotation_aligned") {
|
|
240
245
|
emitAnnotationAligned(event.tool, event.confidence, event.annotations);
|
|
241
246
|
}
|
|
247
|
+
else if (event.type === "tool_test_complete") {
|
|
248
|
+
emitToolTestComplete(event.tool, event.module, event.scenariosPassed, event.scenariosExecuted, event.confidence, event.status, event.executionTime);
|
|
249
|
+
}
|
|
250
|
+
else if (event.type === "validation_summary") {
|
|
251
|
+
emitValidationSummary(event.tool, event.wrongType, event.missingRequired, event.extraParams, event.nullValues, event.invalidValues);
|
|
252
|
+
}
|
|
242
253
|
// module_started and module_complete are handled by orchestrator directly
|
|
254
|
+
// phase_started and phase_complete are emitted directly (not via callback)
|
|
243
255
|
};
|
|
244
256
|
const context = {
|
|
245
257
|
serverName: options.serverName,
|
|
@@ -267,7 +279,12 @@ export async function runFullAssessment(options) {
|
|
|
267
279
|
console.log(`\nš Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
|
|
268
280
|
console.log("");
|
|
269
281
|
}
|
|
282
|
+
// Phase 2: Assessment
|
|
283
|
+
const assessmentStart = Date.now();
|
|
284
|
+
emitPhaseStarted("assessment");
|
|
270
285
|
const results = await orchestrator.runFullAssessment(context);
|
|
286
|
+
// End of assessment phase
|
|
287
|
+
emitPhaseComplete("assessment", Date.now() - assessmentStart);
|
|
271
288
|
// Emit assessment complete event
|
|
272
289
|
const defaultOutputPath = `/tmp/inspector-full-assessment-${options.serverName}.json`;
|
|
273
290
|
emitAssessmentComplete(results.overallStatus, results.totalTestsRun, results.executionTime, options.outputPath || defaultOutputPath);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { DEFAULT_ASSESSMENT_CONFIG, getAllModulesConfig, } from "../../../../client/lib/lib/assessmentTypes.js";
|
|
9
9
|
import { FULL_CLAUDE_CODE_CONFIG } from "../../../../client/lib/services/assessment/lib/claudeCodeBridge.js";
|
|
10
10
|
import { loadPerformanceConfig } from "../../../../client/lib/services/assessment/config/performanceConfig.js";
|
|
11
|
+
import { safeParseAssessmentConfig } from "../../../../client/lib/lib/assessment/configSchemas.js";
|
|
11
12
|
import { getProfileModules, resolveModuleNames, modulesToLegacyConfig, } from "../../profiles.js";
|
|
12
13
|
/**
|
|
13
14
|
* Build assessment configuration from CLI options
|
|
@@ -139,5 +140,14 @@ export function buildConfig(options) {
|
|
|
139
140
|
"This will be required in v2.0.0. " +
|
|
140
141
|
"See docs/DEPRECATION_GUIDE.md for migration info.");
|
|
141
142
|
}
|
|
143
|
+
// Validate built config with Zod schema (Issue #84)
|
|
144
|
+
// Warning only - maintains backward compatibility with existing configs
|
|
145
|
+
const validation = safeParseAssessmentConfig(config);
|
|
146
|
+
if (!validation.success) {
|
|
147
|
+
const issues = validation.error.issues
|
|
148
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
149
|
+
.join(", ");
|
|
150
|
+
console.warn(`ā ļø Config validation warning: ${issues}`);
|
|
151
|
+
}
|
|
142
152
|
return config;
|
|
143
153
|
}
|