@bryan-thompson/inspector-assessment-cli 1.43.1 → 1.43.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/index.test.js +11 -3
- package/build/__tests__/assessment-runner/native-module-detector.test.js +199 -0
- package/build/__tests__/assessment-runner/server-connection.test.js +184 -1
- package/build/__tests__/assessment-runner-facade.test.js +10 -2
- package/build/__tests__/jsonl-events.test.js +83 -1
- package/build/__tests__/static-mode-integration.test.js +387 -0
- package/build/__tests__/static-only-mode.test.js +439 -0
- package/build/__tests__/transport.test.js +141 -0
- package/build/assess-full.js +532 -106
- package/build/assess-security.js +54 -90
- package/build/lib/assessment-runner/assessment-executor.js +194 -0
- package/build/lib/assessment-runner/config-builder.js +46 -1
- package/build/lib/assessment-runner/index.js +2 -0
- package/build/lib/assessment-runner/native-module-detector.js +107 -0
- package/build/lib/assessment-runner/server-connection.js +56 -9
- package/build/lib/cli-parser.js +67 -0
- package/build/lib/cli-parserSchemas.js +12 -0
- package/build/lib/jsonl-events.js +29 -0
- package/build/lib/static-modules.js +103 -0
- package/build/transport.js +32 -7
- package/build/validate-testbed.js +0 -0
- package/package.json +1 -1
- package/build/lib/__tests__/zodErrorFormatter.test.js +0 -282
package/build/assess-security.js
CHANGED
|
@@ -11,73 +11,58 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "fs";
|
|
13
13
|
import * as path from "path";
|
|
14
|
-
import
|
|
14
|
+
import * as os from "os";
|
|
15
15
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
16
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
17
17
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
18
18
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
19
|
-
// Import
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
* Validate that a command is safe to execute
|
|
23
|
-
* - Must be an absolute path or resolvable via PATH
|
|
24
|
-
* - Must not contain shell metacharacters
|
|
25
|
-
*/
|
|
26
|
-
function validateCommand(command) {
|
|
27
|
-
// Check for shell metacharacters that could indicate injection
|
|
28
|
-
const dangerousChars = /[;&|`$(){}[\]<>!\\]/;
|
|
29
|
-
if (dangerousChars.test(command)) {
|
|
30
|
-
throw new Error(`Invalid command: contains shell metacharacters: ${command}`);
|
|
31
|
-
}
|
|
32
|
-
// Verify the command exists and is executable
|
|
33
|
-
try {
|
|
34
|
-
// Use 'which' on Unix-like systems, 'where' on Windows
|
|
35
|
-
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
36
|
-
execSync(`${whichCmd} "${command}"`, { stdio: "pipe" });
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// Check if it's an absolute path that exists
|
|
40
|
-
if (path.isAbsolute(command) && fs.existsSync(command)) {
|
|
41
|
-
try {
|
|
42
|
-
fs.accessSync(command, fs.constants.X_OK);
|
|
43
|
-
return; // Command exists and is executable
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
throw new Error(`Command not executable: ${command}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
throw new Error(`Command not found: ${command}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
19
|
+
// Import from local client lib (will use package exports when published)
|
|
20
|
+
import { SecurityAssessor } from "../../client/lib/services/assessment/modules/SecurityAssessor.js";
|
|
21
|
+
import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
|
|
52
22
|
/**
|
|
53
|
-
*
|
|
54
|
-
* - Keys must be valid env var names (alphanumeric + underscore)
|
|
55
|
-
* - Values should not contain null bytes
|
|
23
|
+
* Load server configuration from Claude Code's MCP settings
|
|
56
24
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!
|
|
65
|
-
console.warn(`Skipping invalid environment variable name: ${key} (must match [a-zA-Z_][a-zA-Z0-9_]*)`);
|
|
25
|
+
function loadServerConfig(serverName, configPath) {
|
|
26
|
+
const possiblePaths = [
|
|
27
|
+
configPath,
|
|
28
|
+
path.join(os.homedir(), ".config", "mcp", "servers", `${serverName}.json`),
|
|
29
|
+
path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json"),
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
for (const tryPath of possiblePaths) {
|
|
32
|
+
if (!fs.existsSync(tryPath))
|
|
66
33
|
continue;
|
|
34
|
+
const config = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
|
|
35
|
+
if (config.mcpServers && config.mcpServers[serverName]) {
|
|
36
|
+
const serverConfig = config.mcpServers[serverName];
|
|
37
|
+
return {
|
|
38
|
+
transport: "stdio",
|
|
39
|
+
command: serverConfig.command,
|
|
40
|
+
args: serverConfig.args || [],
|
|
41
|
+
env: serverConfig.env || {},
|
|
42
|
+
};
|
|
67
43
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
44
|
+
if (config.url ||
|
|
45
|
+
config.transport === "http" ||
|
|
46
|
+
config.transport === "sse") {
|
|
47
|
+
if (!config.url) {
|
|
48
|
+
throw new Error(`Invalid server config: transport is '${config.transport}' but 'url' is missing`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
transport: config.transport || "http",
|
|
52
|
+
url: config.url,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (config.command) {
|
|
56
|
+
return {
|
|
57
|
+
transport: "stdio",
|
|
58
|
+
command: config.command,
|
|
59
|
+
args: config.args || [],
|
|
60
|
+
env: config.env || {},
|
|
61
|
+
};
|
|
72
62
|
}
|
|
73
|
-
validatedEnv[key] = String(value);
|
|
74
63
|
}
|
|
75
|
-
|
|
64
|
+
throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
|
|
76
65
|
}
|
|
77
|
-
// Import from local client lib (will use package exports when published)
|
|
78
|
-
import { SecurityAssessor } from "../../client/lib/services/assessment/modules/SecurityAssessor.js";
|
|
79
|
-
import { DEFAULT_ASSESSMENT_CONFIG, } from "../../client/lib/lib/assessmentTypes.js";
|
|
80
|
-
import { loadPerformanceConfig } from "../../client/lib/services/assessment/config/performanceConfig.js";
|
|
81
66
|
/**
|
|
82
67
|
* Connect to MCP server via configured transport
|
|
83
68
|
*/
|
|
@@ -98,16 +83,12 @@ async function connectToServer(config) {
|
|
|
98
83
|
default:
|
|
99
84
|
if (!config.command)
|
|
100
85
|
throw new Error("Command required for stdio transport");
|
|
101
|
-
// Validate command before execution to prevent injection attacks
|
|
102
|
-
validateCommand(config.command);
|
|
103
|
-
// Validate and sanitize environment variables from config
|
|
104
|
-
const validatedEnv = validateEnvVars(config.env);
|
|
105
86
|
transport = new StdioClientTransport({
|
|
106
87
|
command: config.command,
|
|
107
88
|
args: config.args,
|
|
108
89
|
env: {
|
|
109
90
|
...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
|
|
110
|
-
...
|
|
91
|
+
...config.env,
|
|
111
92
|
},
|
|
112
93
|
stderr: "pipe",
|
|
113
94
|
});
|
|
@@ -172,22 +153,6 @@ function createCallToolWrapper(client) {
|
|
|
172
153
|
async function runSecurityAssessment(options) {
|
|
173
154
|
console.log(`\n🔍 Connecting to MCP server: ${options.serverName}`);
|
|
174
155
|
const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
|
|
175
|
-
// Load custom performance config if provided (Issue #37)
|
|
176
|
-
// Note: Currently, modules use DEFAULT_PERFORMANCE_CONFIG directly.
|
|
177
|
-
// This validates the config file but doesn't override runtime values yet.
|
|
178
|
-
if (options.performanceConfigPath) {
|
|
179
|
-
try {
|
|
180
|
-
const performanceConfig = loadPerformanceConfig(options.performanceConfigPath);
|
|
181
|
-
console.log(`📊 Performance config loaded from: ${options.performanceConfigPath}`);
|
|
182
|
-
console.log(` Batch interval: ${performanceConfig.batchFlushIntervalMs}ms, ` +
|
|
183
|
-
`Security batch: ${performanceConfig.securityBatchSize}`);
|
|
184
|
-
// TODO: Wire performanceConfig through to SecurityAssessor
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
console.error(`❌ Failed to load performance config: ${error instanceof Error ? error.message : String(error)}`);
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
156
|
const client = await connectToServer(serverConfig);
|
|
192
157
|
console.log("✅ Connected successfully");
|
|
193
158
|
const tools = await getTools(client, options.toolName);
|
|
@@ -197,7 +162,7 @@ async function runSecurityAssessment(options) {
|
|
|
197
162
|
}
|
|
198
163
|
const config = {
|
|
199
164
|
...DEFAULT_ASSESSMENT_CONFIG,
|
|
200
|
-
securityPatternsToTest:
|
|
165
|
+
securityPatternsToTest: 13,
|
|
201
166
|
reviewerMode: false,
|
|
202
167
|
testTimeout: 30000,
|
|
203
168
|
};
|
|
@@ -206,8 +171,9 @@ async function runSecurityAssessment(options) {
|
|
|
206
171
|
tools,
|
|
207
172
|
callTool: createCallToolWrapper(client),
|
|
208
173
|
config,
|
|
174
|
+
transportType: serverConfig.transport || "stdio",
|
|
209
175
|
};
|
|
210
|
-
console.log(`🛡️ Running security assessment with
|
|
176
|
+
console.log(`🛡️ Running security assessment with 13 attack patterns...`);
|
|
211
177
|
const assessor = new SecurityAssessor(config);
|
|
212
178
|
const results = await assessor.assess(context);
|
|
213
179
|
await client.close();
|
|
@@ -287,9 +253,6 @@ function parseArgs() {
|
|
|
287
253
|
case "-v":
|
|
288
254
|
options.verbose = true;
|
|
289
255
|
break;
|
|
290
|
-
case "--performance-config":
|
|
291
|
-
options.performanceConfigPath = args[++i];
|
|
292
|
-
break;
|
|
293
256
|
case "--help":
|
|
294
257
|
case "-h":
|
|
295
258
|
printHelp();
|
|
@@ -326,23 +289,24 @@ function printHelp() {
|
|
|
326
289
|
console.log(`
|
|
327
290
|
Usage: mcp-assess-security [options] [server-name]
|
|
328
291
|
|
|
329
|
-
Run security assessment against an MCP server with
|
|
292
|
+
Run security assessment against an MCP server with 13 attack patterns.
|
|
330
293
|
|
|
331
294
|
Options:
|
|
332
295
|
--server, -s <name> Server name (required, or pass as first positional arg)
|
|
333
296
|
--config, -c <path> Path to server config JSON
|
|
334
297
|
--output, -o <path> Output JSON path (default: /tmp/inspector-security-assessment-<server>.json)
|
|
335
298
|
--tool, -t <name> Test only specific tool (default: test all tools)
|
|
336
|
-
--performance-config <path> Path to performance tuning JSON (batch sizes, timeouts, etc.)
|
|
337
299
|
--verbose, -v Enable verbose logging
|
|
338
300
|
--help, -h Show this help message
|
|
339
301
|
|
|
340
|
-
Attack Patterns Tested (
|
|
341
|
-
• Command Injection
|
|
342
|
-
• Calculator Injection
|
|
343
|
-
•
|
|
344
|
-
•
|
|
345
|
-
•
|
|
302
|
+
Attack Patterns Tested (13 total):
|
|
303
|
+
• Command Injection • SQL Injection
|
|
304
|
+
• Calculator Injection • Path Traversal
|
|
305
|
+
• Type Safety • Boundary Testing
|
|
306
|
+
• Required Fields • MCP Error Format
|
|
307
|
+
• Timeout Handling • Indirect Prompt Injection
|
|
308
|
+
• Unicode Bypass • Nested Injection
|
|
309
|
+
• Package Squatting
|
|
346
310
|
|
|
347
311
|
Examples:
|
|
348
312
|
mcp-assess-security my-server
|
|
@@ -26,6 +26,10 @@ import { ExternalAPIDependencyDetector } from "../../../../client/lib/services/a
|
|
|
26
26
|
import { StdioTransportDetector } from "../../../../client/lib/services/assessment/helpers/StdioTransportDetector.js";
|
|
27
27
|
// Issue #170: Import tool annotation extractor for security severity adjustment
|
|
28
28
|
import { extractToolAnnotationsContext } from "../../../../client/lib/services/assessment/helpers/ToolAnnotationExtractor.js";
|
|
29
|
+
// Issue #212: Import native module detector for pre-flight warnings
|
|
30
|
+
import { detectNativeModules } from "./native-module-detector.js";
|
|
31
|
+
// Issue #213: Import static modules for static-only assessment
|
|
32
|
+
import { RUNTIME_MODULES } from "../static-modules.js";
|
|
29
33
|
/**
|
|
30
34
|
* Run full assessment against an MCP server
|
|
31
35
|
*
|
|
@@ -33,6 +37,174 @@ import { extractToolAnnotationsContext } from "../../../../client/lib/services/a
|
|
|
33
37
|
* @returns Assessment results
|
|
34
38
|
*/
|
|
35
39
|
export async function runFullAssessment(options) {
|
|
40
|
+
// Issue #213: Handle static-only mode
|
|
41
|
+
if (options.staticOnly) {
|
|
42
|
+
return runStaticOnlyAssessment(options);
|
|
43
|
+
}
|
|
44
|
+
// Issue #213: Handle fallback-static mode
|
|
45
|
+
if (options.fallbackStatic) {
|
|
46
|
+
try {
|
|
47
|
+
return await runRuntimeAssessment(options);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!options.jsonOnly) {
|
|
51
|
+
console.log(`\n⚠️ Runtime assessment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
console.log(" Falling back to static-only assessment...\n");
|
|
53
|
+
}
|
|
54
|
+
// Fall back to static assessment
|
|
55
|
+
return runStaticOnlyAssessment(options);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Default: runtime assessment
|
|
59
|
+
return runRuntimeAssessment(options);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Run static-only assessment (no server connection)
|
|
63
|
+
*
|
|
64
|
+
* Validates source code, manifest, documentation, and code quality
|
|
65
|
+
* without connecting to or executing the MCP server.
|
|
66
|
+
*
|
|
67
|
+
* @param options - CLI assessment options (must include sourceCodePath)
|
|
68
|
+
* @returns Assessment results from static-capable modules
|
|
69
|
+
* @see Issue #213
|
|
70
|
+
*/
|
|
71
|
+
async function runStaticOnlyAssessment(options) {
|
|
72
|
+
// Issue #155: Enable annotation debug mode if flag is set
|
|
73
|
+
if (options.debugAnnotations) {
|
|
74
|
+
setAnnotationDebugMode(true);
|
|
75
|
+
if (!options.jsonOnly) {
|
|
76
|
+
console.log("🔍 Annotation debug mode enabled (--debug-annotations)");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!options.jsonOnly) {
|
|
80
|
+
console.log(`\n📋 Starting static-only assessment for: ${options.serverName}`);
|
|
81
|
+
console.log(" Mode: No server connection (static analysis only)\n");
|
|
82
|
+
}
|
|
83
|
+
// Validate source code path is provided (should be caught by CLI validation)
|
|
84
|
+
if (!options.sourceCodePath) {
|
|
85
|
+
throw new Error("--static-only requires --source <path>");
|
|
86
|
+
}
|
|
87
|
+
// Phase 1: Static Discovery
|
|
88
|
+
const discoveryStart = Date.now();
|
|
89
|
+
emitPhaseStarted("discovery");
|
|
90
|
+
// Resolve and load source files
|
|
91
|
+
const resolvedSourcePath = resolveSourcePath(options.sourceCodePath);
|
|
92
|
+
if (!fs.existsSync(resolvedSourcePath)) {
|
|
93
|
+
throw new Error(`Source path not found: ${resolvedSourcePath}`);
|
|
94
|
+
}
|
|
95
|
+
const sourceFiles = loadSourceFiles(resolvedSourcePath, options.debugSource);
|
|
96
|
+
if (!options.jsonOnly) {
|
|
97
|
+
console.log(`📁 Loaded source files from: ${resolvedSourcePath}`);
|
|
98
|
+
console.log(` README: ${sourceFiles.readmeContent ? "found" : "not found"}`);
|
|
99
|
+
console.log(` package.json: ${sourceFiles.packageJson ? "found" : "not found"}`);
|
|
100
|
+
console.log(` manifest.json: ${sourceFiles.manifestJson ? "found" : "not found"}`);
|
|
101
|
+
console.log(` Source files: ${sourceFiles.sourceCodeFiles?.size || 0} file(s)`);
|
|
102
|
+
}
|
|
103
|
+
let tools = [];
|
|
104
|
+
if (sourceFiles.manifestJson) {
|
|
105
|
+
const manifest = sourceFiles.manifestJson;
|
|
106
|
+
if (manifest.mcp_config?.tools) {
|
|
107
|
+
// Convert manifest tools to expected schema format
|
|
108
|
+
tools = manifest.mcp_config.tools.map((t) => ({
|
|
109
|
+
name: t.name,
|
|
110
|
+
description: t.description,
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: t.inputSchema?.properties || {},
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
if (!options.jsonOnly) {
|
|
117
|
+
console.log(` Tools from manifest: ${tools.length} tool(s)`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Emit discovery events (minimal - no server discovery)
|
|
122
|
+
emitToolsDiscoveryComplete(tools.length);
|
|
123
|
+
emitPhaseComplete("discovery", Date.now() - discoveryStart);
|
|
124
|
+
// Build static-mode configuration
|
|
125
|
+
const config = buildConfig(options);
|
|
126
|
+
// Emit modules_configured event
|
|
127
|
+
if (config.assessmentCategories) {
|
|
128
|
+
const enabled = [];
|
|
129
|
+
const skipped = [];
|
|
130
|
+
for (const [key, value] of Object.entries(config.assessmentCategories)) {
|
|
131
|
+
if (value) {
|
|
132
|
+
enabled.push(key);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
skipped.push(key);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
emitModulesConfigured(enabled, skipped, "static-only");
|
|
139
|
+
}
|
|
140
|
+
const orchestrator = new AssessmentOrchestrator(config);
|
|
141
|
+
if (!options.jsonOnly) {
|
|
142
|
+
const enabledCount = Object.values(config.assessmentCategories || {}).filter(Boolean).length;
|
|
143
|
+
console.log(`\n🏃 Running ${enabledCount} static module(s)...`);
|
|
144
|
+
console.log(` Skipped: ${RUNTIME_MODULES.length} runtime-only module(s)\n`);
|
|
145
|
+
}
|
|
146
|
+
// Phase 2: Static Assessment
|
|
147
|
+
const assessmentStart = Date.now();
|
|
148
|
+
emitPhaseStarted("assessment");
|
|
149
|
+
// Build assessment context WITHOUT server connection
|
|
150
|
+
// Use type assertion for tools - in static mode, tools are informational only
|
|
151
|
+
// and many static modules don't require them at all
|
|
152
|
+
const context = {
|
|
153
|
+
serverName: options.serverName,
|
|
154
|
+
tools: tools,
|
|
155
|
+
// NO callTool - static mode doesn't execute tools
|
|
156
|
+
callTool: undefined,
|
|
157
|
+
// NO listTools - static mode doesn't refresh tool list
|
|
158
|
+
listTools: undefined,
|
|
159
|
+
config,
|
|
160
|
+
sourceCodePath: options.sourceCodePath,
|
|
161
|
+
// Include all source files
|
|
162
|
+
...sourceFiles,
|
|
163
|
+
// No resources/prompts in static mode
|
|
164
|
+
resources: [],
|
|
165
|
+
resourceTemplates: [],
|
|
166
|
+
prompts: [],
|
|
167
|
+
// No server info in static mode
|
|
168
|
+
serverInfo: undefined,
|
|
169
|
+
serverCapabilities: undefined,
|
|
170
|
+
};
|
|
171
|
+
const results = await orchestrator.runFullAssessment(context);
|
|
172
|
+
// End of assessment phase
|
|
173
|
+
emitPhaseComplete("assessment", Date.now() - assessmentStart);
|
|
174
|
+
// Mark results as static-only mode
|
|
175
|
+
const staticResults = {
|
|
176
|
+
...results,
|
|
177
|
+
assessmentMode: "static-only",
|
|
178
|
+
staticContext: {
|
|
179
|
+
sourceCodePath: resolvedSourcePath,
|
|
180
|
+
manifestFound: !!sourceFiles.manifestJson,
|
|
181
|
+
packageJsonFound: !!sourceFiles.packageJson,
|
|
182
|
+
readmeFound: !!sourceFiles.readmeContent,
|
|
183
|
+
toolsExtracted: tools.length,
|
|
184
|
+
sourceFilesLoaded: sourceFiles.sourceCodeFiles?.size || 0,
|
|
185
|
+
},
|
|
186
|
+
skippedModules: RUNTIME_MODULES.map((name) => ({
|
|
187
|
+
name,
|
|
188
|
+
reason: "requires_runtime",
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
// Emit assessment complete event
|
|
192
|
+
const defaultOutputPath = `/tmp/inspector-static-assessment-${options.serverName}.json`;
|
|
193
|
+
emitAssessmentComplete(staticResults.overallStatus, staticResults.totalTestsRun, staticResults.executionTime, options.outputPath || defaultOutputPath);
|
|
194
|
+
if (!options.jsonOnly) {
|
|
195
|
+
console.log(`\n✅ Static assessment complete`);
|
|
196
|
+
console.log(` Tests run: ${staticResults.totalTestsRun}`);
|
|
197
|
+
console.log(` Status: ${staticResults.overallStatus}`);
|
|
198
|
+
}
|
|
199
|
+
return staticResults;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Run runtime assessment (standard mode with server connection)
|
|
203
|
+
*
|
|
204
|
+
* @param options - CLI assessment options
|
|
205
|
+
* @returns Assessment results
|
|
206
|
+
*/
|
|
207
|
+
async function runRuntimeAssessment(options) {
|
|
36
208
|
// Issue #155: Enable annotation debug mode if flag is set
|
|
37
209
|
if (options.debugAnnotations) {
|
|
38
210
|
setAnnotationDebugMode(true);
|
|
@@ -72,6 +244,28 @@ export async function runFullAssessment(options) {
|
|
|
72
244
|
console.log("✅ Server config loaded");
|
|
73
245
|
}
|
|
74
246
|
}
|
|
247
|
+
// Issue #212: Pre-flight native module detection
|
|
248
|
+
// Run before server connection to warn about potential Gatekeeper/native issues
|
|
249
|
+
if (options.sourceCodePath) {
|
|
250
|
+
try {
|
|
251
|
+
const preflightSourcePath = resolveSourcePath(options.sourceCodePath);
|
|
252
|
+
if (fs.existsSync(preflightSourcePath)) {
|
|
253
|
+
const preflightSource = loadSourceFiles(preflightSourcePath, false);
|
|
254
|
+
if (preflightSource.packageJson) {
|
|
255
|
+
const nativeModuleResult = detectNativeModules(preflightSource.packageJson, {
|
|
256
|
+
jsonOnly: options.jsonOnly,
|
|
257
|
+
serverName: options.serverName,
|
|
258
|
+
});
|
|
259
|
+
if (nativeModuleResult.detected && !options.jsonOnly) {
|
|
260
|
+
console.log(`\u{1F4E6} Pre-flight check: ${nativeModuleResult.count} native module(s) detected`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Pre-flight is best-effort; don't fail assessment if source path is invalid
|
|
267
|
+
}
|
|
268
|
+
}
|
|
75
269
|
// Phase 1: Discovery
|
|
76
270
|
const discoveryStart = Date.now();
|
|
77
271
|
emitPhaseStarted("discovery");
|
|
@@ -10,6 +10,7 @@ import { FULL_CLAUDE_CODE_CONFIG } from "../../../../client/lib/services/assessm
|
|
|
10
10
|
import { loadPerformanceConfig } from "../../../../client/lib/services/assessment/config/performanceConfig.js";
|
|
11
11
|
import { safeParseAssessmentConfig } from "../../../../client/lib/lib/assessment/configSchemas.js";
|
|
12
12
|
import { getProfileModules, resolveModuleNames, modulesToLegacyConfig, } from "../../profiles.js";
|
|
13
|
+
import { STATIC_MODULES } from "../static-modules.js";
|
|
13
14
|
/**
|
|
14
15
|
* Build assessment configuration from CLI options
|
|
15
16
|
*
|
|
@@ -24,7 +25,51 @@ export function buildConfig(options) {
|
|
|
24
25
|
testTimeout: 30000,
|
|
25
26
|
enableSourceCodeAnalysis: Boolean(options.sourceCodePath),
|
|
26
27
|
};
|
|
27
|
-
|
|
28
|
+
// Issue #213: Static-only mode - enable only static-capable modules
|
|
29
|
+
if (options.staticOnly) {
|
|
30
|
+
const staticModuleConfig = {};
|
|
31
|
+
// Start with all modules disabled
|
|
32
|
+
const allModules = getAllModulesConfig({
|
|
33
|
+
sourceCodePath: true, // Static mode always has source
|
|
34
|
+
skipTemporal: true, // Temporal requires runtime
|
|
35
|
+
});
|
|
36
|
+
for (const key of Object.keys(allModules)) {
|
|
37
|
+
staticModuleConfig[key] = false;
|
|
38
|
+
}
|
|
39
|
+
// Enable only static-capable modules
|
|
40
|
+
for (const module of STATIC_MODULES) {
|
|
41
|
+
staticModuleConfig[module] = true;
|
|
42
|
+
}
|
|
43
|
+
// Apply --only-modules filter if provided (whitelist within static modules)
|
|
44
|
+
if (options.onlyModules?.length) {
|
|
45
|
+
const resolved = resolveModuleNames(options.onlyModules);
|
|
46
|
+
for (const key of Object.keys(staticModuleConfig)) {
|
|
47
|
+
// Module must be in whitelist AND be static-capable
|
|
48
|
+
staticModuleConfig[key] =
|
|
49
|
+
resolved.includes(key) && STATIC_MODULES.includes(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Apply --skip-modules filter if provided
|
|
53
|
+
if (options.skipModules?.length) {
|
|
54
|
+
const resolved = resolveModuleNames(options.skipModules);
|
|
55
|
+
for (const module of resolved) {
|
|
56
|
+
if (module in staticModuleConfig) {
|
|
57
|
+
staticModuleConfig[module] = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
config.assessmentCategories =
|
|
62
|
+
staticModuleConfig;
|
|
63
|
+
// Log static mode info
|
|
64
|
+
const enabledModules = Object.entries(staticModuleConfig)
|
|
65
|
+
.filter(([_, enabled]) => enabled)
|
|
66
|
+
.map(([name]) => name);
|
|
67
|
+
console.log(`📋 Static-only mode: ${enabledModules.length} modules enabled`);
|
|
68
|
+
console.log(` Modules: ${enabledModules.join(", ")}`);
|
|
69
|
+
// Skip the rest of the module configuration logic
|
|
70
|
+
// (claudeCode, performance config, logging will still be applied below)
|
|
71
|
+
}
|
|
72
|
+
else if (options.fullAssessment !== false) {
|
|
28
73
|
// Priority: --profile > --only-modules > --skip-modules > default (all)
|
|
29
74
|
if (options.profile) {
|
|
30
75
|
// Use profile-based module selection
|
|
@@ -22,3 +22,5 @@ export { buildConfig } from "./config-builder.js";
|
|
|
22
22
|
export { runFullAssessment } from "./assessment-executor.js";
|
|
23
23
|
// Single Module Execution (Issue #184)
|
|
24
24
|
export { runSingleModule, getValidModuleNames, } from "./single-module-runner.js";
|
|
25
|
+
// Native Module Detection (Issue #212)
|
|
26
|
+
export { detectNativeModules, } from "./native-module-detector.js";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Module Detector
|
|
3
|
+
*
|
|
4
|
+
* Pre-flight detection of native modules that may cause issues
|
|
5
|
+
* during MCP server connection (hangs, Gatekeeper blocks, etc.)
|
|
6
|
+
*
|
|
7
|
+
* This runs BEFORE server connection to warn users about potential
|
|
8
|
+
* issues with native binaries that may be blocked by macOS Gatekeeper
|
|
9
|
+
* or require platform-specific compilation.
|
|
10
|
+
*
|
|
11
|
+
* @module cli/lib/assessment-runner/native-module-detector
|
|
12
|
+
* @see https://github.com/triepod-ai/inspector-assessment/issues/212
|
|
13
|
+
*/
|
|
14
|
+
import { checkPackageJsonNativeModules, } from "../../../../client/lib/lib/nativeModules.js";
|
|
15
|
+
import { emitNativeModuleWarning } from "../jsonl-events.js";
|
|
16
|
+
/**
|
|
17
|
+
* Detect native modules in package.json and emit warnings
|
|
18
|
+
*
|
|
19
|
+
* This function should be called BEFORE attempting to connect to an MCP server.
|
|
20
|
+
* It scans package.json for known problematic native modules and:
|
|
21
|
+
* 1. Emits JSONL warning events for each detected module
|
|
22
|
+
* 2. Prints console warnings (unless jsonOnly is true)
|
|
23
|
+
* 3. Returns aggregated results for potential use in error messages
|
|
24
|
+
*
|
|
25
|
+
* @param packageJson - Parsed package.json content (or undefined if not available)
|
|
26
|
+
* @param options - Detection options
|
|
27
|
+
* @returns Detection result with warnings and suggestions
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const result = detectNativeModules(sourceFiles.packageJson, {
|
|
32
|
+
* jsonOnly: options.jsonOnly,
|
|
33
|
+
* serverName: options.serverName,
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* if (result.detected) {
|
|
37
|
+
* // Native modules found - warnings have been emitted
|
|
38
|
+
* // result.suggestedEnvVars contains mitigation suggestions
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function detectNativeModules(packageJson, options = {}) {
|
|
43
|
+
const result = {
|
|
44
|
+
detected: false,
|
|
45
|
+
count: 0,
|
|
46
|
+
modules: [],
|
|
47
|
+
suggestedEnvVars: {},
|
|
48
|
+
};
|
|
49
|
+
// No package.json available - nothing to check
|
|
50
|
+
if (!packageJson) {
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// Check for native modules
|
|
54
|
+
const matches = checkPackageJsonNativeModules(packageJson);
|
|
55
|
+
if (matches.length === 0) {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
// Found native modules
|
|
59
|
+
result.detected = true;
|
|
60
|
+
result.count = matches.length;
|
|
61
|
+
// Process each match
|
|
62
|
+
for (const match of matches) {
|
|
63
|
+
// Add to result modules list
|
|
64
|
+
result.modules.push({
|
|
65
|
+
name: match.module.name,
|
|
66
|
+
category: match.module.category,
|
|
67
|
+
severity: match.module.severity,
|
|
68
|
+
warningMessage: match.module.warningMessage,
|
|
69
|
+
suggestedEnvVars: match.module.suggestedEnvVars,
|
|
70
|
+
dependencyType: match.dependencyType,
|
|
71
|
+
version: match.version,
|
|
72
|
+
});
|
|
73
|
+
// Collect suggested env vars
|
|
74
|
+
if (match.module.suggestedEnvVars) {
|
|
75
|
+
Object.assign(result.suggestedEnvVars, match.module.suggestedEnvVars);
|
|
76
|
+
}
|
|
77
|
+
// Emit JSONL event for each native module
|
|
78
|
+
emitNativeModuleWarning(match.module.name, match.module.category, match.module.severity, match.module.warningMessage, match.dependencyType, match.version, match.module.suggestedEnvVars);
|
|
79
|
+
}
|
|
80
|
+
// Print console warnings if not in JSON-only mode
|
|
81
|
+
if (!options.jsonOnly) {
|
|
82
|
+
printConsoleWarnings(matches, options.serverName);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Print formatted console warnings for detected native modules
|
|
88
|
+
*/
|
|
89
|
+
function printConsoleWarnings(matches, serverName) {
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(`\x1b[33m\u26a0\ufe0f Native Module Warning: Detected ${matches.length} native module(s) that may cause issues:\x1b[0m`);
|
|
92
|
+
for (const match of matches) {
|
|
93
|
+
const severityIcon = match.module.severity === "HIGH" ? "\u{1F534}" : "\u{1F7E1}";
|
|
94
|
+
console.log(` ${severityIcon} \x1b[1m${match.module.name}\x1b[0m (${match.dependencyType})`);
|
|
95
|
+
console.log(` ${match.module.warningMessage}`);
|
|
96
|
+
if (match.module.suggestedEnvVars) {
|
|
97
|
+
const envVars = Object.entries(match.module.suggestedEnvVars)
|
|
98
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
99
|
+
.join(" ");
|
|
100
|
+
console.log(` \x1b[36mSuggested:\x1b[0m ${envVars}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(" If server hangs or times out, native binaries may be blocked by macOS Gatekeeper.");
|
|
105
|
+
console.log(" See: https://support.apple.com/en-us/HT202491");
|
|
106
|
+
console.log("");
|
|
107
|
+
}
|