@bryan-thompson/inspector-assessment-cli 1.26.6 → 1.26.7
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/assessment-executor.test.js +248 -0
- package/build/__tests__/assessment-runner/config-builder.test.js +289 -0
- package/build/__tests__/assessment-runner/index.test.js +41 -0
- package/build/__tests__/assessment-runner/server-config.test.js +249 -0
- package/build/__tests__/assessment-runner/server-connection.test.js +221 -0
- package/build/__tests__/assessment-runner/source-loader.test.js +341 -0
- package/build/__tests__/assessment-runner/tool-wrapper.test.js +114 -0
- package/build/__tests__/assessment-runner-facade.test.js +118 -0
- package/build/assess-full.js +26 -1254
- package/build/lib/assessment-runner/assessment-executor.js +323 -0
- package/build/lib/assessment-runner/config-builder.js +127 -0
- package/build/lib/assessment-runner/index.js +20 -0
- package/build/lib/assessment-runner/server-config.js +78 -0
- package/build/lib/assessment-runner/server-connection.js +80 -0
- package/build/lib/assessment-runner/source-loader.js +139 -0
- package/build/lib/assessment-runner/tool-wrapper.js +40 -0
- package/build/lib/assessment-runner/types.js +8 -0
- package/build/lib/assessment-runner.js +6 -740
- package/build/lib/comparison-handler.js +84 -0
- package/build/lib/result-output.js +154 -0
- package/package.json +1 -1
package/build/assess-full.js
CHANGED
|
@@ -9,1209 +9,15 @@
|
|
|
9
9
|
* mcp-assess-full --server <server-name> [--claude-enabled] [--full]
|
|
10
10
|
* mcp-assess-full my-server --source ./my-server --output ./results.json
|
|
11
11
|
*/
|
|
12
|
-
import * as fs from "fs";
|
|
13
|
-
import * as path from "path";
|
|
14
|
-
import * as os from "os";
|
|
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 from local client lib (will use package exports when published)
|
|
20
|
-
import { AssessmentOrchestrator, } from "../../client/lib/services/assessment/AssessmentOrchestrator.js";
|
|
21
|
-
import { DEFAULT_ASSESSMENT_CONFIG, ASSESSMENT_CATEGORY_METADATA, getAllModulesConfig, } from "../../client/lib/lib/assessmentTypes.js";
|
|
22
|
-
import { FULL_CLAUDE_CODE_CONFIG } from "../../client/lib/services/assessment/lib/claudeCodeBridge.js";
|
|
23
|
-
import { createFormatter, } from "../../client/lib/lib/reportFormatters/index.js";
|
|
24
|
-
import { generatePolicyComplianceReport } from "../../client/lib/services/assessment/PolicyComplianceGenerator.js";
|
|
25
|
-
import { compareAssessments } from "../../client/lib/lib/assessmentDiffer.js";
|
|
26
|
-
import { formatDiffAsMarkdown } from "../../client/lib/lib/reportFormatters/DiffReportFormatter.js";
|
|
27
|
-
import { AssessmentStateManager } from "./assessmentState.js";
|
|
28
|
-
import { emitServerConnected, emitToolDiscovered, emitToolsDiscoveryComplete, emitAssessmentComplete, emitTestBatch, emitVulnerabilityFound, emitAnnotationMissing, emitAnnotationMisaligned, emitAnnotationReviewRecommended, emitAnnotationAligned, emitModulesConfigured, } from "./lib/jsonl-events.js";
|
|
29
|
-
import { loadPerformanceConfig, } from "../../client/lib/services/assessment/config/performanceConfig.js";
|
|
30
12
|
import { ScopedListenerConfig } from "./lib/event-config.js";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.split(",")
|
|
40
|
-
.map((n) => n.trim())
|
|
41
|
-
.filter(Boolean);
|
|
42
|
-
const invalid = names.filter((n) => !VALID_MODULE_NAMES.includes(n));
|
|
43
|
-
if (invalid.length > 0) {
|
|
44
|
-
console.error(`Error: Invalid module name(s) for ${flagName}: ${invalid.join(", ")}`);
|
|
45
|
-
console.error(`Valid modules: ${VALID_MODULE_NAMES.join(", ")}`);
|
|
46
|
-
setTimeout(() => process.exit(1), 10);
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
return names;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Load server configuration from Claude Code's MCP settings
|
|
53
|
-
*/
|
|
54
|
-
function loadServerConfig(serverName, configPath) {
|
|
55
|
-
const possiblePaths = [
|
|
56
|
-
configPath,
|
|
57
|
-
path.join(os.homedir(), ".config", "mcp", "servers", `${serverName}.json`),
|
|
58
|
-
path.join(os.homedir(), ".config", "claude", "claude_desktop_config.json"),
|
|
59
|
-
].filter(Boolean);
|
|
60
|
-
for (const tryPath of possiblePaths) {
|
|
61
|
-
if (!fs.existsSync(tryPath))
|
|
62
|
-
continue;
|
|
63
|
-
const config = JSON.parse(fs.readFileSync(tryPath, "utf-8"));
|
|
64
|
-
if (config.mcpServers && config.mcpServers[serverName]) {
|
|
65
|
-
const serverConfig = config.mcpServers[serverName];
|
|
66
|
-
// Check if serverConfig specifies http/sse transport
|
|
67
|
-
if (serverConfig.url ||
|
|
68
|
-
serverConfig.transport === "http" ||
|
|
69
|
-
serverConfig.transport === "sse") {
|
|
70
|
-
if (!serverConfig.url) {
|
|
71
|
-
throw new Error(`Invalid server config: transport is '${serverConfig.transport}' but 'url' is missing`);
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
transport: serverConfig.transport || "http",
|
|
75
|
-
url: serverConfig.url,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
// Default to stdio transport
|
|
79
|
-
return {
|
|
80
|
-
transport: "stdio",
|
|
81
|
-
command: serverConfig.command,
|
|
82
|
-
args: serverConfig.args || [],
|
|
83
|
-
env: serverConfig.env || {},
|
|
84
|
-
cwd: serverConfig.cwd,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (config.url ||
|
|
88
|
-
config.transport === "http" ||
|
|
89
|
-
config.transport === "sse") {
|
|
90
|
-
if (!config.url) {
|
|
91
|
-
throw new Error(`Invalid server config: transport is '${config.transport}' but 'url' is missing`);
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
transport: config.transport || "http",
|
|
95
|
-
url: config.url,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
if (config.command) {
|
|
99
|
-
return {
|
|
100
|
-
transport: "stdio",
|
|
101
|
-
command: config.command,
|
|
102
|
-
args: config.args || [],
|
|
103
|
-
env: config.env || {},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
throw new Error(`Server config not found for: ${serverName}\nTried: ${possiblePaths.join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Load optional files from source code path
|
|
111
|
-
*/
|
|
112
|
-
function loadSourceFiles(sourcePath) {
|
|
113
|
-
const result = {};
|
|
114
|
-
// Search for README in source directory and parent directories (up to 3 levels)
|
|
115
|
-
// This handles cases where --source points to a subdirectory but README is at repo root
|
|
116
|
-
const readmePaths = ["README.md", "readme.md", "Readme.md"];
|
|
117
|
-
let readmeFound = false;
|
|
118
|
-
// First try the source directory itself
|
|
119
|
-
for (const readmePath of readmePaths) {
|
|
120
|
-
const fullPath = path.join(sourcePath, readmePath);
|
|
121
|
-
if (fs.existsSync(fullPath)) {
|
|
122
|
-
result.readmeContent = fs.readFileSync(fullPath, "utf-8");
|
|
123
|
-
readmeFound = true;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// If not found, search parent directories (up to 3 levels)
|
|
128
|
-
if (!readmeFound) {
|
|
129
|
-
let currentDir = sourcePath;
|
|
130
|
-
for (let i = 0; i < 3; i++) {
|
|
131
|
-
const parentDir = path.dirname(currentDir);
|
|
132
|
-
if (parentDir === currentDir)
|
|
133
|
-
break; // Reached filesystem root
|
|
134
|
-
for (const readmePath of readmePaths) {
|
|
135
|
-
const fullPath = path.join(parentDir, readmePath);
|
|
136
|
-
if (fs.existsSync(fullPath)) {
|
|
137
|
-
result.readmeContent = fs.readFileSync(fullPath, "utf-8");
|
|
138
|
-
readmeFound = true;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (readmeFound)
|
|
143
|
-
break;
|
|
144
|
-
currentDir = parentDir;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const packagePath = path.join(sourcePath, "package.json");
|
|
148
|
-
if (fs.existsSync(packagePath)) {
|
|
149
|
-
result.packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
150
|
-
}
|
|
151
|
-
const manifestPath = path.join(sourcePath, "manifest.json");
|
|
152
|
-
if (fs.existsSync(manifestPath)) {
|
|
153
|
-
result.manifestRaw = fs.readFileSync(manifestPath, "utf-8");
|
|
154
|
-
try {
|
|
155
|
-
result.manifestJson = JSON.parse(result.manifestRaw);
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
console.warn("[Assessment] Failed to parse manifest.json");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
result.sourceCodeFiles = new Map();
|
|
162
|
-
// Include config files for portability analysis
|
|
163
|
-
const sourceExtensions = [
|
|
164
|
-
".ts",
|
|
165
|
-
".js",
|
|
166
|
-
".py",
|
|
167
|
-
".go",
|
|
168
|
-
".rs",
|
|
169
|
-
".json",
|
|
170
|
-
".sh",
|
|
171
|
-
".yaml",
|
|
172
|
-
".yml",
|
|
173
|
-
];
|
|
174
|
-
// Parse .gitignore patterns
|
|
175
|
-
const gitignorePatterns = [];
|
|
176
|
-
const gitignorePath = path.join(sourcePath, ".gitignore");
|
|
177
|
-
if (fs.existsSync(gitignorePath)) {
|
|
178
|
-
const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
|
|
179
|
-
for (const line of gitignoreContent.split("\n")) {
|
|
180
|
-
const trimmed = line.trim();
|
|
181
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
182
|
-
continue;
|
|
183
|
-
// Convert gitignore pattern to regex
|
|
184
|
-
const pattern = trimmed
|
|
185
|
-
.replace(/\./g, "\\.")
|
|
186
|
-
.replace(/\*\*/g, ".*")
|
|
187
|
-
.replace(/\*/g, "[^/]*")
|
|
188
|
-
.replace(/\?/g, ".");
|
|
189
|
-
try {
|
|
190
|
-
gitignorePatterns.push(new RegExp(pattern));
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// Skip invalid patterns
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const isGitignored = (relativePath) => {
|
|
198
|
-
return gitignorePatterns.some((pattern) => pattern.test(relativePath));
|
|
199
|
-
};
|
|
200
|
-
const loadSourceDir = (dir, prefix = "") => {
|
|
201
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
202
|
-
for (const entry of entries) {
|
|
203
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
204
|
-
continue;
|
|
205
|
-
const fullPath = path.join(dir, entry.name);
|
|
206
|
-
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
207
|
-
// Skip gitignored files
|
|
208
|
-
if (isGitignored(relativePath))
|
|
209
|
-
continue;
|
|
210
|
-
if (entry.isDirectory()) {
|
|
211
|
-
loadSourceDir(fullPath, relativePath);
|
|
212
|
-
}
|
|
213
|
-
else if (sourceExtensions.some((ext) => entry.name.endsWith(ext))) {
|
|
214
|
-
try {
|
|
215
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
216
|
-
if (content.length < 100000) {
|
|
217
|
-
result.sourceCodeFiles.set(relativePath, content);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
// Skip unreadable files
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
try {
|
|
227
|
-
loadSourceDir(sourcePath);
|
|
228
|
-
}
|
|
229
|
-
catch (e) {
|
|
230
|
-
console.warn("[Assessment] Could not load source files:", e);
|
|
231
|
-
}
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Connect to MCP server via configured transport
|
|
236
|
-
*/
|
|
237
|
-
async function connectToServer(config) {
|
|
238
|
-
let transport;
|
|
239
|
-
let stderrData = ""; // Capture stderr for error reporting
|
|
240
|
-
switch (config.transport) {
|
|
241
|
-
case "http":
|
|
242
|
-
if (!config.url)
|
|
243
|
-
throw new Error("URL required for HTTP transport");
|
|
244
|
-
transport = new StreamableHTTPClientTransport(new URL(config.url));
|
|
245
|
-
break;
|
|
246
|
-
case "sse":
|
|
247
|
-
if (!config.url)
|
|
248
|
-
throw new Error("URL required for SSE transport");
|
|
249
|
-
transport = new SSEClientTransport(new URL(config.url));
|
|
250
|
-
break;
|
|
251
|
-
case "stdio":
|
|
252
|
-
default:
|
|
253
|
-
if (!config.command)
|
|
254
|
-
throw new Error("Command required for stdio transport");
|
|
255
|
-
transport = new StdioClientTransport({
|
|
256
|
-
command: config.command,
|
|
257
|
-
args: config.args,
|
|
258
|
-
env: {
|
|
259
|
-
...Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined)),
|
|
260
|
-
...config.env,
|
|
261
|
-
},
|
|
262
|
-
cwd: config.cwd,
|
|
263
|
-
stderr: "pipe",
|
|
264
|
-
});
|
|
265
|
-
// Capture stderr BEFORE connecting - critical for error context
|
|
266
|
-
// The MCP SDK creates a PassThrough stream immediately when stderr: "pipe"
|
|
267
|
-
// is set, allowing us to attach listeners before start() is called
|
|
268
|
-
const stderrStream = transport.stderr;
|
|
269
|
-
if (stderrStream) {
|
|
270
|
-
stderrStream.on("data", (data) => {
|
|
271
|
-
stderrData += data.toString();
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
const client = new Client({
|
|
277
|
-
name: "mcp-assess-full",
|
|
278
|
-
version: "1.0.0",
|
|
279
|
-
}, {
|
|
280
|
-
capabilities: {},
|
|
281
|
-
});
|
|
282
|
-
try {
|
|
283
|
-
await client.connect(transport);
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
287
|
-
// Provide helpful context when connection fails
|
|
288
|
-
if (stderrData.trim()) {
|
|
289
|
-
throw new Error(`Failed to connect to MCP server: ${errorMessage}\n\n` +
|
|
290
|
-
`Server stderr:\n${stderrData.trim()}\n\n` +
|
|
291
|
-
`Common causes:\n` +
|
|
292
|
-
` - Missing environment variables (check .env file)\n` +
|
|
293
|
-
` - Required external services not running\n` +
|
|
294
|
-
` - Missing API credentials`);
|
|
295
|
-
}
|
|
296
|
-
throw new Error(`Failed to connect to MCP server: ${errorMessage}`);
|
|
297
|
-
}
|
|
298
|
-
return client;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Create callTool wrapper for assessment context
|
|
302
|
-
*/
|
|
303
|
-
function createCallToolWrapper(client) {
|
|
304
|
-
return async (name, params) => {
|
|
305
|
-
try {
|
|
306
|
-
const response = await client.callTool({
|
|
307
|
-
name,
|
|
308
|
-
arguments: params,
|
|
309
|
-
});
|
|
310
|
-
return {
|
|
311
|
-
content: response.content,
|
|
312
|
-
isError: response.isError || false,
|
|
313
|
-
structuredContent: response
|
|
314
|
-
.structuredContent,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
return {
|
|
319
|
-
content: [
|
|
320
|
-
{
|
|
321
|
-
type: "text",
|
|
322
|
-
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
323
|
-
},
|
|
324
|
-
],
|
|
325
|
-
isError: true,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Build assessment configuration
|
|
332
|
-
*/
|
|
333
|
-
function buildConfig(options) {
|
|
334
|
-
const config = {
|
|
335
|
-
...DEFAULT_ASSESSMENT_CONFIG,
|
|
336
|
-
enableExtendedAssessment: options.fullAssessment !== false,
|
|
337
|
-
parallelTesting: true,
|
|
338
|
-
testTimeout: 30000,
|
|
339
|
-
enableSourceCodeAnalysis: Boolean(options.sourceCodePath),
|
|
340
|
-
};
|
|
341
|
-
if (options.fullAssessment !== false) {
|
|
342
|
-
// Priority: --profile > --only-modules > --skip-modules > default (all)
|
|
343
|
-
if (options.profile) {
|
|
344
|
-
// Use profile-based module selection
|
|
345
|
-
const profileModules = getProfileModules(options.profile, {
|
|
346
|
-
hasSourceCode: Boolean(options.sourceCodePath),
|
|
347
|
-
skipTemporal: options.skipTemporal,
|
|
348
|
-
});
|
|
349
|
-
// Convert new-style module list to legacy config format
|
|
350
|
-
// (until orchestrator is updated to use new naming)
|
|
351
|
-
config.assessmentCategories = modulesToLegacyConfig(profileModules);
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
// Derive module config from ASSESSMENT_CATEGORY_METADATA (single source of truth)
|
|
355
|
-
const allModules = getAllModulesConfig({
|
|
356
|
-
sourceCodePath: Boolean(options.sourceCodePath),
|
|
357
|
-
skipTemporal: options.skipTemporal,
|
|
358
|
-
});
|
|
359
|
-
// Apply --only-modules filter (whitelist mode)
|
|
360
|
-
if (options.onlyModules?.length) {
|
|
361
|
-
// Resolve any deprecated module names
|
|
362
|
-
const resolved = resolveModuleNames(options.onlyModules);
|
|
363
|
-
for (const key of Object.keys(allModules)) {
|
|
364
|
-
// Disable all modules except those in the whitelist
|
|
365
|
-
allModules[key] = resolved.includes(key);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Apply --skip-modules filter (blacklist mode)
|
|
369
|
-
if (options.skipModules?.length) {
|
|
370
|
-
// Resolve any deprecated module names
|
|
371
|
-
const resolved = resolveModuleNames(options.skipModules);
|
|
372
|
-
for (const module of resolved) {
|
|
373
|
-
if (module in allModules) {
|
|
374
|
-
allModules[module] = false;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
config.assessmentCategories =
|
|
379
|
-
allModules;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
// Temporal/rug pull detection configuration
|
|
383
|
-
if (options.temporalInvocations) {
|
|
384
|
-
config.temporalInvocations = options.temporalInvocations;
|
|
385
|
-
}
|
|
386
|
-
if (options.claudeEnabled) {
|
|
387
|
-
// Check for HTTP transport via --claude-http flag or environment variables
|
|
388
|
-
const useHttpTransport = options.claudeHttp || process.env.INSPECTOR_CLAUDE === "true";
|
|
389
|
-
const auditorUrl = options.mcpAuditorUrl ||
|
|
390
|
-
process.env.INSPECTOR_MCP_AUDITOR_URL ||
|
|
391
|
-
"http://localhost:8085";
|
|
392
|
-
config.claudeCode = {
|
|
393
|
-
enabled: true,
|
|
394
|
-
timeout: FULL_CLAUDE_CODE_CONFIG.timeout || 60000,
|
|
395
|
-
maxRetries: FULL_CLAUDE_CODE_CONFIG.maxRetries || 2,
|
|
396
|
-
// Use HTTP transport when --claude-http flag or INSPECTOR_CLAUDE env is set
|
|
397
|
-
...(useHttpTransport && {
|
|
398
|
-
transport: "http",
|
|
399
|
-
httpConfig: {
|
|
400
|
-
baseUrl: auditorUrl,
|
|
401
|
-
},
|
|
402
|
-
}),
|
|
403
|
-
features: {
|
|
404
|
-
intelligentTestGeneration: true,
|
|
405
|
-
aupSemanticAnalysis: true,
|
|
406
|
-
annotationInference: true,
|
|
407
|
-
documentationQuality: true,
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
if (useHttpTransport) {
|
|
411
|
-
console.log(`🔗 Claude Bridge HTTP transport: ${auditorUrl}`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// Pass custom annotation pattern config path
|
|
415
|
-
if (options.patternConfigPath) {
|
|
416
|
-
config.patternConfigPath = options.patternConfigPath;
|
|
417
|
-
}
|
|
418
|
-
// Load custom performance config if provided (Issue #37)
|
|
419
|
-
// Note: Currently, modules use DEFAULT_PERFORMANCE_CONFIG directly.
|
|
420
|
-
// This validates the config file but doesn't override runtime values yet.
|
|
421
|
-
// Future enhancement: Pass performanceConfig through AssessmentContext.
|
|
422
|
-
if (options.performanceConfigPath) {
|
|
423
|
-
try {
|
|
424
|
-
const performanceConfig = loadPerformanceConfig(options.performanceConfigPath);
|
|
425
|
-
console.log(`📊 Performance config loaded from: ${options.performanceConfigPath}`);
|
|
426
|
-
console.log(` Batch interval: ${performanceConfig.batchFlushIntervalMs}ms, ` +
|
|
427
|
-
`Security batch: ${performanceConfig.securityBatchSize}, ` +
|
|
428
|
-
`Functionality batch: ${performanceConfig.functionalityBatchSize}`);
|
|
429
|
-
// TODO: Wire performanceConfig through AssessmentContext to modules
|
|
430
|
-
}
|
|
431
|
-
catch (error) {
|
|
432
|
-
console.error(`❌ Failed to load performance config: ${error instanceof Error ? error.message : String(error)}`);
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Logging configuration
|
|
437
|
-
// Precedence: CLI flags > LOG_LEVEL env var > default (info)
|
|
438
|
-
const envLogLevel = process.env.LOG_LEVEL;
|
|
439
|
-
const logLevel = options.logLevel ?? envLogLevel ?? "info";
|
|
440
|
-
config.logging = { level: logLevel };
|
|
441
|
-
return config;
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Run full assessment
|
|
445
|
-
*/
|
|
446
|
-
async function runFullAssessment(options) {
|
|
447
|
-
if (!options.jsonOnly) {
|
|
448
|
-
console.log(`\n🔍 Starting full assessment for: ${options.serverName}`);
|
|
449
|
-
}
|
|
450
|
-
const serverConfig = loadServerConfig(options.serverName, options.serverConfigPath);
|
|
451
|
-
if (!options.jsonOnly) {
|
|
452
|
-
console.log("✅ Server config loaded");
|
|
453
|
-
}
|
|
454
|
-
const client = await connectToServer(serverConfig);
|
|
455
|
-
emitServerConnected(options.serverName, serverConfig.transport || "stdio");
|
|
456
|
-
if (!options.jsonOnly) {
|
|
457
|
-
console.log("✅ Connected to MCP server");
|
|
458
|
-
}
|
|
459
|
-
// Capture server info from initialization for protocol conformance checks
|
|
460
|
-
// Apply defensive null checks for protocol conformance validation
|
|
461
|
-
const rawServerInfo = client.getServerVersion();
|
|
462
|
-
const rawServerCapabilities = client.getServerCapabilities();
|
|
463
|
-
// Build serverInfo with safe fallbacks
|
|
464
|
-
const serverInfo = rawServerInfo
|
|
465
|
-
? {
|
|
466
|
-
name: rawServerInfo.name || "unknown",
|
|
467
|
-
version: rawServerInfo.version,
|
|
468
|
-
metadata: rawServerInfo.metadata,
|
|
469
|
-
}
|
|
470
|
-
: undefined;
|
|
471
|
-
// ServerCapabilities can be undefined - that's valid per MCP spec
|
|
472
|
-
const serverCapabilities = rawServerCapabilities ?? undefined;
|
|
473
|
-
// Log warning if server didn't provide initialization info
|
|
474
|
-
if (!serverInfo && !options.jsonOnly) {
|
|
475
|
-
console.log("⚠️ Server did not provide serverInfo during initialization");
|
|
476
|
-
}
|
|
477
|
-
const response = await client.listTools();
|
|
478
|
-
const tools = response.tools || [];
|
|
479
|
-
// Emit JSONL tool discovery events for audit-worker parsing
|
|
480
|
-
for (const tool of tools) {
|
|
481
|
-
emitToolDiscovered(tool);
|
|
482
|
-
}
|
|
483
|
-
emitToolsDiscoveryComplete(tools.length);
|
|
484
|
-
if (!options.jsonOnly) {
|
|
485
|
-
console.log(`🔧 Found ${tools.length} tool${tools.length !== 1 ? "s" : ""}`);
|
|
486
|
-
}
|
|
487
|
-
// Fetch resources for new capability assessments
|
|
488
|
-
let resources = [];
|
|
489
|
-
let resourceTemplates = [];
|
|
490
|
-
try {
|
|
491
|
-
const resourcesResponse = await client.listResources();
|
|
492
|
-
resources = (resourcesResponse.resources || []).map((r) => ({
|
|
493
|
-
uri: r.uri,
|
|
494
|
-
name: r.name,
|
|
495
|
-
description: r.description,
|
|
496
|
-
mimeType: r.mimeType,
|
|
497
|
-
}));
|
|
498
|
-
// resourceTemplates may be typed as unknown in some SDK versions
|
|
499
|
-
const templates = resourcesResponse.resourceTemplates;
|
|
500
|
-
if (templates) {
|
|
501
|
-
resourceTemplates = templates.map((rt) => ({
|
|
502
|
-
uriTemplate: rt.uriTemplate,
|
|
503
|
-
name: rt.name,
|
|
504
|
-
description: rt.description,
|
|
505
|
-
mimeType: rt.mimeType,
|
|
506
|
-
}));
|
|
507
|
-
}
|
|
508
|
-
if (!options.jsonOnly &&
|
|
509
|
-
(resources.length > 0 || resourceTemplates.length > 0)) {
|
|
510
|
-
console.log(`📦 Found ${resources.length} resource(s) and ${resourceTemplates.length} resource template(s)`);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
catch {
|
|
514
|
-
// Server may not support resources - that's okay
|
|
515
|
-
if (!options.jsonOnly) {
|
|
516
|
-
console.log("📦 Resources not supported by server");
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
// Fetch prompts for new capability assessments
|
|
520
|
-
let prompts = [];
|
|
521
|
-
try {
|
|
522
|
-
const promptsResponse = await client.listPrompts();
|
|
523
|
-
prompts = (promptsResponse.prompts || []).map((p) => ({
|
|
524
|
-
name: p.name,
|
|
525
|
-
description: p.description,
|
|
526
|
-
arguments: p.arguments?.map((a) => ({
|
|
527
|
-
name: a.name,
|
|
528
|
-
description: a.description,
|
|
529
|
-
required: a.required,
|
|
530
|
-
})),
|
|
531
|
-
}));
|
|
532
|
-
if (!options.jsonOnly && prompts.length > 0) {
|
|
533
|
-
console.log(`💬 Found ${prompts.length} prompt(s)`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
catch {
|
|
537
|
-
// Server may not support prompts - that's okay
|
|
538
|
-
if (!options.jsonOnly) {
|
|
539
|
-
console.log("💬 Prompts not supported by server");
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
// State management for resumable assessments
|
|
543
|
-
const stateManager = new AssessmentStateManager(options.serverName);
|
|
544
|
-
if (stateManager.exists() && !options.noResume) {
|
|
545
|
-
const summary = stateManager.getSummary();
|
|
546
|
-
if (summary) {
|
|
547
|
-
if (!options.jsonOnly) {
|
|
548
|
-
console.log(`\n📋 Found interrupted session from ${summary.startedAt}`);
|
|
549
|
-
console.log(` Completed modules: ${summary.completedModules.length > 0 ? summary.completedModules.join(", ") : "none"}`);
|
|
550
|
-
}
|
|
551
|
-
if (options.resume) {
|
|
552
|
-
if (!options.jsonOnly) {
|
|
553
|
-
console.log(" Resuming from previous state...");
|
|
554
|
-
}
|
|
555
|
-
// Will use partial results later
|
|
556
|
-
}
|
|
557
|
-
else if (!options.jsonOnly) {
|
|
558
|
-
console.log(" Use --resume to continue or --no-resume to start fresh");
|
|
559
|
-
// Clear state and start fresh by default
|
|
560
|
-
stateManager.clear();
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
else if (options.noResume && stateManager.exists()) {
|
|
565
|
-
stateManager.clear();
|
|
566
|
-
if (!options.jsonOnly) {
|
|
567
|
-
console.log("🗑️ Cleared previous assessment state");
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
// Pre-flight validation checks
|
|
571
|
-
if (options.preflightOnly) {
|
|
572
|
-
const preflightResult = {
|
|
573
|
-
passed: true,
|
|
574
|
-
toolCount: tools.length,
|
|
575
|
-
errors: [],
|
|
576
|
-
};
|
|
577
|
-
// Check 1: Tools exist
|
|
578
|
-
if (tools.length === 0) {
|
|
579
|
-
preflightResult.passed = false;
|
|
580
|
-
preflightResult.errors.push("No tools discovered from server");
|
|
581
|
-
}
|
|
582
|
-
// Check 2: Manifest valid (if source path provided)
|
|
583
|
-
if (options.sourceCodePath) {
|
|
584
|
-
const manifestPath = path.join(options.sourceCodePath, "manifest.json");
|
|
585
|
-
if (fs.existsSync(manifestPath)) {
|
|
586
|
-
try {
|
|
587
|
-
JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
588
|
-
preflightResult.manifestValid = true;
|
|
589
|
-
}
|
|
590
|
-
catch {
|
|
591
|
-
preflightResult.passed = false;
|
|
592
|
-
preflightResult.manifestValid = false;
|
|
593
|
-
preflightResult.errors.push("Invalid manifest.json (JSON parse error)");
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
// Check 3: First tool responds (basic connectivity)
|
|
598
|
-
if (tools.length > 0) {
|
|
599
|
-
try {
|
|
600
|
-
const callTool = createCallToolWrapper(client);
|
|
601
|
-
const firstToolResult = await callTool(tools[0].name, {});
|
|
602
|
-
preflightResult.serverResponsive = !firstToolResult.isError;
|
|
603
|
-
if (firstToolResult.isError) {
|
|
604
|
-
preflightResult.errors.push(`First tool (${tools[0].name}) returned error - server may not be fully functional`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
catch (e) {
|
|
608
|
-
preflightResult.serverResponsive = false;
|
|
609
|
-
preflightResult.errors.push(`First tool call failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
await client.close();
|
|
613
|
-
// Output pre-flight result
|
|
614
|
-
console.log(JSON.stringify(preflightResult, null, 2));
|
|
615
|
-
setTimeout(() => process.exit(preflightResult.passed ? 0 : 1), 10);
|
|
616
|
-
// Return empty result (won't be used due to process.exit)
|
|
617
|
-
return {};
|
|
618
|
-
}
|
|
619
|
-
const config = buildConfig(options);
|
|
620
|
-
// Emit modules_configured event for consumer progress tracking
|
|
621
|
-
if (config.assessmentCategories) {
|
|
622
|
-
const enabled = [];
|
|
623
|
-
const skipped = [];
|
|
624
|
-
for (const [key, value] of Object.entries(config.assessmentCategories)) {
|
|
625
|
-
if (value) {
|
|
626
|
-
enabled.push(key);
|
|
627
|
-
}
|
|
628
|
-
else {
|
|
629
|
-
skipped.push(key);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
const reason = options.onlyModules?.length
|
|
633
|
-
? "only-modules"
|
|
634
|
-
: options.skipModules?.length
|
|
635
|
-
? "skip-modules"
|
|
636
|
-
: "default";
|
|
637
|
-
emitModulesConfigured(enabled, skipped, reason);
|
|
638
|
-
}
|
|
639
|
-
const orchestrator = new AssessmentOrchestrator(config);
|
|
640
|
-
if (!options.jsonOnly) {
|
|
641
|
-
if (orchestrator.isClaudeEnabled()) {
|
|
642
|
-
console.log("🤖 Claude Code integration enabled");
|
|
643
|
-
}
|
|
644
|
-
else if (options.claudeEnabled) {
|
|
645
|
-
console.log("⚠️ Claude Code requested but not available");
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
let sourceFiles = {};
|
|
649
|
-
if (options.sourceCodePath && fs.existsSync(options.sourceCodePath)) {
|
|
650
|
-
sourceFiles = loadSourceFiles(options.sourceCodePath);
|
|
651
|
-
if (!options.jsonOnly) {
|
|
652
|
-
console.log(`📁 Loaded source files from: ${options.sourceCodePath}`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
// Create readResource wrapper for ResourceAssessor
|
|
656
|
-
const readResource = async (uri) => {
|
|
657
|
-
const response = await client.readResource({ uri });
|
|
658
|
-
// Extract text content from response
|
|
659
|
-
if (response.contents && response.contents.length > 0) {
|
|
660
|
-
const content = response.contents[0];
|
|
661
|
-
if ("text" in content && content.text) {
|
|
662
|
-
return content.text;
|
|
663
|
-
}
|
|
664
|
-
if ("blob" in content && content.blob) {
|
|
665
|
-
// Return base64 blob as string
|
|
666
|
-
return content.blob;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return "";
|
|
670
|
-
};
|
|
671
|
-
// Create getPrompt wrapper for PromptAssessor
|
|
672
|
-
const getPrompt = async (name, args) => {
|
|
673
|
-
const response = await client.getPrompt({ name, arguments: args });
|
|
674
|
-
return {
|
|
675
|
-
messages: (response.messages || []).map((m) => ({
|
|
676
|
-
role: m.role,
|
|
677
|
-
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
|
678
|
-
})),
|
|
679
|
-
};
|
|
680
|
-
};
|
|
681
|
-
// Progress callback to emit JSONL events for real-time monitoring
|
|
682
|
-
const onProgress = (event) => {
|
|
683
|
-
if (event.type === "test_batch") {
|
|
684
|
-
emitTestBatch(event.module, event.completed, event.total, event.batchSize, event.elapsed);
|
|
685
|
-
}
|
|
686
|
-
else if (event.type === "vulnerability_found") {
|
|
687
|
-
emitVulnerabilityFound(event.tool, event.pattern, event.confidence, event.evidence, event.riskLevel, event.requiresReview, event.payload);
|
|
688
|
-
}
|
|
689
|
-
else if (event.type === "annotation_missing") {
|
|
690
|
-
emitAnnotationMissing(event.tool, event.title, event.description, event.parameters, event.inferredBehavior);
|
|
691
|
-
}
|
|
692
|
-
else if (event.type === "annotation_misaligned") {
|
|
693
|
-
emitAnnotationMisaligned(event.tool, event.title, event.description, event.parameters, event.field, event.actual, event.expected, event.confidence, event.reason);
|
|
694
|
-
}
|
|
695
|
-
else if (event.type === "annotation_review_recommended") {
|
|
696
|
-
emitAnnotationReviewRecommended(event.tool, event.title, event.description, event.parameters, event.field, event.actual, event.inferred, event.confidence, event.isAmbiguous, event.reason);
|
|
697
|
-
}
|
|
698
|
-
else if (event.type === "annotation_aligned") {
|
|
699
|
-
emitAnnotationAligned(event.tool, event.confidence, event.annotations);
|
|
700
|
-
}
|
|
701
|
-
// module_started and module_complete are handled by orchestrator directly
|
|
702
|
-
};
|
|
703
|
-
const context = {
|
|
704
|
-
serverName: options.serverName,
|
|
705
|
-
tools,
|
|
706
|
-
callTool: createCallToolWrapper(client),
|
|
707
|
-
listTools: async () => {
|
|
708
|
-
const response = await client.listTools();
|
|
709
|
-
return response.tools;
|
|
710
|
-
},
|
|
711
|
-
config,
|
|
712
|
-
sourceCodePath: options.sourceCodePath,
|
|
713
|
-
onProgress,
|
|
714
|
-
...sourceFiles,
|
|
715
|
-
// New capability assessment data
|
|
716
|
-
resources,
|
|
717
|
-
resourceTemplates,
|
|
718
|
-
prompts,
|
|
719
|
-
readResource,
|
|
720
|
-
getPrompt,
|
|
721
|
-
// Server info for protocol conformance checks
|
|
722
|
-
serverInfo,
|
|
723
|
-
serverCapabilities: serverCapabilities,
|
|
724
|
-
};
|
|
725
|
-
if (!options.jsonOnly) {
|
|
726
|
-
console.log(`\n🏃 Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
|
|
727
|
-
console.log("");
|
|
728
|
-
}
|
|
729
|
-
const results = await orchestrator.runFullAssessment(context);
|
|
730
|
-
// Emit assessment complete event
|
|
731
|
-
const defaultOutputPath = `/tmp/inspector-full-assessment-${options.serverName}.json`;
|
|
732
|
-
emitAssessmentComplete(results.overallStatus, results.totalTestsRun, results.executionTime, options.outputPath || defaultOutputPath);
|
|
733
|
-
await client.close();
|
|
734
|
-
return results;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Save results to file with appropriate format
|
|
738
|
-
*/
|
|
739
|
-
function saveResults(serverName, results, options) {
|
|
740
|
-
const format = options.format || "json";
|
|
741
|
-
// Generate policy compliance report if requested
|
|
742
|
-
const policyReport = options.includePolicy
|
|
743
|
-
? generatePolicyComplianceReport(results, serverName)
|
|
744
|
-
: undefined;
|
|
745
|
-
// Create formatter with options
|
|
746
|
-
const formatter = createFormatter({
|
|
747
|
-
format,
|
|
748
|
-
includePolicyMapping: options.includePolicy,
|
|
749
|
-
policyReport,
|
|
750
|
-
serverName,
|
|
751
|
-
includeDetails: true,
|
|
752
|
-
prettyPrint: true,
|
|
753
|
-
});
|
|
754
|
-
const fileExtension = formatter.getFileExtension();
|
|
755
|
-
const defaultPath = `/tmp/inspector-full-assessment-${serverName}${fileExtension}`;
|
|
756
|
-
const finalPath = options.outputPath || defaultPath;
|
|
757
|
-
// For JSON format, add metadata wrapper
|
|
758
|
-
if (format === "json") {
|
|
759
|
-
// Filter out undefined/skipped modules from results (--skip-modules support)
|
|
760
|
-
const filteredResults = Object.fromEntries(Object.entries(results).filter(([_, v]) => v !== undefined));
|
|
761
|
-
const output = {
|
|
762
|
-
timestamp: new Date().toISOString(),
|
|
763
|
-
assessmentType: "full",
|
|
764
|
-
...filteredResults,
|
|
765
|
-
...(policyReport ? { policyCompliance: policyReport } : {}),
|
|
766
|
-
};
|
|
767
|
-
fs.writeFileSync(finalPath, JSON.stringify(output, null, 2));
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
// For other formats (markdown), use the formatter
|
|
771
|
-
const content = formatter.format(results);
|
|
772
|
-
fs.writeFileSync(finalPath, content);
|
|
773
|
-
}
|
|
774
|
-
return finalPath;
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Display summary
|
|
778
|
-
*/
|
|
779
|
-
function displaySummary(results) {
|
|
780
|
-
const { overallStatus, summary, totalTestsRun, executionTime,
|
|
781
|
-
// Destructuring order matches display order below
|
|
782
|
-
functionality, security, documentation, errorHandling, usability, mcpSpecCompliance, aupCompliance, toolAnnotations, prohibitedLibraries, manifestValidation, portability, externalAPIScanner, authentication, temporal, resources, prompts, crossCapability, } = results;
|
|
783
|
-
console.log("\n" + "=".repeat(70));
|
|
784
|
-
console.log("FULL ASSESSMENT RESULTS");
|
|
785
|
-
console.log("=".repeat(70));
|
|
786
|
-
console.log(`Server: ${results.serverName}`);
|
|
787
|
-
console.log(`Overall Status: ${overallStatus}`);
|
|
788
|
-
console.log(`Total Tests Run: ${totalTestsRun}`);
|
|
789
|
-
console.log(`Execution Time: ${executionTime}ms`);
|
|
790
|
-
console.log("-".repeat(70));
|
|
791
|
-
console.log("\n📊 MODULE STATUS:");
|
|
792
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
793
|
-
const modules = [
|
|
794
|
-
["Functionality", functionality, "functionality"],
|
|
795
|
-
["Security", security, "security"],
|
|
796
|
-
["Documentation", documentation, "documentation"],
|
|
797
|
-
["Error Handling", errorHandling, "errorHandling"],
|
|
798
|
-
["Usability", usability, "usability"],
|
|
799
|
-
["MCP Spec Compliance", mcpSpecCompliance, "mcpSpecCompliance"],
|
|
800
|
-
["AUP Compliance", aupCompliance, "aupCompliance"],
|
|
801
|
-
["Tool Annotations", toolAnnotations, "toolAnnotations"],
|
|
802
|
-
["Prohibited Libraries", prohibitedLibraries, "prohibitedLibraries"],
|
|
803
|
-
["Manifest Validation", manifestValidation, "manifestValidation"],
|
|
804
|
-
["Portability", portability, "portability"],
|
|
805
|
-
["External API Scanner", externalAPIScanner, "externalAPIScanner"],
|
|
806
|
-
["Authentication", authentication, "authentication"],
|
|
807
|
-
["Temporal", temporal, "temporal"],
|
|
808
|
-
["Resources", resources, "resources"],
|
|
809
|
-
["Prompts", prompts, "prompts"],
|
|
810
|
-
["Cross-Capability", crossCapability, "crossCapability"],
|
|
811
|
-
];
|
|
812
|
-
for (const [name, module, categoryKey] of modules) {
|
|
813
|
-
if (module) {
|
|
814
|
-
const metadata = ASSESSMENT_CATEGORY_METADATA[categoryKey];
|
|
815
|
-
const optionalMarker = metadata?.tier === "optional" ? " (optional)" : "";
|
|
816
|
-
const icon = module.status === "PASS"
|
|
817
|
-
? "✅"
|
|
818
|
-
: module.status === "FAIL"
|
|
819
|
-
? "❌"
|
|
820
|
-
: "⚠️";
|
|
821
|
-
console.log(` ${icon} ${name}${optionalMarker}: ${module.status}`);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
console.log("\n📋 KEY FINDINGS:");
|
|
825
|
-
console.log(` ${summary}`);
|
|
826
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
827
|
-
const securityModule = security;
|
|
828
|
-
if (securityModule?.vulnerabilities?.length > 0) {
|
|
829
|
-
const vulns = securityModule.vulnerabilities;
|
|
830
|
-
console.log(`\n🔒 SECURITY VULNERABILITIES (${vulns.length}):`);
|
|
831
|
-
for (const vuln of vulns.slice(0, 5)) {
|
|
832
|
-
console.log(` • ${vuln}`);
|
|
833
|
-
}
|
|
834
|
-
if (vulns.length > 5) {
|
|
835
|
-
console.log(` ... and ${vulns.length - 5} more`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
839
|
-
const aupModule = aupCompliance;
|
|
840
|
-
if (aupModule?.violations?.length > 0) {
|
|
841
|
-
const violations = aupModule.violations;
|
|
842
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
843
|
-
const critical = violations.filter((v) => v.severity === "CRITICAL");
|
|
844
|
-
console.log(`\n⚖️ AUP FINDINGS:`);
|
|
845
|
-
console.log(` Total flagged: ${violations.length}`);
|
|
846
|
-
if (critical.length > 0) {
|
|
847
|
-
console.log(` 🚨 CRITICAL violations: ${critical.length}`);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
851
|
-
const annotationsModule = toolAnnotations;
|
|
852
|
-
if (annotationsModule) {
|
|
853
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
854
|
-
const funcModule = functionality;
|
|
855
|
-
console.log(`\n🏷️ TOOL ANNOTATIONS:`);
|
|
856
|
-
console.log(` Annotated: ${annotationsModule.annotatedCount || 0}/${funcModule?.workingTools || 0}`);
|
|
857
|
-
if (annotationsModule.missingAnnotationsCount > 0) {
|
|
858
|
-
console.log(` Missing: ${annotationsModule.missingAnnotationsCount}`);
|
|
859
|
-
}
|
|
860
|
-
if (annotationsModule.misalignedAnnotationsCount > 0) {
|
|
861
|
-
console.log(` ⚠️ Misalignments: ${annotationsModule.misalignedAnnotationsCount}`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
if (results.recommendations?.length > 0) {
|
|
865
|
-
console.log("\n💡 RECOMMENDATIONS:");
|
|
866
|
-
for (const rec of results.recommendations.slice(0, 5)) {
|
|
867
|
-
console.log(` • ${rec}`);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
console.log("\n" + "=".repeat(70));
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* Parse command-line arguments
|
|
874
|
-
*/
|
|
875
|
-
function parseArgs() {
|
|
876
|
-
const args = process.argv.slice(2);
|
|
877
|
-
const options = {};
|
|
878
|
-
for (let i = 0; i < args.length; i++) {
|
|
879
|
-
const arg = args[i];
|
|
880
|
-
if (!arg)
|
|
881
|
-
continue;
|
|
882
|
-
switch (arg) {
|
|
883
|
-
case "--server":
|
|
884
|
-
case "-s":
|
|
885
|
-
options.serverName = args[++i];
|
|
886
|
-
break;
|
|
887
|
-
case "--config":
|
|
888
|
-
case "-c":
|
|
889
|
-
options.serverConfigPath = args[++i];
|
|
890
|
-
break;
|
|
891
|
-
case "--output":
|
|
892
|
-
case "-o":
|
|
893
|
-
options.outputPath = args[++i];
|
|
894
|
-
break;
|
|
895
|
-
case "--source":
|
|
896
|
-
options.sourceCodePath = args[++i];
|
|
897
|
-
break;
|
|
898
|
-
case "--pattern-config":
|
|
899
|
-
case "-p":
|
|
900
|
-
options.patternConfigPath = args[++i];
|
|
901
|
-
break;
|
|
902
|
-
case "--performance-config":
|
|
903
|
-
options.performanceConfigPath = args[++i];
|
|
904
|
-
break;
|
|
905
|
-
case "--claude-enabled":
|
|
906
|
-
options.claudeEnabled = true;
|
|
907
|
-
break;
|
|
908
|
-
case "--claude-http":
|
|
909
|
-
// Enable Claude Bridge with HTTP transport (connects to mcp-auditor)
|
|
910
|
-
options.claudeEnabled = true;
|
|
911
|
-
options.claudeHttp = true;
|
|
912
|
-
break;
|
|
913
|
-
case "--mcp-auditor-url": {
|
|
914
|
-
const urlValue = args[++i];
|
|
915
|
-
if (!urlValue || urlValue.startsWith("-")) {
|
|
916
|
-
console.error("Error: --mcp-auditor-url requires a URL argument");
|
|
917
|
-
setTimeout(() => process.exit(1), 10);
|
|
918
|
-
options.helpRequested = true;
|
|
919
|
-
return options;
|
|
920
|
-
}
|
|
921
|
-
try {
|
|
922
|
-
new URL(urlValue); // Validate URL format
|
|
923
|
-
options.mcpAuditorUrl = urlValue;
|
|
924
|
-
}
|
|
925
|
-
catch {
|
|
926
|
-
console.error(`Error: Invalid URL for --mcp-auditor-url: ${urlValue}`);
|
|
927
|
-
console.error(" Expected format: http://hostname:port or https://hostname:port");
|
|
928
|
-
setTimeout(() => process.exit(1), 10);
|
|
929
|
-
options.helpRequested = true;
|
|
930
|
-
return options;
|
|
931
|
-
}
|
|
932
|
-
break;
|
|
933
|
-
}
|
|
934
|
-
case "--full":
|
|
935
|
-
options.fullAssessment = true;
|
|
936
|
-
break;
|
|
937
|
-
case "--verbose":
|
|
938
|
-
case "-v":
|
|
939
|
-
options.verbose = true;
|
|
940
|
-
options.logLevel = "debug";
|
|
941
|
-
break;
|
|
942
|
-
case "--silent":
|
|
943
|
-
options.logLevel = "silent";
|
|
944
|
-
break;
|
|
945
|
-
case "--log-level": {
|
|
946
|
-
const levelValue = args[++i];
|
|
947
|
-
const validLevels = [
|
|
948
|
-
"silent",
|
|
949
|
-
"error",
|
|
950
|
-
"warn",
|
|
951
|
-
"info",
|
|
952
|
-
"debug",
|
|
953
|
-
];
|
|
954
|
-
if (!validLevels.includes(levelValue)) {
|
|
955
|
-
console.error(`Invalid log level: ${levelValue}. Valid options: ${validLevels.join(", ")}`);
|
|
956
|
-
setTimeout(() => process.exit(1), 10);
|
|
957
|
-
options.helpRequested = true;
|
|
958
|
-
return options;
|
|
959
|
-
}
|
|
960
|
-
options.logLevel = levelValue;
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
case "--json":
|
|
964
|
-
options.jsonOnly = true;
|
|
965
|
-
break;
|
|
966
|
-
case "--format":
|
|
967
|
-
case "-f":
|
|
968
|
-
const formatValue = args[++i];
|
|
969
|
-
if (formatValue !== "json" && formatValue !== "markdown") {
|
|
970
|
-
console.error(`Invalid format: ${formatValue}. Valid options: json, markdown`);
|
|
971
|
-
setTimeout(() => process.exit(1), 10);
|
|
972
|
-
options.helpRequested = true;
|
|
973
|
-
return options;
|
|
974
|
-
}
|
|
975
|
-
options.format = formatValue;
|
|
976
|
-
break;
|
|
977
|
-
case "--include-policy":
|
|
978
|
-
options.includePolicy = true;
|
|
979
|
-
break;
|
|
980
|
-
case "--preflight":
|
|
981
|
-
options.preflightOnly = true;
|
|
982
|
-
break;
|
|
983
|
-
case "--compare":
|
|
984
|
-
options.comparePath = args[++i];
|
|
985
|
-
break;
|
|
986
|
-
case "--diff-only":
|
|
987
|
-
options.diffOnly = true;
|
|
988
|
-
break;
|
|
989
|
-
case "--resume":
|
|
990
|
-
options.resume = true;
|
|
991
|
-
break;
|
|
992
|
-
case "--no-resume":
|
|
993
|
-
options.noResume = true;
|
|
994
|
-
break;
|
|
995
|
-
case "--temporal-invocations":
|
|
996
|
-
options.temporalInvocations = parseInt(args[++i], 10);
|
|
997
|
-
break;
|
|
998
|
-
case "--skip-temporal":
|
|
999
|
-
options.skipTemporal = true;
|
|
1000
|
-
break;
|
|
1001
|
-
case "--profile": {
|
|
1002
|
-
const profileValue = args[++i];
|
|
1003
|
-
if (!profileValue) {
|
|
1004
|
-
console.error("Error: --profile requires a profile name");
|
|
1005
|
-
console.error(`Valid profiles: ${Object.keys(ASSESSMENT_PROFILES).join(", ")}`);
|
|
1006
|
-
setTimeout(() => process.exit(1), 10);
|
|
1007
|
-
options.helpRequested = true;
|
|
1008
|
-
return options;
|
|
1009
|
-
}
|
|
1010
|
-
if (!isValidProfileName(profileValue)) {
|
|
1011
|
-
console.error(`Error: Invalid profile name: ${profileValue}`);
|
|
1012
|
-
console.error(`Valid profiles: ${Object.keys(ASSESSMENT_PROFILES).join(", ")}`);
|
|
1013
|
-
setTimeout(() => process.exit(1), 10);
|
|
1014
|
-
options.helpRequested = true;
|
|
1015
|
-
return options;
|
|
1016
|
-
}
|
|
1017
|
-
options.profile = profileValue;
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
case "--skip-modules": {
|
|
1021
|
-
const skipValue = args[++i];
|
|
1022
|
-
if (!skipValue) {
|
|
1023
|
-
console.error("Error: --skip-modules requires a comma-separated list");
|
|
1024
|
-
setTimeout(() => process.exit(1), 10);
|
|
1025
|
-
options.helpRequested = true;
|
|
1026
|
-
return options;
|
|
1027
|
-
}
|
|
1028
|
-
options.skipModules = validateModuleNames(skipValue, "--skip-modules");
|
|
1029
|
-
if (options.skipModules.length === 0 && skipValue) {
|
|
1030
|
-
options.helpRequested = true;
|
|
1031
|
-
return options;
|
|
1032
|
-
}
|
|
1033
|
-
break;
|
|
1034
|
-
}
|
|
1035
|
-
case "--only-modules": {
|
|
1036
|
-
const onlyValue = args[++i];
|
|
1037
|
-
if (!onlyValue) {
|
|
1038
|
-
console.error("Error: --only-modules requires a comma-separated list");
|
|
1039
|
-
setTimeout(() => process.exit(1), 10);
|
|
1040
|
-
options.helpRequested = true;
|
|
1041
|
-
return options;
|
|
1042
|
-
}
|
|
1043
|
-
options.onlyModules = validateModuleNames(onlyValue, "--only-modules");
|
|
1044
|
-
if (options.onlyModules.length === 0 && onlyValue) {
|
|
1045
|
-
options.helpRequested = true;
|
|
1046
|
-
return options;
|
|
1047
|
-
}
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
case "--help":
|
|
1051
|
-
case "-h":
|
|
1052
|
-
printHelp();
|
|
1053
|
-
options.helpRequested = true;
|
|
1054
|
-
return options;
|
|
1055
|
-
default:
|
|
1056
|
-
if (!arg.startsWith("-")) {
|
|
1057
|
-
if (!options.serverName) {
|
|
1058
|
-
options.serverName = arg;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
console.error(`Unknown argument: ${arg}`);
|
|
1063
|
-
printHelp();
|
|
1064
|
-
setTimeout(() => process.exit(1), 10);
|
|
1065
|
-
options.helpRequested = true;
|
|
1066
|
-
return options;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
// Validate mutual exclusivity of --profile, --skip-modules, and --only-modules
|
|
1071
|
-
if (options.profile &&
|
|
1072
|
-
(options.skipModules?.length || options.onlyModules?.length)) {
|
|
1073
|
-
console.error("Error: --profile cannot be used with --skip-modules or --only-modules");
|
|
1074
|
-
setTimeout(() => process.exit(1), 10);
|
|
1075
|
-
options.helpRequested = true;
|
|
1076
|
-
return options;
|
|
1077
|
-
}
|
|
1078
|
-
if (options.skipModules?.length && options.onlyModules?.length) {
|
|
1079
|
-
console.error("Error: --skip-modules and --only-modules are mutually exclusive");
|
|
1080
|
-
setTimeout(() => process.exit(1), 10);
|
|
1081
|
-
options.helpRequested = true;
|
|
1082
|
-
return options;
|
|
1083
|
-
}
|
|
1084
|
-
if (!options.serverName) {
|
|
1085
|
-
console.error("Error: --server is required");
|
|
1086
|
-
printHelp();
|
|
1087
|
-
setTimeout(() => process.exit(1), 10);
|
|
1088
|
-
options.helpRequested = true;
|
|
1089
|
-
return options;
|
|
1090
|
-
}
|
|
1091
|
-
// Environment variable fallbacks (matches run-security-assessment.ts behavior)
|
|
1092
|
-
// INSPECTOR_CLAUDE=true enables Claude with HTTP transport
|
|
1093
|
-
if (process.env.INSPECTOR_CLAUDE === "true" && !options.claudeEnabled) {
|
|
1094
|
-
options.claudeEnabled = true;
|
|
1095
|
-
options.claudeHttp = true; // HTTP transport when enabled via env var
|
|
1096
|
-
}
|
|
1097
|
-
// INSPECTOR_MCP_AUDITOR_URL overrides default URL (only if not set via CLI)
|
|
1098
|
-
if (process.env.INSPECTOR_MCP_AUDITOR_URL && !options.mcpAuditorUrl) {
|
|
1099
|
-
const envUrl = process.env.INSPECTOR_MCP_AUDITOR_URL;
|
|
1100
|
-
try {
|
|
1101
|
-
new URL(envUrl);
|
|
1102
|
-
options.mcpAuditorUrl = envUrl;
|
|
1103
|
-
}
|
|
1104
|
-
catch {
|
|
1105
|
-
console.warn(`Warning: Invalid INSPECTOR_MCP_AUDITOR_URL: ${envUrl}, using default`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return options;
|
|
1109
|
-
}
|
|
1110
|
-
/**
|
|
1111
|
-
* Print help message
|
|
1112
|
-
*/
|
|
1113
|
-
function printHelp() {
|
|
1114
|
-
console.log(`
|
|
1115
|
-
Usage: mcp-assess-full [options] [server-name]
|
|
1116
|
-
|
|
1117
|
-
Run comprehensive MCP server assessment with 16 assessor modules organized in 4 tiers.
|
|
1118
|
-
|
|
1119
|
-
Options:
|
|
1120
|
-
--server, -s <name> Server name (required, or pass as first positional arg)
|
|
1121
|
-
--config, -c <path> Path to server config JSON
|
|
1122
|
-
--output, -o <path> Output path (default: /tmp/inspector-full-assessment-<server>.<ext>)
|
|
1123
|
-
--source <path> Source code path for deep analysis (AUP, portability, etc.)
|
|
1124
|
-
--pattern-config, -p <path> Path to custom annotation pattern JSON
|
|
1125
|
-
--performance-config <path> Path to performance tuning JSON (batch sizes, timeouts, etc.)
|
|
1126
|
-
--format, -f <type> Output format: json (default) or markdown
|
|
1127
|
-
--include-policy Include policy compliance mapping in report (30 requirements)
|
|
1128
|
-
--preflight Run quick validation only (tools exist, manifest valid, server responds)
|
|
1129
|
-
--compare <path> Compare current assessment against baseline JSON file
|
|
1130
|
-
--diff-only Output only the comparison diff (requires --compare)
|
|
1131
|
-
--resume Resume from previous interrupted assessment
|
|
1132
|
-
--no-resume Force fresh start, clear any existing state
|
|
1133
|
-
--claude-enabled Enable Claude Code integration (CLI transport: requires 'claude' binary)
|
|
1134
|
-
--claude-http Enable Claude Code via HTTP transport (connects to mcp-auditor proxy)
|
|
1135
|
-
--mcp-auditor-url <url> mcp-auditor URL for HTTP transport (default: http://localhost:8085)
|
|
1136
|
-
--full Enable all assessment modules (default)
|
|
1137
|
-
--profile <name> Use predefined module profile (quick, security, compliance, full)
|
|
1138
|
-
--temporal-invocations <n> Number of invocations per tool for rug pull detection (default: 25)
|
|
1139
|
-
--skip-temporal Skip temporal/rug pull testing (faster assessment)
|
|
1140
|
-
--skip-modules <list> Skip specific modules (comma-separated)
|
|
1141
|
-
--only-modules <list> Run only specific modules (comma-separated)
|
|
1142
|
-
--json Output only JSON path (no console summary)
|
|
1143
|
-
--verbose, -v Enable verbose logging (same as --log-level debug)
|
|
1144
|
-
--silent Suppress all diagnostic logging
|
|
1145
|
-
--log-level <level> Set log level: silent, error, warn, info (default), debug
|
|
1146
|
-
Also supports LOG_LEVEL environment variable
|
|
1147
|
-
--help, -h Show this help message
|
|
1148
|
-
|
|
1149
|
-
Environment Variables:
|
|
1150
|
-
INSPECTOR_CLAUDE=true Enable Claude with HTTP transport (same as --claude-http)
|
|
1151
|
-
INSPECTOR_MCP_AUDITOR_URL Override default mcp-auditor URL (default: http://localhost:8085)
|
|
1152
|
-
LOG_LEVEL Set log level (overridden by --log-level flag)
|
|
1153
|
-
|
|
1154
|
-
${getProfileHelpText()}
|
|
1155
|
-
Module Selection:
|
|
1156
|
-
--profile, --skip-modules, and --only-modules are mutually exclusive.
|
|
1157
|
-
Use --profile for common assessment scenarios.
|
|
1158
|
-
Use --skip-modules for custom runs by disabling expensive modules.
|
|
1159
|
-
Use --only-modules to focus on specific areas (e.g., tool annotation PRs).
|
|
1160
|
-
|
|
1161
|
-
Valid module names (new naming):
|
|
1162
|
-
functionality, security, errorHandling, protocolCompliance, aupCompliance,
|
|
1163
|
-
toolAnnotations, prohibitedLibraries, manifestValidation, authentication,
|
|
1164
|
-
temporal, resources, prompts, crossCapability, developerExperience,
|
|
1165
|
-
portability, externalAPIScanner
|
|
1166
|
-
|
|
1167
|
-
Legacy module names (deprecated, will map to new names):
|
|
1168
|
-
documentation -> developerExperience
|
|
1169
|
-
usability -> developerExperience
|
|
1170
|
-
mcpSpecCompliance -> protocolCompliance
|
|
1171
|
-
protocolConformance -> protocolCompliance
|
|
1172
|
-
|
|
1173
|
-
Module Tiers (16 total):
|
|
1174
|
-
Tier 1 - Core Security (Always Run):
|
|
1175
|
-
• Functionality - Tests all tools work correctly
|
|
1176
|
-
• Security - Prompt injection & vulnerability testing
|
|
1177
|
-
• Error Handling - Validates error responses
|
|
1178
|
-
• Protocol Compliance - MCP protocol + JSON-RPC validation
|
|
1179
|
-
• AUP Compliance - Acceptable Use Policy checks
|
|
1180
|
-
• Temporal - Rug pull/temporal behavior change detection
|
|
1181
|
-
|
|
1182
|
-
Tier 2 - Compliance (MCP Directory):
|
|
1183
|
-
• Tool Annotations - readOnlyHint/destructiveHint validation
|
|
1184
|
-
• Prohibited Libs - Dependency security checks
|
|
1185
|
-
• Manifest - MCPB manifest.json validation
|
|
1186
|
-
• Authentication - OAuth/auth evaluation
|
|
1187
|
-
|
|
1188
|
-
Tier 3 - Capability-Based (Conditional):
|
|
1189
|
-
• Resources - Resource capability assessment
|
|
1190
|
-
• Prompts - Prompt capability assessment
|
|
1191
|
-
• Cross-Capability - Chained vulnerability detection
|
|
1192
|
-
|
|
1193
|
-
Tier 4 - Extended (Optional):
|
|
1194
|
-
• Developer Experience - Documentation + usability assessment
|
|
1195
|
-
• Portability - Cross-platform compatibility
|
|
1196
|
-
• External API - External service detection
|
|
1197
|
-
|
|
1198
|
-
Examples:
|
|
1199
|
-
# Profile-based (recommended):
|
|
1200
|
-
mcp-assess-full my-server --profile quick # CI/CD fast check (~30s)
|
|
1201
|
-
mcp-assess-full my-server --profile security # Security audit (~2-3min)
|
|
1202
|
-
mcp-assess-full my-server --profile compliance # Directory submission (~5min)
|
|
1203
|
-
mcp-assess-full my-server --profile full # Comprehensive audit (~10-15min)
|
|
1204
|
-
|
|
1205
|
-
# Custom module selection:
|
|
1206
|
-
mcp-assess-full my-server --skip-modules temporal,resources # Skip expensive modules
|
|
1207
|
-
mcp-assess-full my-server --only-modules functionality,toolAnnotations # Annotation PR review
|
|
1208
|
-
|
|
1209
|
-
# Advanced options:
|
|
1210
|
-
mcp-assess-full --server my-server --source ./my-server --output ./results.json
|
|
1211
|
-
mcp-assess-full --server my-server --format markdown --include-policy
|
|
1212
|
-
mcp-assess-full --server my-server --compare ./baseline.json --diff-only
|
|
1213
|
-
`);
|
|
1214
|
-
}
|
|
13
|
+
// Import from extracted modules
|
|
14
|
+
import { parseArgs } from "./lib/cli-parser.js";
|
|
15
|
+
import { runFullAssessment } from "./lib/assessment-runner.js";
|
|
16
|
+
import { saveResults, displaySummary } from "./lib/result-output.js";
|
|
17
|
+
import { handleComparison, displayComparisonSummary, } from "./lib/comparison-handler.js";
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Main Entry Point
|
|
20
|
+
// ============================================================================
|
|
1215
21
|
/**
|
|
1216
22
|
* Main execution
|
|
1217
23
|
*/
|
|
@@ -1232,62 +38,27 @@ async function main() {
|
|
|
1232
38
|
return;
|
|
1233
39
|
}
|
|
1234
40
|
// Handle comparison mode
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
`/tmp/inspector-diff-${options.serverName}.md`;
|
|
1251
|
-
fs.writeFileSync(diffPath, formatDiffAsMarkdown(diff));
|
|
1252
|
-
console.log(diffPath);
|
|
1253
|
-
}
|
|
1254
|
-
else {
|
|
1255
|
-
const diffPath = options.outputPath ||
|
|
1256
|
-
`/tmp/inspector-diff-${options.serverName}.json`;
|
|
1257
|
-
fs.writeFileSync(diffPath, JSON.stringify(diff, null, 2));
|
|
1258
|
-
console.log(diffPath);
|
|
1259
|
-
}
|
|
1260
|
-
const exitCode = diff.summary.overallChange === "regressed" ? 1 : 0;
|
|
1261
|
-
setTimeout(() => process.exit(exitCode), 10);
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
// Include diff in output alongside full assessment
|
|
1265
|
-
if (!options.jsonOnly) {
|
|
1266
|
-
console.log("\n" + "=".repeat(70));
|
|
1267
|
-
console.log("VERSION COMPARISON");
|
|
1268
|
-
console.log("=".repeat(70));
|
|
1269
|
-
console.log(`Baseline: ${diff.baseline.version || "N/A"} (${diff.baseline.date})`);
|
|
1270
|
-
console.log(`Current: ${diff.current.version || "N/A"} (${diff.current.date})`);
|
|
1271
|
-
console.log(`Overall Change: ${diff.summary.overallChange.toUpperCase()}`);
|
|
1272
|
-
console.log(`Modules Improved: ${diff.summary.modulesImproved}`);
|
|
1273
|
-
console.log(`Modules Regressed: ${diff.summary.modulesRegressed}`);
|
|
1274
|
-
if (diff.securityDelta.newVulnerabilities.length > 0) {
|
|
1275
|
-
console.log(`\n⚠️ NEW VULNERABILITIES: ${diff.securityDelta.newVulnerabilities.length}`);
|
|
1276
|
-
}
|
|
1277
|
-
if (diff.securityDelta.fixedVulnerabilities.length > 0) {
|
|
1278
|
-
console.log(`✅ FIXED VULNERABILITIES: ${diff.securityDelta.fixedVulnerabilities.length}`);
|
|
1279
|
-
}
|
|
1280
|
-
if (diff.functionalityDelta.newBrokenTools.length > 0) {
|
|
1281
|
-
console.log(`❌ NEW BROKEN TOOLS: ${diff.functionalityDelta.newBrokenTools.length}`);
|
|
1282
|
-
}
|
|
1283
|
-
if (diff.functionalityDelta.fixedTools.length > 0) {
|
|
1284
|
-
console.log(`✅ FIXED TOOLS: ${diff.functionalityDelta.fixedTools.length}`);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
41
|
+
const comparison = handleComparison(results, options);
|
|
42
|
+
// If comparison was requested but returned null, baseline file was not found
|
|
43
|
+
if (options.comparePath && !comparison) {
|
|
44
|
+
setTimeout(() => process.exit(1), 10);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (comparison?.diffOutputPath) {
|
|
48
|
+
// Diff-only mode: output path and exit
|
|
49
|
+
console.log(comparison.diffOutputPath);
|
|
50
|
+
setTimeout(() => process.exit(comparison.exitCode), 10);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Display comparison summary if in comparison mode (not diff-only)
|
|
54
|
+
if (comparison && !options.jsonOnly) {
|
|
55
|
+
displayComparisonSummary(comparison.diff);
|
|
1287
56
|
}
|
|
57
|
+
// Display results summary
|
|
1288
58
|
if (!options.jsonOnly) {
|
|
1289
59
|
displaySummary(results);
|
|
1290
60
|
}
|
|
61
|
+
// Save results to file
|
|
1291
62
|
const outputPath = saveResults(options.serverName, results, options);
|
|
1292
63
|
if (options.jsonOnly) {
|
|
1293
64
|
console.log(outputPath);
|
|
@@ -1295,6 +66,7 @@ async function main() {
|
|
|
1295
66
|
else {
|
|
1296
67
|
console.log(`📄 Results saved to: ${outputPath}\n`);
|
|
1297
68
|
}
|
|
69
|
+
// Exit with appropriate code
|
|
1298
70
|
const exitCode = results.overallStatus === "FAIL" ? 1 : 0;
|
|
1299
71
|
setTimeout(() => process.exit(exitCode), 10);
|
|
1300
72
|
}
|