@mcp-guardian/cli 4.1.2
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/.turbo/turbo-build.log +9 -0
- package/.turbo/turbo-test.log +49 -0
- package/README.md +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +140 -0
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +430 -0
- package/dist/tui/data-fetcher.d.ts +144 -0
- package/dist/tui/data-fetcher.js +268 -0
- package/package.json +20 -0
- package/package.json.prepack-backup +28 -0
- package/src/index.ts +156 -0
- package/tests/index.test.ts +122 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +7 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { scanServer, verifyToolDefinitions } from "@mcp-guardian/core";
|
|
7
|
+
import { fetchToolsFromStdio } from "@mcp-guardian/core";
|
|
8
|
+
import { fetchToolsFromHttp, fetchToolsFromSse } from "@mcp-guardian/core";
|
|
9
|
+
const { values: flags, positionals } = parseArgs({
|
|
10
|
+
args: process.argv.slice(2),
|
|
11
|
+
options: {
|
|
12
|
+
json: { type: "boolean", default: false },
|
|
13
|
+
"fail-on-critical": { type: "boolean", default: false },
|
|
14
|
+
"fail-on-warning": { type: "boolean", default: false },
|
|
15
|
+
"skip-semantic": { type: "boolean", default: false },
|
|
16
|
+
"skip-pinning": { type: "boolean", default: false },
|
|
17
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
18
|
+
url: { type: "string" },
|
|
19
|
+
transport: { type: "string" },
|
|
20
|
+
server: { type: "string" },
|
|
21
|
+
mcp: { type: "boolean", default: false },
|
|
22
|
+
},
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
});
|
|
25
|
+
async function resolveConfigPath() {
|
|
26
|
+
if (positionals[0])
|
|
27
|
+
return positionals[0];
|
|
28
|
+
const candidates = [
|
|
29
|
+
join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
30
|
+
join(homedir(), ".config", "Claude", "claude_desktop_config.json"),
|
|
31
|
+
join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json"),
|
|
32
|
+
];
|
|
33
|
+
return candidates.find(existsSync) ?? candidates[0];
|
|
34
|
+
}
|
|
35
|
+
function printReport(results, verbose) {
|
|
36
|
+
for (const server of results) {
|
|
37
|
+
const icon = server.status === "critical" ? "[CRIT]" :
|
|
38
|
+
server.status === "warning" ? "[WARN]" : "[OK]";
|
|
39
|
+
console.log(`\n${icon} ${server.serverName} [${server.transport}]`);
|
|
40
|
+
console.log(` ${server.summary.total} tools | ${server.summary.critical} critical | ${server.summary.warnings} warnings | ${server.summary.clean} clean`);
|
|
41
|
+
for (const tool of server.tools) {
|
|
42
|
+
if (tool.status === "clean" && !verbose)
|
|
43
|
+
continue;
|
|
44
|
+
const toolIcon = tool.status === "critical" ? " [CRIT]" : " [WARN]";
|
|
45
|
+
console.log(`${toolIcon} ${tool.toolName}`);
|
|
46
|
+
for (const issue of tool.issues) {
|
|
47
|
+
if (issue.severity === "info")
|
|
48
|
+
continue;
|
|
49
|
+
console.log(` [${issue.id}] ${issue.message}`);
|
|
50
|
+
if (verbose && issue.evidence)
|
|
51
|
+
console.log(` evidence: "${issue.evidence}"`);
|
|
52
|
+
if (verbose && issue.confidence < 1.0)
|
|
53
|
+
console.log(` confidence: ${(issue.confidence * 100).toFixed(0)}%`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function main() {
|
|
59
|
+
if (flags.mcp) {
|
|
60
|
+
const { startMcpServer } = await import("@mcp-guardian/server");
|
|
61
|
+
await startMcpServer();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
// Single URL mode
|
|
66
|
+
if (flags.url) {
|
|
67
|
+
const transport = (flags.transport ?? "http");
|
|
68
|
+
const fetchFn = transport === "sse" ? fetchToolsFromSse : fetchToolsFromHttp;
|
|
69
|
+
const tools = await fetchFn({ url: flags.url });
|
|
70
|
+
const scanResult = await scanServer(flags.url, tools, transport, { skipSemantic: flags["skip-semantic"] });
|
|
71
|
+
if (!flags["skip-pinning"]) {
|
|
72
|
+
const pinResult = verifyToolDefinitions(tools, flags.url);
|
|
73
|
+
if (pinResult.status === "changed" || pinResult.status === "tampered") {
|
|
74
|
+
scanResult.status = "critical";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
results.push(scanResult);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Claude Desktop config mode
|
|
81
|
+
const configPath = await resolveConfigPath();
|
|
82
|
+
if (!existsSync(configPath)) {
|
|
83
|
+
console.error(`Config not found: ${configPath}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
87
|
+
const servers = config.mcpServers ?? {};
|
|
88
|
+
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
89
|
+
if (flags.server && serverName !== flags.server)
|
|
90
|
+
continue;
|
|
91
|
+
let tools;
|
|
92
|
+
let transport = "stdio";
|
|
93
|
+
if (serverConfig.url) {
|
|
94
|
+
const raw = serverConfig.transport ?? "http";
|
|
95
|
+
if (raw === "sse") {
|
|
96
|
+
transport = "sse";
|
|
97
|
+
tools = await fetchToolsFromSse({ url: serverConfig.url });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
transport = "http";
|
|
101
|
+
tools = await fetchToolsFromHttp({ url: serverConfig.url });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (serverConfig.command) {
|
|
105
|
+
tools = await fetchToolsFromStdio({ command: serverConfig.command, args: serverConfig.args, env: serverConfig.env });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.warn(` Skipping ${serverName}: no command or URL`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const scanResult = await scanServer(serverName, tools, transport, { skipSemantic: flags["skip-semantic"] });
|
|
112
|
+
if (!flags["skip-pinning"]) {
|
|
113
|
+
const pinResult = verifyToolDefinitions(tools, serverName);
|
|
114
|
+
if (pinResult.status === "changed")
|
|
115
|
+
console.warn(` Tool definitions changed in "${serverName}" since last approval`);
|
|
116
|
+
if (pinResult.status === "tampered") {
|
|
117
|
+
console.error(` Manifest tampered for "${serverName}" - treat as critical`);
|
|
118
|
+
scanResult.status = "critical";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
results.push(scanResult);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (flags.json) {
|
|
125
|
+
console.log(JSON.stringify(results, null, 2));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
printReport(results, flags.verbose ?? false);
|
|
129
|
+
}
|
|
130
|
+
const hasCritical = results.some(r => r.status === "critical");
|
|
131
|
+
const hasWarning = results.some(r => r.status === "warning");
|
|
132
|
+
if (flags["fail-on-critical"] && hasCritical)
|
|
133
|
+
process.exit(2);
|
|
134
|
+
if (flags["fail-on-warning"] && (hasCritical || hasWarning))
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
main().catch(err => {
|
|
138
|
+
console.error("Fatal:", err.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startTui(dashboardUrl?: string): Promise<void>;
|
package/dist/tui/app.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Guardian Interactive Polished CLI/TUI
|
|
3
|
+
*
|
|
4
|
+
* A real-time terminal dashboard aggregating metrics, logs, and audit trails
|
|
5
|
+
* from all MCP Guardian instances into a unified view.
|
|
6
|
+
*
|
|
7
|
+
* Zero mock data — all values come from live dashboard API or PostgreSQL.
|
|
8
|
+
*/
|
|
9
|
+
import { DataFetcher } from './data-fetcher.js';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
// ── Color helpers ──────────────────────────────────────────────────
|
|
12
|
+
const C = {
|
|
13
|
+
dim: chalk.dim,
|
|
14
|
+
bold: chalk.bold,
|
|
15
|
+
green: chalk.green,
|
|
16
|
+
red: chalk.red,
|
|
17
|
+
yellow: chalk.yellow,
|
|
18
|
+
cyan: chalk.cyan,
|
|
19
|
+
magenta: chalk.magenta,
|
|
20
|
+
white: chalk.white,
|
|
21
|
+
gray: chalk.gray,
|
|
22
|
+
blue: chalk.blue,
|
|
23
|
+
bgRed: chalk.bgRed,
|
|
24
|
+
bgGreen: chalk.bgGreen,
|
|
25
|
+
bgYellow: chalk.bgYellow,
|
|
26
|
+
};
|
|
27
|
+
export async function startTui(dashboardUrl) {
|
|
28
|
+
const fetcher = new DataFetcher(dashboardUrl);
|
|
29
|
+
const state = { activeTab: 0, running: true, lastRefresh: '', frameCount: 0 };
|
|
30
|
+
// ── Terminal setup ───────────────────────────────────────────────
|
|
31
|
+
const stdout = process.stdout;
|
|
32
|
+
const stdin = process.stdin;
|
|
33
|
+
stdin.setRawMode(true);
|
|
34
|
+
stdin.resume();
|
|
35
|
+
stdin.setEncoding('utf-8');
|
|
36
|
+
// Hide cursor
|
|
37
|
+
stdout.write('\x1B[?25l');
|
|
38
|
+
// ── Key handler ──────────────────────────────────────────────────
|
|
39
|
+
stdin.on('data', (key) => {
|
|
40
|
+
const code = key.charCodeAt(0);
|
|
41
|
+
if (code === 3 || code === 27) {
|
|
42
|
+
// Ctrl+C or Escape → quit
|
|
43
|
+
state.running = false;
|
|
44
|
+
}
|
|
45
|
+
else if (code === 9) {
|
|
46
|
+
// Tab → next tab
|
|
47
|
+
state.activeTab = (state.activeTab + 1) % 8;
|
|
48
|
+
}
|
|
49
|
+
else if (key === '1')
|
|
50
|
+
state.activeTab = 0;
|
|
51
|
+
else if (key === '2')
|
|
52
|
+
state.activeTab = 1;
|
|
53
|
+
else if (key === '3')
|
|
54
|
+
state.activeTab = 2;
|
|
55
|
+
else if (key === '4')
|
|
56
|
+
state.activeTab = 3;
|
|
57
|
+
else if (key === '5')
|
|
58
|
+
state.activeTab = 4;
|
|
59
|
+
else if (key === '6')
|
|
60
|
+
state.activeTab = 5;
|
|
61
|
+
else if (key === '7')
|
|
62
|
+
state.activeTab = 6;
|
|
63
|
+
else if (key === '8')
|
|
64
|
+
state.activeTab = 7;
|
|
65
|
+
else if (key === 'r') {
|
|
66
|
+
// Manual refresh
|
|
67
|
+
fetcher.fetchAll().catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// ── Connect to dashboard ─────────────────────────────────────────
|
|
71
|
+
fetcher.connectWebSocket();
|
|
72
|
+
// Fallback polling
|
|
73
|
+
const pollInterval = fetcher.startPolling(3000);
|
|
74
|
+
// Initial fetch
|
|
75
|
+
try {
|
|
76
|
+
await fetcher.fetchAll();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Will show empty dashboard until data arrives
|
|
80
|
+
}
|
|
81
|
+
// Subscribe to data changes
|
|
82
|
+
fetcher.onChange(() => {
|
|
83
|
+
render(state, fetcher.getData());
|
|
84
|
+
});
|
|
85
|
+
// ── Render loop ──────────────────────────────────────────────────
|
|
86
|
+
const renderInterval = setInterval(() => {
|
|
87
|
+
if (!state.running) {
|
|
88
|
+
clearInterval(renderInterval);
|
|
89
|
+
clearInterval(pollInterval);
|
|
90
|
+
fetcher.stop();
|
|
91
|
+
stdout.write('\x1B[?25h'); // Show cursor
|
|
92
|
+
stdout.write('\x1B[2J\x1B[H'); // Clear screen
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
render(state, fetcher.getData());
|
|
96
|
+
state.frameCount++;
|
|
97
|
+
}, 1000 / 15); // 15 FPS
|
|
98
|
+
// Initial render
|
|
99
|
+
render(state, fetcher.getData());
|
|
100
|
+
// Keep process alive
|
|
101
|
+
await new Promise((resolve) => {
|
|
102
|
+
const check = setInterval(() => {
|
|
103
|
+
if (!state.running) {
|
|
104
|
+
clearInterval(check);
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
}, 100);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// ── TABS ────────────────────────────────────────────────────────────
|
|
111
|
+
const TABS = [
|
|
112
|
+
{ name: 'Overview', key: '1' },
|
|
113
|
+
{ name: 'Security', key: '2' },
|
|
114
|
+
{ name: 'Cost', key: '3' },
|
|
115
|
+
{ name: 'Health', key: '4' },
|
|
116
|
+
{ name: 'AI Engine', key: '5' },
|
|
117
|
+
{ name: 'Audit Trail', key: '6' },
|
|
118
|
+
{ name: 'Policy', key: '7' },
|
|
119
|
+
{ name: 'Instances', key: '8' },
|
|
120
|
+
];
|
|
121
|
+
// ── Main render function ───────────────────────────────────────────
|
|
122
|
+
function render(state, data) {
|
|
123
|
+
const stdout = process.stdout;
|
|
124
|
+
const termWidth = stdout.columns || 120;
|
|
125
|
+
const termHeight = stdout.rows || 30;
|
|
126
|
+
// Clear screen
|
|
127
|
+
stdout.write('\x1B[2J\x1B[H');
|
|
128
|
+
// ── Header ──────────────────────────────────────────────────────
|
|
129
|
+
const header = [
|
|
130
|
+
C.bold.cyan('╔══════════════════════════════════════════════════════════════════════════════════╗'),
|
|
131
|
+
C.bold.cyan(`║ MCP GUARDIAN — UNIFIED OBSERVABILITY PLATFORM${' '.repeat(Math.max(0, termWidth - 39 - 39))}║`),
|
|
132
|
+
C.bold.magenta(`║ Adaptive AI-Driven Policy Engine | Real-Time Metrics | Multi-Instance Aggregation ║`),
|
|
133
|
+
];
|
|
134
|
+
for (const line of header)
|
|
135
|
+
stdout.write(line + '\n');
|
|
136
|
+
// ── Top bar ─────────────────────────────────────────────────────
|
|
137
|
+
if (data) {
|
|
138
|
+
const instancesText = `${data.instances.length} instances`;
|
|
139
|
+
const blockedText = `${data.overview.blockedRequests} blocked`;
|
|
140
|
+
const costText = `$${data.overview.totalCostUsd.toFixed(4)}`;
|
|
141
|
+
const latencyText = `${data.overview.avgLatencyMs.toFixed(0)}ms avg`;
|
|
142
|
+
const bar = C.dim(` ${C.green(instancesText)} │ ${C.red(blockedText)} │ ${C.yellow(costText)} │ ${C.cyan(latencyText)} │ ` +
|
|
143
|
+
`Updated: ${data.overview.lastUpdated?.slice(11, 19) || 'N/A'}`);
|
|
144
|
+
stdout.write(bar + '\n');
|
|
145
|
+
}
|
|
146
|
+
stdout.write(C.dim('─'.repeat(termWidth - 1)) + '\n');
|
|
147
|
+
// ── Tab bar ──────────────────────────────────────────────────────
|
|
148
|
+
let tabBar = '';
|
|
149
|
+
for (let i = 0; i < TABS.length; i++) {
|
|
150
|
+
if (i === state.activeTab) {
|
|
151
|
+
tabBar += C.bgGreen.black(` ${TABS[i].key}. ${TABS[i].name} `) + ' ';
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
tabBar += C.dim(`${TABS[i].key}.`) + ` ${TABS[i].name} `;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
stdout.write(' ' + tabBar + '\n');
|
|
158
|
+
stdout.write(C.dim('─'.repeat(termWidth - 1)) + '\n\n');
|
|
159
|
+
// ── Tab content ─────────────────────────────────────────────────
|
|
160
|
+
if (!data) {
|
|
161
|
+
stdout.write(C.yellow('\n ⏳ Connecting to dashboard... Fetching real-time data.\n'));
|
|
162
|
+
stdout.write(C.dim(' Ensure mcp-guardian proxy is running with DASHBOARD_ENABLED=true\n\n'));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
switch (state.activeTab) {
|
|
166
|
+
case 0:
|
|
167
|
+
renderOverview(data);
|
|
168
|
+
break;
|
|
169
|
+
case 1:
|
|
170
|
+
renderSecurity(data);
|
|
171
|
+
break;
|
|
172
|
+
case 2:
|
|
173
|
+
renderCost(data);
|
|
174
|
+
break;
|
|
175
|
+
case 3:
|
|
176
|
+
renderHealth(data);
|
|
177
|
+
break;
|
|
178
|
+
case 4:
|
|
179
|
+
renderAi(data);
|
|
180
|
+
break;
|
|
181
|
+
case 5:
|
|
182
|
+
renderAudit(data);
|
|
183
|
+
break;
|
|
184
|
+
case 6:
|
|
185
|
+
renderPolicy(data);
|
|
186
|
+
break;
|
|
187
|
+
case 7:
|
|
188
|
+
renderInstances(data);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ── Footer ──────────────────────────────────────────────────────
|
|
193
|
+
stdout.write('\n' + C.dim('─'.repeat(termWidth - 1)) + '\n');
|
|
194
|
+
stdout.write(C.dim(' 1-8:Switch Tab Tab:Next r:Refresh Esc:Quit Frame: ' + state.frameCount + '\n'));
|
|
195
|
+
}
|
|
196
|
+
// ── PANEL RENDERERS ────────────────────────────────────────────────
|
|
197
|
+
function renderOverview(data) {
|
|
198
|
+
const stdout = process.stdout;
|
|
199
|
+
const o = data.overview;
|
|
200
|
+
// Score gauge
|
|
201
|
+
const score = data.security.overallScore;
|
|
202
|
+
const scoreColor = score >= 70 ? C.green : score >= 40 ? C.yellow : C.red;
|
|
203
|
+
const bar = '█'.repeat(Math.round(score / 5)) + '░'.repeat(20 - Math.round(score / 5));
|
|
204
|
+
stdout.write(C.bold.white(' 📊 EXECUTIVE SUMMARY\n\n'));
|
|
205
|
+
stdout.write(` Overall Security Score: ${scoreColor(`${score}/100`)} ${C.dim(`[${bar}]`)}\n`);
|
|
206
|
+
stdout.write(` Active Instances: ${C.green(o.activeInstances)} / ${C.white(o.totalInstances)}\n`);
|
|
207
|
+
stdout.write(` Total Requests: ${C.white(o.totalRequests.toLocaleString())}\n`);
|
|
208
|
+
stdout.write(` Blocked Requests: ${o.blockedRequests > 0 ? C.red(o.blockedRequests.toLocaleString()) : C.green('0')}\n`);
|
|
209
|
+
stdout.write(` Pass Rate: ${o.passRate > 90 ? C.green(`${o.passRate.toFixed(1)}%`) : C.yellow(`${o.passRate.toFixed(1)}%`)}\n`);
|
|
210
|
+
stdout.write(` Total Cost: ${C.yellow(`$${o.totalCostUsd.toFixed(4)}`)}\n`);
|
|
211
|
+
stdout.write(` Burn Rate: ${C.yellow(`$${o.burnRatePerHour.toFixed(4)}/hr`)}\n`);
|
|
212
|
+
stdout.write(` Avg Latency: ${o.avgLatencyMs < 200 ? C.green(`${o.avgLatencyMs.toFixed(0)}ms`) : C.yellow(`${o.avgLatencyMs.toFixed(0)}ms`)}\n`);
|
|
213
|
+
stdout.write(` Active Servers: ${C.cyan(o.activeServers)}\n`);
|
|
214
|
+
// Top risks
|
|
215
|
+
if (data.security.worstOffenders.length > 0) {
|
|
216
|
+
stdout.write(C.bold.red(`\n ⚠️ TOP RISKS:\n`));
|
|
217
|
+
for (const w of data.security.worstOffenders.slice(0, 5)) {
|
|
218
|
+
stdout.write(C.red(` • ${w}\n`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// AI Insights
|
|
222
|
+
if (data.ai.report?.executiveSummary?.topRisks?.length > 0) {
|
|
223
|
+
stdout.write(C.bold.yellow(`\n 🤖 AI INSIGHTS:\n`));
|
|
224
|
+
for (const r of data.ai.report.executiveSummary.recommendations?.slice(0, 3) || []) {
|
|
225
|
+
stdout.write(C.yellow(` • ${r}\n`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Budget alerts
|
|
229
|
+
if (data.cost.budgetAlerts.length > 0) {
|
|
230
|
+
stdout.write(C.bold.red(`\n 💰 BUDGET ALERTS:\n`));
|
|
231
|
+
for (const alert of data.cost.budgetAlerts) {
|
|
232
|
+
stdout.write(C.red(` • ${alert}\n`));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function renderSecurity(data) {
|
|
237
|
+
const stdout = process.stdout;
|
|
238
|
+
const s = data.security;
|
|
239
|
+
stdout.write(C.bold.white(' 🔒 SECURITY POSTURE\n\n'));
|
|
240
|
+
stdout.write(` Overall Score: ${s.overallScore >= 70 ? C.green(s.overallScore) : C.red(s.overallScore)}/100\n`);
|
|
241
|
+
stdout.write(` Active Threats: ${s.activeThreats > 0 ? C.red(s.activeThreats) : C.green('0')}\n`);
|
|
242
|
+
stdout.write(` Last Scan: ${C.dim(s.lastScan)}\n\n`);
|
|
243
|
+
if (s.servers.length === 0) {
|
|
244
|
+
stdout.write(C.dim(' No security data available — run scan_security to populate.\n'));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Table header
|
|
248
|
+
stdout.write(C.dim(' ┌──────────────────────────┬─────────┬───────┬──────────┬────────┐\n'));
|
|
249
|
+
stdout.write(C.dim(' │ Server │ Score │ CVEs │ Critical │ Auth │\n'));
|
|
250
|
+
stdout.write(C.dim(' ├──────────────────────────┼─────────┼───────┼──────────┼────────┤\n'));
|
|
251
|
+
for (const server of s.servers) {
|
|
252
|
+
const scoreColor = server.score >= 70 ? C.green : server.score >= 40 ? C.yellow : C.red;
|
|
253
|
+
const name = server.name.slice(0, 24).padEnd(24);
|
|
254
|
+
const score = scoreColor(String(server.score).padEnd(7));
|
|
255
|
+
const cves = server.cves > 0 ? C.red(String(server.cves).padEnd(5)) : C.dim(String(server.cves).padEnd(5));
|
|
256
|
+
const critical = server.critical > 0 ? C.bgRed.white(` ${server.critical} `) : C.dim(' 0 ');
|
|
257
|
+
const auth = server.auth ? C.green('✅') : C.red('❌');
|
|
258
|
+
stdout.write(` │ ${C.white(name)}│ ${score}│ ${cves}│ ${critical} │ ${auth} │\n`);
|
|
259
|
+
}
|
|
260
|
+
stdout.write(C.dim(' └──────────────────────────┴─────────┴───────┴──────────┴────────┘\n'));
|
|
261
|
+
}
|
|
262
|
+
function renderCost(data) {
|
|
263
|
+
const stdout = process.stdout;
|
|
264
|
+
const c = data.cost;
|
|
265
|
+
stdout.write(C.bold.white(' 💰 COST ANALYSIS\n\n'));
|
|
266
|
+
stdout.write(` Total Cost: ${C.yellow(`$${c.totalCost.toFixed(4)}`)}\n`);
|
|
267
|
+
stdout.write(` Projected Monthly: ${C.yellow(`$${c.projectedMonthly.toFixed(2)}`)}\n`);
|
|
268
|
+
stdout.write(` Pricing Model: ${C.cyan(c.pricingModel)}\n\n`);
|
|
269
|
+
if (c.servers.length === 0) {
|
|
270
|
+
stdout.write(C.dim(' No cost data available — run audit_costs to populate.\n'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// Table
|
|
274
|
+
stdout.write(C.dim(' ┌──────────────────────────┬────────────┬──────────┬─────────┐\n'));
|
|
275
|
+
stdout.write(C.dim(' │ Server │ Tokens │ Cost USD │ Trend │\n'));
|
|
276
|
+
stdout.write(C.dim(' ├──────────────────────────┼────────────┼──────────┼─────────┤\n'));
|
|
277
|
+
for (const server of c.servers) {
|
|
278
|
+
const name = server.name.slice(0, 24).padEnd(24);
|
|
279
|
+
const tokens = String(server.tokens.toLocaleString()).padEnd(10);
|
|
280
|
+
const cost = `$${server.cost.toFixed(4)}`.padEnd(8);
|
|
281
|
+
const trendIcon = server.trend === 'increasing' ? '📈' : server.trend === 'decreasing' ? '📉' : '➡️';
|
|
282
|
+
stdout.write(` │ ${C.white(name)}│ ${C.cyan(tokens)}│ ${C.yellow(cost)}│ ${trendIcon} │\n`);
|
|
283
|
+
}
|
|
284
|
+
stdout.write(C.dim(' └──────────────────────────┴────────────┴──────────┴─────────┘\n'));
|
|
285
|
+
}
|
|
286
|
+
function renderHealth(data) {
|
|
287
|
+
const stdout = process.stdout;
|
|
288
|
+
const h = data.health;
|
|
289
|
+
stdout.write(C.bold.white(' ❤️ HEALTH STATUS\n\n'));
|
|
290
|
+
stdout.write(` Avg Latency: ${h.avgLatency < 200 ? C.green(`${h.avgLatency}ms`) : C.yellow(`${h.avgLatency}ms`)}\n`);
|
|
291
|
+
stdout.write(` Total Tools: ${C.cyan(h.totalTools)}\n`);
|
|
292
|
+
stdout.write(` At Risk: ${h.atRisk.length > 0 ? C.red(h.atRisk.join(', ')) : C.green('None')}\n\n`);
|
|
293
|
+
if (h.servers.length === 0) {
|
|
294
|
+
stdout.write(C.dim(' No health data available — run check_health to populate.\n'));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Table
|
|
298
|
+
stdout.write(C.dim(' ┌──────────────────────────┬──────────┬──────────┬───────┬───────────┐\n'));
|
|
299
|
+
stdout.write(C.dim(' │ Server │ Latency │ Success │ Tools │ Breaker │\n'));
|
|
300
|
+
stdout.write(C.dim(' ├──────────────────────────┼──────────┼──────────┼───────┼───────────┤\n'));
|
|
301
|
+
for (const server of h.servers) {
|
|
302
|
+
const name = server.name.slice(0, 24).padEnd(24);
|
|
303
|
+
const latency = server.latency < 200 ? C.green(`${server.latency}ms`.padEnd(8)) : C.yellow(`${server.latency}ms`.padEnd(8));
|
|
304
|
+
const success = server.successRate > 90 ? C.green(`${server.successRate.toFixed(0)}%`.padEnd(8)) : C.red(`${server.successRate.toFixed(0)}%`.padEnd(8));
|
|
305
|
+
const tools = String(server.tools).padEnd(5);
|
|
306
|
+
const breaker = server.circuitBreaker === 'open' ? C.red('OPEN') : server.circuitBreaker === 'half_open' ? C.yellow('HALF') : C.green('CLOSED');
|
|
307
|
+
stdout.write(` │ ${C.white(name)}│ ${latency}│ ${success}│ ${C.cyan(tools)}│ ${breaker} │\n`);
|
|
308
|
+
}
|
|
309
|
+
stdout.write(C.dim(' └──────────────────────────┴──────────┴──────────┴───────┴───────────┘\n'));
|
|
310
|
+
}
|
|
311
|
+
function renderAi(data) {
|
|
312
|
+
const stdout = process.stdout;
|
|
313
|
+
const ai = data.ai;
|
|
314
|
+
stdout.write(C.bold.white(' 🤖 ADAPTIVE AI ENGINE\n\n'));
|
|
315
|
+
// Learning state
|
|
316
|
+
const ls = ai.learningState;
|
|
317
|
+
stdout.write(C.bold.cyan(' Self-Improvement State:\n'));
|
|
318
|
+
stdout.write(` Adaptive Threshold: ${C.magenta(ls.adaptiveThreshold.toFixed(2))}\n`);
|
|
319
|
+
stdout.write(` True Positive Rate: ${C.green(`${(ls.truePositiveRate * 100).toFixed(0)}%`)}\n`);
|
|
320
|
+
stdout.write(` False Positive Rate: ${ls.falsePositiveRate > 0.3 ? C.red(`${(ls.falsePositiveRate * 100).toFixed(0)}%`) : C.green(`${(ls.falsePositiveRate * 100).toFixed(0)}%`)}\n`);
|
|
321
|
+
// Module weights
|
|
322
|
+
stdout.write(C.dim(`\n Module Weights:\n`));
|
|
323
|
+
const modules = ls.moduleWeights || {};
|
|
324
|
+
for (const [name, weight] of Object.entries(modules)) {
|
|
325
|
+
const w = typeof weight === 'number' ? weight : parseFloat(weight);
|
|
326
|
+
const bar = '█'.repeat(Math.round(w * 20)) + '░'.repeat(20 - Math.round(w * 20));
|
|
327
|
+
stdout.write(` ${C.cyan(name.padEnd(12))} ${bar} ${w.toFixed(2)}\n`);
|
|
328
|
+
}
|
|
329
|
+
// AI Suggestions
|
|
330
|
+
stdout.write(C.bold.cyan(`\n Active Suggestions (${ai.suggestions.length}):\n`));
|
|
331
|
+
if (ai.suggestions.length === 0) {
|
|
332
|
+
stdout.write(C.dim(' No suggestions yet — AI engine needs data to analyze.\n'));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
for (const s of ai.suggestions.slice(0, 5)) {
|
|
336
|
+
const conf = s.confidence || 0;
|
|
337
|
+
const confColor = conf >= 0.85 ? C.green : conf >= 0.5 ? C.yellow : C.dim;
|
|
338
|
+
stdout.write(` ${confColor(`${(conf * 100).toFixed(0)}%`)} ${C.white(s.reason || s.id || 'Unknown')}\n`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Threat intel
|
|
342
|
+
stdout.write(C.bold.cyan(`\n Threat Intelligence (${ai.threats.length} active):\n`));
|
|
343
|
+
if (ai.threats.length === 0) {
|
|
344
|
+
stdout.write(C.dim(' No active threats — feeds are clean.\n'));
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
for (const t of ai.threats.slice(0, 5)) {
|
|
348
|
+
const sev = t.severity || t.entry?.severity;
|
|
349
|
+
const sevColor = sev === 'CRITICAL' ? C.bgRed.white : sev === 'HIGH' ? C.red : C.yellow;
|
|
350
|
+
stdout.write(` ${sevColor(` ${sev || 'UNKNOWN'} `)} ${C.white(t.reason || t.entry?.description || 'Unknown threat')}\n`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Baselines
|
|
354
|
+
stdout.write(C.bold.cyan(`\n Behavioral Baselines (${ai.baselines.length}):\n`));
|
|
355
|
+
if (ai.baselines.length === 0) {
|
|
356
|
+
stdout.write(C.dim(' No baselines learned yet.\n'));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
for (const b of ai.baselines.slice(0, 5)) {
|
|
360
|
+
stdout.write(` ${C.white(b.toolName || b.tool)} on ${C.cyan(b.serverName || b.server)}: ${b.sampleCount} samples, avg ${Math.round(b.avgTokens || b.avg_tokens || 0)} tokens\n`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function renderAudit(data) {
|
|
365
|
+
const stdout = process.stdout;
|
|
366
|
+
const a = data.audit;
|
|
367
|
+
stdout.write(C.bold.white(' 📋 AUDIT TRAIL\n\n'));
|
|
368
|
+
stdout.write(` Total Events: ${C.white(a.total.toLocaleString())}\n`);
|
|
369
|
+
stdout.write(` Passed: ${C.green(a.passed.toLocaleString())}\n`);
|
|
370
|
+
stdout.write(` Blocked: ${a.blocked > 0 ? C.red(a.blocked.toLocaleString()) : C.green('0')}\n`);
|
|
371
|
+
stdout.write(` Flagged: ${a.flagged > 0 ? C.yellow(a.flagged.toLocaleString()) : C.green('0')}\n`);
|
|
372
|
+
stdout.write(C.dim(`\n Recent Events:\n`));
|
|
373
|
+
if (a.events.length === 0) {
|
|
374
|
+
stdout.write(C.dim(' No audit events recorded yet.\n'));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
for (const event of a.events.slice(0, 10)) {
|
|
378
|
+
const time = (event.timestamp || '').slice(11, 19) || '--:--:--';
|
|
379
|
+
const action = event.action === 'block' ? C.red('BLOCK') : event.action === 'flag' ? C.yellow('FLAG') : C.green('PASS');
|
|
380
|
+
const server = (event.server_name || event.serverName || '').slice(0, 20).padEnd(20);
|
|
381
|
+
const tool = (event.tool_name || event.toolName || '').slice(0, 15).padEnd(15);
|
|
382
|
+
const rule = (event.rule_name || event.ruleName || '-').slice(0, 25);
|
|
383
|
+
stdout.write(` ${C.dim(time)} ${action} ${C.cyan(server)} ${C.white(tool)} ${C.dim(rule)}\n`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function renderPolicy(data) {
|
|
388
|
+
const stdout = process.stdout;
|
|
389
|
+
const p = data.policy;
|
|
390
|
+
stdout.write(C.bold.white(' 📜 ACTIVE POLICY\n\n'));
|
|
391
|
+
stdout.write(` Mode: ${p.mode === 'block' ? C.red('BLOCK') : p.mode === 'warn' ? C.yellow('WARN') : C.green('AUDIT')}\n`);
|
|
392
|
+
stdout.write(` Active Rules: ${C.cyan(p.activeRules)}\n`);
|
|
393
|
+
stdout.write(` Auto-Generated: ${C.magenta(p.autoGeneratedRules.length)}\n`);
|
|
394
|
+
if (p.autoGeneratedRules.length > 0) {
|
|
395
|
+
stdout.write(C.dim(`\n AI-Generated Rules:\n`));
|
|
396
|
+
for (const rule of p.autoGeneratedRules.slice(0, 10)) {
|
|
397
|
+
stdout.write(` ${C.magenta('✦')} ${rule}\n`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (p.rules.length === 0) {
|
|
401
|
+
stdout.write(C.dim('\n No policy rules loaded. Start proxy with --policy to activate.\n'));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function renderInstances(data) {
|
|
405
|
+
const stdout = process.stdout;
|
|
406
|
+
const instances = data.instances;
|
|
407
|
+
stdout.write(C.bold.white(' 🖥️ GUARDIAN INSTANCES\n\n'));
|
|
408
|
+
stdout.write(` Total Instances: ${C.white(instances.length)}\n`);
|
|
409
|
+
const active = instances.filter(i => i.status === 'active').length;
|
|
410
|
+
const degraded = instances.filter(i => i.status === 'degraded').length;
|
|
411
|
+
const offline = instances.filter(i => i.status === 'offline').length;
|
|
412
|
+
stdout.write(` Active: ${C.green(active)} Degraded: ${C.yellow(degraded)} Offline: ${C.red(offline)}\n\n`);
|
|
413
|
+
if (instances.length === 0) {
|
|
414
|
+
stdout.write(C.dim(' No instances registered. Start mcp-guardian proxy to register.\n'));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Table
|
|
418
|
+
stdout.write(C.dim(' ┌──────────────────────┬──────────┬───────────┬──────────┬───────────┐\n'));
|
|
419
|
+
stdout.write(C.dim(' │ Instance │ Status │ Requests │ Blocked │ Latency │\n'));
|
|
420
|
+
stdout.write(C.dim(' ├──────────────────────┼──────────┼───────────┼──────────┼───────────┤\n'));
|
|
421
|
+
for (const inst of instances) {
|
|
422
|
+
const name = inst.instanceId.slice(0, 20).padEnd(20);
|
|
423
|
+
const status = inst.status === 'active' ? C.green('ACTIVE') : inst.status === 'degraded' ? C.yellow('DEGRADED') : C.red('OFFLINE');
|
|
424
|
+
const requests = String(inst.totalRequests.toLocaleString()).padEnd(9);
|
|
425
|
+
const blocked = inst.blockedRequests > 0 ? C.red(String(inst.blockedRequests).padEnd(8)) : C.dim('0'.padEnd(8));
|
|
426
|
+
const latency = inst.avgLatencyMs ? `${inst.avgLatencyMs.toFixed(0)}ms`.padEnd(9) : 'N/A'.padEnd(9);
|
|
427
|
+
stdout.write(` │ ${C.white(name)}│ ${status} │ ${C.cyan(requests)}│ ${blocked}│ ${C.dim(latency)}│\n`);
|
|
428
|
+
}
|
|
429
|
+
stdout.write(C.dim(' └──────────────────────┴──────────┴───────────┴──────────┴───────────┘\n'));
|
|
430
|
+
}
|