@bryan-thompson/inspector-assessment-cli 1.32.2 → 1.32.4

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.
@@ -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
+ });
@@ -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: 17,
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 23 attack patterns...`);
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 23 attack patterns.
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 (23 total):
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
- • And more...
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
  }