@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.
@@ -11,73 +11,58 @@
11
11
  */
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
- import { execSync } from "child_process";
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 shared server config loading (Issue #84 - Zod validation)
20
- import { loadServerConfig } from "./lib/assessment-runner/server-config.js";
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
- * Validate environment variables from config
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 validateEnvVars(env) {
58
- if (!env)
59
- return {};
60
- const validatedEnv = {};
61
- const validKeyPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
62
- for (const [key, value] of Object.entries(env)) {
63
- // Validate key format
64
- if (!validKeyPattern.test(key)) {
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
- // Check for null bytes in value (could truncate strings)
69
- if (typeof value === "string" && value.includes("\0")) {
70
- console.warn(`Skipping environment variable with null byte: ${key}`);
71
- continue;
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
- return validatedEnv;
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
- ...validatedEnv,
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: 30,
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 30 attack patterns...`);
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 30 attack patterns.
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 (30 total):
341
- • Command Injection, SQL Injection, Path Traversal
342
- • Calculator Injection, Code Execution, XXE
343
- Data Exfiltration, Token Theft, NoSQL Injection
344
- Unicode Bypass, Nested Injection, Package Squatting
345
- Session Management, Auth Bypass, and more...
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
- if (options.fullAssessment !== false) {
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
+ }