@bryan-thompson/inspector-assessment 1.15.2 → 1.16.0

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.
Files changed (57) hide show
  1. package/cli/build/assess-full.js +91 -0
  2. package/cli/build/assess-security.js +111 -27
  3. package/cli/build/cli.js +119 -3
  4. package/client/dist/assets/{OAuthCallback-B2W3bBou.js → OAuthCallback-KwMiy-L3.js} +1 -1
  5. package/client/dist/assets/{OAuthDebugCallback-BL3_Hknj.js → OAuthDebugCallback-hckdJlo3.js} +1 -1
  6. package/client/dist/assets/{index-Css2Fvxh.js → index-C89umkGV.js} +512 -665
  7. package/client/dist/index.html +1 -1
  8. package/client/lib/lib/assessmentTypes.d.ts +123 -0
  9. package/client/lib/lib/assessmentTypes.d.ts.map +1 -1
  10. package/client/lib/lib/assessmentTypes.js +20 -0
  11. package/client/lib/lib/securityPatterns.d.ts +2 -2
  12. package/client/lib/lib/securityPatterns.d.ts.map +1 -1
  13. package/client/lib/lib/securityPatterns.js +215 -16
  14. package/client/lib/services/assessment/AssessmentOrchestrator.d.ts +67 -0
  15. package/client/lib/services/assessment/AssessmentOrchestrator.d.ts.map +1 -1
  16. package/client/lib/services/assessment/AssessmentOrchestrator.js +91 -1
  17. package/client/lib/services/assessment/ResponseValidator.d.ts +7 -34
  18. package/client/lib/services/assessment/ResponseValidator.d.ts.map +1 -1
  19. package/client/lib/services/assessment/ResponseValidator.js +100 -704
  20. package/client/lib/services/assessment/config/annotationPatterns.js +1 -1
  21. package/client/lib/services/assessment/lib/RequestHistoryAnalyzer.d.ts +67 -0
  22. package/client/lib/services/assessment/lib/RequestHistoryAnalyzer.d.ts.map +1 -0
  23. package/client/lib/services/assessment/lib/RequestHistoryAnalyzer.js +191 -0
  24. package/client/lib/services/assessment/lib/claudeCodeBridge.d.ts +1 -0
  25. package/client/lib/services/assessment/lib/claudeCodeBridge.d.ts.map +1 -1
  26. package/client/lib/services/assessment/lib/claudeCodeBridge.js +5 -4
  27. package/client/lib/services/assessment/modules/AuthenticationAssessor.d.ts +4 -0
  28. package/client/lib/services/assessment/modules/AuthenticationAssessor.d.ts.map +1 -1
  29. package/client/lib/services/assessment/modules/AuthenticationAssessor.js +97 -1
  30. package/client/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.d.ts +39 -0
  31. package/client/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.d.ts.map +1 -0
  32. package/client/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.js +330 -0
  33. package/client/lib/services/assessment/modules/FunctionalityAssessor.d.ts.map +1 -1
  34. package/client/lib/services/assessment/modules/FunctionalityAssessor.js +46 -13
  35. package/client/lib/services/assessment/modules/MCPSpecComplianceAssessor.d.ts +5 -0
  36. package/client/lib/services/assessment/modules/MCPSpecComplianceAssessor.d.ts.map +1 -1
  37. package/client/lib/services/assessment/modules/MCPSpecComplianceAssessor.js +81 -0
  38. package/client/lib/services/assessment/modules/ManifestValidationAssessor.js +1 -1
  39. package/client/lib/services/assessment/modules/PromptAssessor.d.ts +30 -0
  40. package/client/lib/services/assessment/modules/PromptAssessor.d.ts.map +1 -0
  41. package/client/lib/services/assessment/modules/PromptAssessor.js +367 -0
  42. package/client/lib/services/assessment/modules/ResourceAssessor.d.ts +28 -0
  43. package/client/lib/services/assessment/modules/ResourceAssessor.d.ts.map +1 -0
  44. package/client/lib/services/assessment/modules/ResourceAssessor.js +296 -0
  45. package/client/lib/services/assessment/modules/SecurityAssessor.d.ts +4 -2
  46. package/client/lib/services/assessment/modules/SecurityAssessor.d.ts.map +1 -1
  47. package/client/lib/services/assessment/modules/SecurityAssessor.js +10 -41
  48. package/client/lib/utils/jsonUtils.d.ts +68 -0
  49. package/client/lib/utils/jsonUtils.d.ts.map +1 -0
  50. package/client/lib/utils/jsonUtils.js +141 -0
  51. package/client/lib/utils/paramUtils.d.ts +11 -0
  52. package/client/lib/utils/paramUtils.d.ts.map +1 -0
  53. package/client/lib/utils/paramUtils.js +37 -0
  54. package/client/lib/utils/schemaUtils.d.ts +74 -0
  55. package/client/lib/utils/schemaUtils.d.ts.map +1 -0
  56. package/client/lib/utils/schemaUtils.js +268 -0
  57. package/package.json +4 -4
@@ -252,6 +252,10 @@ function buildConfig(options) {
252
252
  portability: true,
253
253
  externalAPIScanner: !!options.sourceCodePath,
254
254
  temporal: !options.skipTemporal, // Enable by default with --full, skip with --skip-temporal
255
+ // New capability assessors - always enabled in full mode
256
+ resources: true,
257
+ prompts: true,
258
+ crossCapability: true,
255
259
  };
256
260
  }
257
261
  // Temporal/rug pull detection configuration
@@ -304,6 +308,61 @@ async function runFullAssessment(options) {
304
308
  if (!options.jsonOnly) {
305
309
  console.log(`🔧 Found ${tools.length} tool${tools.length !== 1 ? "s" : ""}`);
306
310
  }
311
+ // Fetch resources for new capability assessments
312
+ let resources = [];
313
+ let resourceTemplates = [];
314
+ try {
315
+ const resourcesResponse = await client.listResources();
316
+ resources = (resourcesResponse.resources || []).map((r) => ({
317
+ uri: r.uri,
318
+ name: r.name,
319
+ description: r.description,
320
+ mimeType: r.mimeType,
321
+ }));
322
+ // resourceTemplates may be typed as unknown in some SDK versions
323
+ const templates = resourcesResponse.resourceTemplates;
324
+ if (templates) {
325
+ resourceTemplates = templates.map((rt) => ({
326
+ uriTemplate: rt.uriTemplate,
327
+ name: rt.name,
328
+ description: rt.description,
329
+ mimeType: rt.mimeType,
330
+ }));
331
+ }
332
+ if (!options.jsonOnly &&
333
+ (resources.length > 0 || resourceTemplates.length > 0)) {
334
+ console.log(`📦 Found ${resources.length} resource(s) and ${resourceTemplates.length} resource template(s)`);
335
+ }
336
+ }
337
+ catch {
338
+ // Server may not support resources - that's okay
339
+ if (!options.jsonOnly) {
340
+ console.log("📦 Resources not supported by server");
341
+ }
342
+ }
343
+ // Fetch prompts for new capability assessments
344
+ let prompts = [];
345
+ try {
346
+ const promptsResponse = await client.listPrompts();
347
+ prompts = (promptsResponse.prompts || []).map((p) => ({
348
+ name: p.name,
349
+ description: p.description,
350
+ arguments: p.arguments?.map((a) => ({
351
+ name: a.name,
352
+ description: a.description,
353
+ required: a.required,
354
+ })),
355
+ }));
356
+ if (!options.jsonOnly && prompts.length > 0) {
357
+ console.log(`💬 Found ${prompts.length} prompt(s)`);
358
+ }
359
+ }
360
+ catch {
361
+ // Server may not support prompts - that's okay
362
+ if (!options.jsonOnly) {
363
+ console.log("💬 Prompts not supported by server");
364
+ }
365
+ }
307
366
  // State management for resumable assessments
308
367
  const stateManager = new AssessmentStateManager(options.serverName);
309
368
  if (stateManager.exists() && !options.noResume) {
@@ -398,6 +457,32 @@ async function runFullAssessment(options) {
398
457
  console.log(`📁 Loaded source files from: ${options.sourceCodePath}`);
399
458
  }
400
459
  }
460
+ // Create readResource wrapper for ResourceAssessor
461
+ const readResource = async (uri) => {
462
+ const response = await client.readResource({ uri });
463
+ // Extract text content from response
464
+ if (response.contents && response.contents.length > 0) {
465
+ const content = response.contents[0];
466
+ if ("text" in content && content.text) {
467
+ return content.text;
468
+ }
469
+ if ("blob" in content && content.blob) {
470
+ // Return base64 blob as string
471
+ return content.blob;
472
+ }
473
+ }
474
+ return "";
475
+ };
476
+ // Create getPrompt wrapper for PromptAssessor
477
+ const getPrompt = async (name, args) => {
478
+ const response = await client.getPrompt({ name, arguments: args });
479
+ return {
480
+ messages: (response.messages || []).map((m) => ({
481
+ role: m.role,
482
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
483
+ })),
484
+ };
485
+ };
401
486
  const context = {
402
487
  serverName: options.serverName,
403
488
  tools,
@@ -405,6 +490,12 @@ async function runFullAssessment(options) {
405
490
  config,
406
491
  sourceCodePath: options.sourceCodePath,
407
492
  ...sourceFiles,
493
+ // New capability assessment data
494
+ resources,
495
+ resourceTemplates,
496
+ prompts,
497
+ readResource,
498
+ getPrompt,
408
499
  };
409
500
  if (!options.jsonOnly) {
410
501
  console.log(`\n🏃 Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
@@ -12,6 +12,74 @@
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
14
  import * as os from "os";
15
+ import { execSync } from "child_process";
16
+ /**
17
+ * Validate that a command is safe to execute
18
+ * - Must be an absolute path or resolvable via PATH
19
+ * - Must not contain shell metacharacters
20
+ */
21
+ function validateCommand(command) {
22
+ // Check for shell metacharacters that could indicate injection
23
+ const dangerousChars = /[;&|`$(){}[\]<>!\\]/;
24
+ if (dangerousChars.test(command)) {
25
+ throw new Error(`Invalid command: contains shell metacharacters: ${command}`);
26
+ }
27
+ // Verify the command exists and is executable
28
+ try {
29
+ // Use 'which' on Unix-like systems, 'where' on Windows
30
+ const whichCmd = process.platform === "win32" ? "where" : "which";
31
+ execSync(`${whichCmd} "${command}"`, { stdio: "pipe" });
32
+ }
33
+ catch {
34
+ // Check if it's an absolute path that exists
35
+ if (path.isAbsolute(command) && fs.existsSync(command)) {
36
+ try {
37
+ fs.accessSync(command, fs.constants.X_OK);
38
+ return; // Command exists and is executable
39
+ }
40
+ catch {
41
+ throw new Error(`Command not executable: ${command}`);
42
+ }
43
+ }
44
+ throw new Error(`Command not found: ${command}`);
45
+ }
46
+ }
47
+ /**
48
+ * Validate environment variables from config
49
+ * - Keys must be valid env var names (alphanumeric + underscore)
50
+ * - Values should not contain null bytes
51
+ */
52
+ function validateEnvVars(env) {
53
+ if (!env)
54
+ return {};
55
+ const validatedEnv = {};
56
+ const validKeyPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
57
+ for (const [key, value] of Object.entries(env)) {
58
+ // Validate key format
59
+ if (!validKeyPattern.test(key)) {
60
+ console.warn(`Skipping invalid environment variable name: ${key} (must match [a-zA-Z_][a-zA-Z0-9_]*)`);
61
+ continue;
62
+ }
63
+ // Check for null bytes in value (could truncate strings)
64
+ if (typeof value === "string" && value.includes("\0")) {
65
+ console.warn(`Skipping environment variable with null byte: ${key}`);
66
+ continue;
67
+ }
68
+ validatedEnv[key] = String(value);
69
+ }
70
+ return validatedEnv;
71
+ }
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
+ }
15
83
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
84
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
17
85
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
@@ -31,34 +99,46 @@ function loadServerConfig(serverName, configPath) {
31
99
  for (const tryPath of possiblePaths) {
32
100
  if (!fs.existsSync(tryPath))
33
101
  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
- };
43
- }
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`);
102
+ const rawConfig = safeJsonParse(fs.readFileSync(tryPath, "utf-8"), tryPath);
103
+ // Type guard: check if it's a Claude Desktop config with mcpServers
104
+ if (rawConfig &&
105
+ typeof rawConfig === "object" &&
106
+ "mcpServers" in rawConfig) {
107
+ const desktopConfig = rawConfig;
108
+ const serverConfig = desktopConfig.mcpServers?.[serverName];
109
+ if (serverConfig) {
110
+ return {
111
+ transport: "stdio",
112
+ command: serverConfig.command,
113
+ args: serverConfig.args || [],
114
+ env: serverConfig.env || {},
115
+ };
49
116
  }
50
- return {
51
- transport: config.transport || "http",
52
- url: config.url,
53
- };
54
117
  }
55
- if (config.command) {
56
- return {
57
- transport: "stdio",
58
- command: config.command,
59
- args: config.args || [],
60
- env: config.env || {},
61
- };
118
+ // Type guard: check if it's a direct config file
119
+ if (rawConfig && typeof rawConfig === "object") {
120
+ const directConfig = rawConfig;
121
+ // Check for HTTP/SSE transport
122
+ if (directConfig.url ||
123
+ directConfig.transport === "http" ||
124
+ directConfig.transport === "sse") {
125
+ if (!directConfig.url) {
126
+ throw new Error(`Invalid server config: transport is '${directConfig.transport}' but 'url' is missing`);
127
+ }
128
+ return {
129
+ transport: directConfig.transport || "http",
130
+ url: directConfig.url,
131
+ };
132
+ }
133
+ // Check for stdio transport
134
+ if (directConfig.command) {
135
+ return {
136
+ transport: "stdio",
137
+ command: directConfig.command,
138
+ args: directConfig.args || [],
139
+ env: directConfig.env || {},
140
+ };
141
+ }
62
142
  }
63
143
  }
64
144
  throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
@@ -83,12 +163,16 @@ async function connectToServer(config) {
83
163
  default:
84
164
  if (!config.command)
85
165
  throw new Error("Command required for stdio transport");
166
+ // Validate command before execution to prevent injection attacks
167
+ validateCommand(config.command);
168
+ // Validate and sanitize environment variables from config
169
+ const validatedEnv = validateEnvVars(config.env);
86
170
  transport = new StdioClientTransport({
87
171
  command: config.command,
88
172
  args: config.args,
89
173
  env: {
90
174
  ...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
91
- ...config.env,
175
+ ...validatedEnv,
92
176
  },
93
177
  stderr: "pipe",
94
178
  });
package/cli/build/cli.js CHANGED
@@ -5,7 +5,103 @@ import path from "node:path";
5
5
  import { dirname, resolve } from "path";
6
6
  import { spawnPromise } from "spawn-rx";
7
7
  import { fileURLToPath } from "url";
8
+ import { execSync } from "node:child_process";
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ /**
11
+ * Validate environment variable names
12
+ * - Must start with letter or underscore
13
+ * - Can contain letters, numbers, underscores
14
+ */
15
+ function isValidEnvVarName(name) {
16
+ return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
17
+ }
18
+ /**
19
+ * Validate environment variable values
20
+ * - No null bytes (could truncate strings)
21
+ */
22
+ function isValidEnvVarValue(value) {
23
+ return !value.includes("\0");
24
+ }
25
+ /**
26
+ * Validate and sanitize environment variables
27
+ * Returns filtered environment variables with invalid entries removed
28
+ */
29
+ function validateEnvVars(env) {
30
+ const validated = {};
31
+ for (const [key, value] of Object.entries(env)) {
32
+ if (!isValidEnvVarName(key)) {
33
+ console.warn(`Warning: Skipping invalid environment variable name: ${key}`);
34
+ continue;
35
+ }
36
+ if (!isValidEnvVarValue(value)) {
37
+ console.warn(`Warning: Skipping environment variable with invalid value: ${key}`);
38
+ continue;
39
+ }
40
+ validated[key] = value;
41
+ }
42
+ return validated;
43
+ }
44
+ /**
45
+ * Validate that a URL is safe for connection
46
+ * - Must be http or https
47
+ * - Blocks private/internal IPs to prevent SSRF
48
+ */
49
+ function validateServerUrl(url) {
50
+ try {
51
+ const parsed = new URL(url);
52
+ // Must be http or https
53
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
54
+ throw new Error(`Invalid URL protocol: ${parsed.protocol}. Must be http or https.`);
55
+ }
56
+ // Block private IPs to prevent SSRF attacks
57
+ const hostname = parsed.hostname.toLowerCase();
58
+ const privatePatterns = [
59
+ /^localhost$/,
60
+ /^127\./,
61
+ /^10\./,
62
+ /^172\.(1[6-9]|2[0-9]|3[01])\./,
63
+ /^192\.168\./,
64
+ /^169\.254\./, // Link-local
65
+ /^\[::1\]$/, // IPv6 localhost
66
+ /^\[fe80:/i, // IPv6 link-local
67
+ ];
68
+ // Only warn for private IPs (don't block - may be intentional for local testing)
69
+ if (privatePatterns.some((pattern) => pattern.test(hostname))) {
70
+ console.warn(`Warning: Connecting to private/internal address: ${hostname}`);
71
+ }
72
+ }
73
+ catch (error) {
74
+ if (error instanceof Error && error.message.startsWith("Invalid URL")) {
75
+ throw new Error(`Invalid server URL: ${url}`);
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ /**
81
+ * Validate a command exists and is safe to execute
82
+ */
83
+ function validateCommand(command) {
84
+ // Check for shell metacharacters
85
+ const dangerousChars = /[;&|`$(){}[\]<>!]/;
86
+ if (dangerousChars.test(command)) {
87
+ throw new Error(`Invalid command: contains shell metacharacters: ${command}`);
88
+ }
89
+ // For absolute paths, verify the file exists
90
+ if (path.isAbsolute(command)) {
91
+ if (!fs.existsSync(command)) {
92
+ throw new Error(`Command not found: ${command}`);
93
+ }
94
+ return;
95
+ }
96
+ // For relative commands, verify they exist in PATH
97
+ try {
98
+ const whichCmd = process.platform === "win32" ? "where" : "which";
99
+ execSync(`${whichCmd} "${command}"`, { stdio: "pipe" });
100
+ }
101
+ catch {
102
+ throw new Error(`Command not found in PATH: ${command}`);
103
+ }
104
+ }
9
105
  function handleError(error) {
10
106
  let message;
11
107
  if (error instanceof Error) {
@@ -24,6 +120,14 @@ function delay(ms) {
24
120
  return new Promise((resolve) => setTimeout(resolve, ms, true));
25
121
  }
26
122
  async function runWebClient(args) {
123
+ // Validate inputs before proceeding
124
+ const validatedEnvArgs = validateEnvVars(args.envArgs);
125
+ if (args.serverUrl) {
126
+ validateServerUrl(args.serverUrl);
127
+ }
128
+ if (args.command) {
129
+ validateCommand(args.command);
130
+ }
27
131
  // Path to the client entry point
28
132
  const inspectorClientPath = resolve(__dirname, "../../", "client", "bin", "start.js");
29
133
  const abort = new AbortController();
@@ -34,8 +138,8 @@ async function runWebClient(args) {
34
138
  });
35
139
  // Build arguments to pass to start.js
36
140
  const startArgs = [];
37
- // Pass environment variables
38
- for (const [key, value] of Object.entries(args.envArgs)) {
141
+ // Pass validated environment variables
142
+ for (const [key, value] of Object.entries(validatedEnvArgs)) {
39
143
  startArgs.push("-e", `${key}=${value}`);
40
144
  }
41
145
  // Pass transport type if specified
@@ -70,6 +174,18 @@ async function runWebClient(args) {
70
174
  }
71
175
  }
72
176
  async function runCli(args) {
177
+ // Validate inputs before proceeding
178
+ const validatedEnvArgs = validateEnvVars(args.envArgs);
179
+ if (args.command) {
180
+ // For CLI mode, command might be a URL - validate appropriately
181
+ if (args.command.startsWith("http://") ||
182
+ args.command.startsWith("https://")) {
183
+ validateServerUrl(args.command);
184
+ }
185
+ else {
186
+ validateCommand(args.command);
187
+ }
188
+ }
73
189
  const projectRoot = resolve(__dirname, "..");
74
190
  const cliPath = resolve(projectRoot, "build", "index.js");
75
191
  const abort = new AbortController();
@@ -96,7 +212,7 @@ async function runCli(args) {
96
212
  }
97
213
  }
98
214
  await spawnPromise("node", cliArgs, {
99
- env: { ...process.env, ...args.envArgs },
215
+ env: { ...process.env, ...validatedEnvArgs },
100
216
  signal: abort.signal,
101
217
  echoOutput: true,
102
218
  // pipe the stdout through here, prevents issues with buffering and
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-Css2Fvxh.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-C89umkGV.js";
2
2
  const OAuthCallback = ({ onConnect }) => {
3
3
  const { toast } = useToast();
4
4
  const hasProcessedRef = reactExports.useRef(false);
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-Css2Fvxh.js";
1
+ import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-C89umkGV.js";
2
2
  const OAuthDebugCallback = ({ onConnect }) => {
3
3
  reactExports.useEffect(() => {
4
4
  let isProcessed = false;