defense-mcp-server 0.6.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.
- package/CHANGELOG.md +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- package/package.json +87 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API security tools for Kali Defense MCP Server.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool: api_security (actions: scan_local_apis, audit_auth,
|
|
5
|
+
* check_rate_limiting, tls_verify, cors_check)
|
|
6
|
+
*
|
|
7
|
+
* Provides local API discovery, authentication auditing, rate limiting
|
|
8
|
+
* verification, TLS configuration checking, and CORS policy analysis.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { spawnSafe } from "../core/spawn-safe.js";
|
|
12
|
+
import { createTextContent, createErrorContent, formatToolOutput, } from "../core/parsers.js";
|
|
13
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
14
|
+
const DEFAULT_PORT_RANGE = "80,443,3000,4000,5000,8000,8080,8443,9000";
|
|
15
|
+
const COMMON_API_PATHS = [
|
|
16
|
+
"/api",
|
|
17
|
+
"/api/v1",
|
|
18
|
+
"/health",
|
|
19
|
+
"/status",
|
|
20
|
+
"/swagger.json",
|
|
21
|
+
"/openapi.json",
|
|
22
|
+
"/.well-known/openid-configuration",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Run a command via spawnSafe and collect output as a promise.
|
|
26
|
+
* Handles errors gracefully — returns error info instead of throwing.
|
|
27
|
+
*/
|
|
28
|
+
async function runCommand(command, args, timeoutMs = 30_000) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
let child;
|
|
31
|
+
try {
|
|
32
|
+
child = spawnSafe(command, args);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
resolve({ stdout: "", stderr: msg, exitCode: -1 });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let stdout = "";
|
|
40
|
+
let stderr = "";
|
|
41
|
+
let resolved = false;
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
if (!resolved) {
|
|
44
|
+
resolved = true;
|
|
45
|
+
child.kill("SIGTERM");
|
|
46
|
+
resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
|
|
47
|
+
}
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
child.stdout?.on("data", (data) => {
|
|
50
|
+
stdout += data.toString();
|
|
51
|
+
});
|
|
52
|
+
child.stderr?.on("data", (data) => {
|
|
53
|
+
stderr += data.toString();
|
|
54
|
+
});
|
|
55
|
+
child.on("close", (code) => {
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
resolved = true;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
resolve({ stdout, stderr, exitCode: code ?? -1 });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
child.on("error", (err) => {
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
resolved = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
resolve({ stdout, stderr: err.message, exitCode: -1 });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate and normalize a target URL.
|
|
73
|
+
* Returns the normalized URL or null if invalid.
|
|
74
|
+
*/
|
|
75
|
+
function validateTarget(target) {
|
|
76
|
+
try {
|
|
77
|
+
// Add scheme if missing
|
|
78
|
+
let url = target;
|
|
79
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
80
|
+
url = `http://${url}`;
|
|
81
|
+
}
|
|
82
|
+
const parsed = new URL(url);
|
|
83
|
+
if (!parsed.hostname)
|
|
84
|
+
return null;
|
|
85
|
+
return parsed.toString().replace(/\/$/, "");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse host and port from a target URL or host:port string.
|
|
93
|
+
*/
|
|
94
|
+
function parseHostPort(target) {
|
|
95
|
+
try {
|
|
96
|
+
let url = target;
|
|
97
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
98
|
+
url = `http://${url}`;
|
|
99
|
+
}
|
|
100
|
+
const parsed = new URL(url);
|
|
101
|
+
const host = parsed.hostname;
|
|
102
|
+
let port = parsed.port;
|
|
103
|
+
if (!port) {
|
|
104
|
+
port = parsed.protocol === "https:" ? "443" : "80";
|
|
105
|
+
}
|
|
106
|
+
return { host, port };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Fallback: try host:port parsing
|
|
110
|
+
const parts = target.split(":");
|
|
111
|
+
return { host: parts[0] || "localhost", port: parts[1] || "80" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function scanLocalApis(portRange) {
|
|
115
|
+
const result = {
|
|
116
|
+
listeningPorts: [],
|
|
117
|
+
discoveredApis: [],
|
|
118
|
+
totalDiscovered: 0,
|
|
119
|
+
recommendations: [],
|
|
120
|
+
};
|
|
121
|
+
// Get listening TCP ports
|
|
122
|
+
const ssResult = await runCommand("ss", ["-tlnp"], 10_000);
|
|
123
|
+
const listeningPorts = new Set();
|
|
124
|
+
if (ssResult.exitCode === 0) {
|
|
125
|
+
const lines = ssResult.stdout.split("\n");
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const portMatch = line.match(/:(\d+)\s/);
|
|
128
|
+
if (portMatch) {
|
|
129
|
+
listeningPorts.add(parseInt(portMatch[1], 10));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
result.listeningPorts = Array.from(listeningPorts).sort((a, b) => a - b);
|
|
134
|
+
// Parse target ports from range
|
|
135
|
+
const targetPorts = portRange
|
|
136
|
+
.split(",")
|
|
137
|
+
.map((p) => parseInt(p.trim(), 10))
|
|
138
|
+
.filter((p) => !isNaN(p));
|
|
139
|
+
// Check each target port
|
|
140
|
+
for (const port of targetPorts) {
|
|
141
|
+
// Check if HTTP service responds
|
|
142
|
+
const httpCheck = await runCommand("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}", "-m", "5", `http://localhost:${port}/`], 10_000);
|
|
143
|
+
if (httpCheck.exitCode !== 0 || httpCheck.stdout.trim() === "000") {
|
|
144
|
+
continue; // No HTTP response
|
|
145
|
+
}
|
|
146
|
+
const httpStatus = parseInt(httpCheck.stdout.trim(), 10);
|
|
147
|
+
if (isNaN(httpStatus))
|
|
148
|
+
continue;
|
|
149
|
+
const api = {
|
|
150
|
+
port,
|
|
151
|
+
httpStatus,
|
|
152
|
+
protocol: "http",
|
|
153
|
+
frameworkGuess: "unknown",
|
|
154
|
+
apiPaths: [],
|
|
155
|
+
docsUrl: null,
|
|
156
|
+
};
|
|
157
|
+
// Try to identify framework via response headers
|
|
158
|
+
const headerCheck = await runCommand("curl", ["-sI", "-m", "5", `http://localhost:${port}/`], 10_000);
|
|
159
|
+
if (headerCheck.exitCode === 0) {
|
|
160
|
+
const headers = headerCheck.stdout.toLowerCase();
|
|
161
|
+
if (headers.includes("x-powered-by: express")) {
|
|
162
|
+
api.frameworkGuess = "Express.js";
|
|
163
|
+
}
|
|
164
|
+
else if (headers.includes("x-powered-by: php")) {
|
|
165
|
+
api.frameworkGuess = "PHP";
|
|
166
|
+
}
|
|
167
|
+
else if (headers.includes("server: nginx")) {
|
|
168
|
+
api.frameworkGuess = "Nginx";
|
|
169
|
+
}
|
|
170
|
+
else if (headers.includes("server: apache")) {
|
|
171
|
+
api.frameworkGuess = "Apache";
|
|
172
|
+
}
|
|
173
|
+
else if (headers.includes("server: uvicorn") ||
|
|
174
|
+
headers.includes("server: gunicorn")) {
|
|
175
|
+
api.frameworkGuess = "Python (ASGI/WSGI)";
|
|
176
|
+
}
|
|
177
|
+
else if (headers.includes("x-powered-by: next.js")) {
|
|
178
|
+
api.frameworkGuess = "Next.js";
|
|
179
|
+
}
|
|
180
|
+
else if (headers.includes("server: kestrel")) {
|
|
181
|
+
api.frameworkGuess = ".NET Kestrel";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Check common API paths
|
|
185
|
+
for (const path of COMMON_API_PATHS) {
|
|
186
|
+
const pathCheck = await runCommand("curl", [
|
|
187
|
+
"-s",
|
|
188
|
+
"-o",
|
|
189
|
+
"/dev/null",
|
|
190
|
+
"-w",
|
|
191
|
+
"%{http_code}",
|
|
192
|
+
"-m",
|
|
193
|
+
"5",
|
|
194
|
+
`http://localhost:${port}${path}`,
|
|
195
|
+
], 10_000);
|
|
196
|
+
if (pathCheck.exitCode === 0) {
|
|
197
|
+
const status = parseInt(pathCheck.stdout.trim(), 10);
|
|
198
|
+
if (status >= 200 && status < 404) {
|
|
199
|
+
api.apiPaths.push(path);
|
|
200
|
+
if (path === "/swagger.json" || path === "/openapi.json") {
|
|
201
|
+
api.docsUrl = `http://localhost:${port}${path}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
result.discoveredApis.push(api);
|
|
207
|
+
}
|
|
208
|
+
result.totalDiscovered = result.discoveredApis.length;
|
|
209
|
+
if (result.totalDiscovered === 0) {
|
|
210
|
+
result.recommendations.push("No API services found on scanned ports — verify services are running");
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
for (const api of result.discoveredApis) {
|
|
214
|
+
if (!api.docsUrl) {
|
|
215
|
+
result.recommendations.push(`Port ${api.port}: No API documentation endpoint found — consider adding OpenAPI/Swagger`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
async function auditAuth(target) {
|
|
222
|
+
const result = {
|
|
223
|
+
target,
|
|
224
|
+
accessible: false,
|
|
225
|
+
statusWithoutAuth: 0,
|
|
226
|
+
statusWithAuth: 0,
|
|
227
|
+
authRequired: false,
|
|
228
|
+
authType: "none",
|
|
229
|
+
verboseErrors: false,
|
|
230
|
+
errorDetails: [],
|
|
231
|
+
recommendations: [],
|
|
232
|
+
};
|
|
233
|
+
// Check without authentication
|
|
234
|
+
const noAuthCheck = await runCommand("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}", "-m", "5", target], 10_000);
|
|
235
|
+
if (noAuthCheck.exitCode !== 0 || noAuthCheck.stdout.trim() === "000") {
|
|
236
|
+
result.recommendations.push("Target is not reachable — check URL and connectivity");
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
result.accessible = true;
|
|
240
|
+
result.statusWithoutAuth = parseInt(noAuthCheck.stdout.trim(), 10);
|
|
241
|
+
// Check with Bearer token
|
|
242
|
+
const bearerCheck = await runCommand("curl", [
|
|
243
|
+
"-s",
|
|
244
|
+
"-o",
|
|
245
|
+
"/dev/null",
|
|
246
|
+
"-w",
|
|
247
|
+
"%{http_code}",
|
|
248
|
+
"-m",
|
|
249
|
+
"5",
|
|
250
|
+
"-H",
|
|
251
|
+
"Authorization: Bearer test",
|
|
252
|
+
target,
|
|
253
|
+
], 10_000);
|
|
254
|
+
if (bearerCheck.exitCode === 0) {
|
|
255
|
+
result.statusWithAuth = parseInt(bearerCheck.stdout.trim(), 10);
|
|
256
|
+
}
|
|
257
|
+
// Determine auth enforcement
|
|
258
|
+
if (result.statusWithoutAuth === 401 ||
|
|
259
|
+
result.statusWithoutAuth === 403) {
|
|
260
|
+
result.authRequired = true;
|
|
261
|
+
result.authType = "detected";
|
|
262
|
+
// Try to detect auth type from response headers
|
|
263
|
+
const headerCheck = await runCommand("curl", ["-sI", "-m", "5", target], 10_000);
|
|
264
|
+
if (headerCheck.exitCode === 0) {
|
|
265
|
+
const headers = headerCheck.stdout;
|
|
266
|
+
if (headers.match(/www-authenticate:.*bearer/i)) {
|
|
267
|
+
result.authType = "Bearer/OAuth2";
|
|
268
|
+
}
|
|
269
|
+
else if (headers.match(/www-authenticate:.*basic/i)) {
|
|
270
|
+
result.authType = "Basic";
|
|
271
|
+
}
|
|
272
|
+
else if (headers.match(/www-authenticate:.*digest/i)) {
|
|
273
|
+
result.authType = "Digest";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (result.statusWithoutAuth >= 200 &&
|
|
278
|
+
result.statusWithoutAuth < 300) {
|
|
279
|
+
result.authRequired = false;
|
|
280
|
+
result.recommendations.push("WARNING: Endpoint accessible without authentication — enforce auth if this is a protected resource");
|
|
281
|
+
}
|
|
282
|
+
// Check for verbose error messages
|
|
283
|
+
const errorCheck = await runCommand("curl", ["-s", "-m", "5", target], 10_000);
|
|
284
|
+
if (errorCheck.exitCode === 0) {
|
|
285
|
+
const body = errorCheck.stdout;
|
|
286
|
+
if (body.includes("stack") && body.includes("at ")) {
|
|
287
|
+
result.verboseErrors = true;
|
|
288
|
+
result.errorDetails.push("Stack trace detected in response body");
|
|
289
|
+
}
|
|
290
|
+
if (body.match(/\/home\/|\/var\/|\/usr\/|\/opt\//)) {
|
|
291
|
+
result.verboseErrors = true;
|
|
292
|
+
result.errorDetails.push("Internal file paths detected in response body");
|
|
293
|
+
}
|
|
294
|
+
if (body.match(/sql|query|database|mysql|postgresql|mongodb/i) &&
|
|
295
|
+
body.match(/error|exception/i)) {
|
|
296
|
+
result.verboseErrors = true;
|
|
297
|
+
result.errorDetails.push("Database error details detected in response body");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (result.verboseErrors) {
|
|
301
|
+
result.recommendations.push("CRITICAL: Verbose error messages expose internal details — configure proper error handling");
|
|
302
|
+
}
|
|
303
|
+
// Check API key headers
|
|
304
|
+
const apiKeyCheck = await runCommand("curl", [
|
|
305
|
+
"-s",
|
|
306
|
+
"-o",
|
|
307
|
+
"/dev/null",
|
|
308
|
+
"-w",
|
|
309
|
+
"%{http_code}",
|
|
310
|
+
"-m",
|
|
311
|
+
"5",
|
|
312
|
+
"-H",
|
|
313
|
+
"X-API-Key: test",
|
|
314
|
+
target,
|
|
315
|
+
], 10_000);
|
|
316
|
+
if (apiKeyCheck.exitCode === 0) {
|
|
317
|
+
const apiKeyStatus = parseInt(apiKeyCheck.stdout.trim(), 10);
|
|
318
|
+
if (apiKeyStatus !== result.statusWithoutAuth &&
|
|
319
|
+
apiKeyStatus >= 200 &&
|
|
320
|
+
apiKeyStatus < 300) {
|
|
321
|
+
result.authType = "API Key";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
async function checkRateLimiting(target) {
|
|
327
|
+
const result = {
|
|
328
|
+
target,
|
|
329
|
+
rateLimitDetected: false,
|
|
330
|
+
headers: {},
|
|
331
|
+
got429: false,
|
|
332
|
+
requestCount: 10,
|
|
333
|
+
responseTimes: [],
|
|
334
|
+
recommendations: [],
|
|
335
|
+
};
|
|
336
|
+
const rateLimitHeaders = [
|
|
337
|
+
"x-ratelimit-limit",
|
|
338
|
+
"x-ratelimit-remaining",
|
|
339
|
+
"x-ratelimit-reset",
|
|
340
|
+
"retry-after",
|
|
341
|
+
];
|
|
342
|
+
// Send 10 rapid requests
|
|
343
|
+
for (let i = 0; i < 10; i++) {
|
|
344
|
+
const startTime = Date.now();
|
|
345
|
+
const check = await runCommand("curl", [
|
|
346
|
+
"-sI",
|
|
347
|
+
"-m",
|
|
348
|
+
"5",
|
|
349
|
+
"-o",
|
|
350
|
+
"/dev/null",
|
|
351
|
+
"-w",
|
|
352
|
+
"%{http_code}\n%{time_total}",
|
|
353
|
+
target,
|
|
354
|
+
], 10_000);
|
|
355
|
+
const elapsed = Date.now() - startTime;
|
|
356
|
+
result.responseTimes.push(elapsed);
|
|
357
|
+
if (check.exitCode !== 0)
|
|
358
|
+
continue;
|
|
359
|
+
const outputParts = check.stdout.trim().split("\n");
|
|
360
|
+
const statusCode = parseInt(outputParts[0], 10);
|
|
361
|
+
if (statusCode === 429) {
|
|
362
|
+
result.got429 = true;
|
|
363
|
+
result.rateLimitDetected = true;
|
|
364
|
+
}
|
|
365
|
+
// Check headers on first request for rate limit headers
|
|
366
|
+
if (i === 0) {
|
|
367
|
+
const headerCheck = await runCommand("curl", ["-sI", "-m", "5", target], 10_000);
|
|
368
|
+
if (headerCheck.exitCode === 0) {
|
|
369
|
+
const headerLines = headerCheck.stdout.split("\n");
|
|
370
|
+
for (const line of headerLines) {
|
|
371
|
+
const colonIdx = line.indexOf(":");
|
|
372
|
+
if (colonIdx < 0)
|
|
373
|
+
continue;
|
|
374
|
+
const headerName = line
|
|
375
|
+
.substring(0, colonIdx)
|
|
376
|
+
.trim()
|
|
377
|
+
.toLowerCase();
|
|
378
|
+
const headerValue = line.substring(colonIdx + 1).trim();
|
|
379
|
+
if (rateLimitHeaders.includes(headerName)) {
|
|
380
|
+
result.headers[headerName] = headerValue;
|
|
381
|
+
result.rateLimitDetected = true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!result.rateLimitDetected) {
|
|
388
|
+
result.recommendations.push("WARNING: No rate limiting detected — implement rate limiting to prevent abuse");
|
|
389
|
+
result.recommendations.push("Consider using headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset");
|
|
390
|
+
}
|
|
391
|
+
if (result.got429) {
|
|
392
|
+
result.recommendations.push("Rate limiting is active — 429 responses detected");
|
|
393
|
+
}
|
|
394
|
+
// Analyze response time patterns
|
|
395
|
+
if (result.responseTimes.length >= 2) {
|
|
396
|
+
const lastTime = result.responseTimes[result.responseTimes.length - 1];
|
|
397
|
+
const firstTime = result.responseTimes[0];
|
|
398
|
+
if (lastTime > firstTime * 3 && lastTime > 1000) {
|
|
399
|
+
result.recommendations.push("Possible throttling detected — response times increasing significantly");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
async function tlsVerify(target) {
|
|
405
|
+
const { host, port } = parseHostPort(target);
|
|
406
|
+
const result = {
|
|
407
|
+
target,
|
|
408
|
+
host,
|
|
409
|
+
port,
|
|
410
|
+
certificateValid: false,
|
|
411
|
+
certSubject: "unknown",
|
|
412
|
+
certIssuer: "unknown",
|
|
413
|
+
certExpiry: "unknown",
|
|
414
|
+
deprecatedProtocols: [],
|
|
415
|
+
hstsEnabled: false,
|
|
416
|
+
hstsValue: "",
|
|
417
|
+
securityGrade: "F",
|
|
418
|
+
recommendations: [],
|
|
419
|
+
};
|
|
420
|
+
// Check certificate details
|
|
421
|
+
const certCheck = await runCommand("openssl", ["s_client", "-connect", `${host}:${port}`, "-servername", host], 10_000);
|
|
422
|
+
if (certCheck.exitCode === 0 ||
|
|
423
|
+
certCheck.stdout.includes("BEGIN CERTIFICATE")) {
|
|
424
|
+
result.certificateValid = true;
|
|
425
|
+
// Parse subject
|
|
426
|
+
const subjectMatch = certCheck.stdout.match(/subject=(.+)/);
|
|
427
|
+
if (subjectMatch) {
|
|
428
|
+
result.certSubject = subjectMatch[1].trim();
|
|
429
|
+
}
|
|
430
|
+
// Parse issuer
|
|
431
|
+
const issuerMatch = certCheck.stdout.match(/issuer=(.+)/);
|
|
432
|
+
if (issuerMatch) {
|
|
433
|
+
result.certIssuer = issuerMatch[1].trim();
|
|
434
|
+
}
|
|
435
|
+
// Parse expiry
|
|
436
|
+
const expiryMatch = certCheck.stdout.match(/notAfter=(.+)/);
|
|
437
|
+
if (expiryMatch) {
|
|
438
|
+
result.certExpiry = expiryMatch[1].trim();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
result.recommendations.push("CRITICAL: Cannot establish TLS connection — check if TLS is configured");
|
|
443
|
+
result.securityGrade = "F";
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
// Check for deprecated TLS 1.0
|
|
447
|
+
const tls10Check = await runCommand("openssl", ["s_client", "-connect", `${host}:${port}`, "-tls1"], 10_000);
|
|
448
|
+
if (tls10Check.exitCode === 0 &&
|
|
449
|
+
!tls10Check.stderr.includes("no protocols available")) {
|
|
450
|
+
result.deprecatedProtocols.push("TLSv1.0");
|
|
451
|
+
result.recommendations.push("WARNING: TLSv1.0 is supported — disable this deprecated protocol");
|
|
452
|
+
}
|
|
453
|
+
// Check for deprecated TLS 1.1
|
|
454
|
+
const tls11Check = await runCommand("openssl", ["s_client", "-connect", `${host}:${port}`, "-tls1_1"], 10_000);
|
|
455
|
+
if (tls11Check.exitCode === 0 &&
|
|
456
|
+
!tls11Check.stderr.includes("no protocols available")) {
|
|
457
|
+
result.deprecatedProtocols.push("TLSv1.1");
|
|
458
|
+
result.recommendations.push("WARNING: TLSv1.1 is supported — disable this deprecated protocol");
|
|
459
|
+
}
|
|
460
|
+
// Check HSTS header
|
|
461
|
+
const hstsUrl = target.startsWith("https://") ? target : `https://${host}`;
|
|
462
|
+
const hstsCheck = await runCommand("curl", ["-sI", "-m", "5", hstsUrl], 10_000);
|
|
463
|
+
if (hstsCheck.exitCode === 0) {
|
|
464
|
+
const hstsMatch = hstsCheck.stdout.match(/strict-transport-security:\s*(.+)/i);
|
|
465
|
+
if (hstsMatch) {
|
|
466
|
+
result.hstsEnabled = true;
|
|
467
|
+
result.hstsValue = hstsMatch[1].trim();
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
result.recommendations.push("WARNING: HSTS header not found — enable Strict-Transport-Security");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Calculate security grade
|
|
474
|
+
let score = 100;
|
|
475
|
+
if (result.deprecatedProtocols.length > 0)
|
|
476
|
+
score -= 30 * result.deprecatedProtocols.length;
|
|
477
|
+
if (!result.hstsEnabled)
|
|
478
|
+
score -= 20;
|
|
479
|
+
if (!result.certificateValid)
|
|
480
|
+
score -= 50;
|
|
481
|
+
if (score >= 90)
|
|
482
|
+
result.securityGrade = "A";
|
|
483
|
+
else if (score >= 80)
|
|
484
|
+
result.securityGrade = "B";
|
|
485
|
+
else if (score >= 60)
|
|
486
|
+
result.securityGrade = "C";
|
|
487
|
+
else if (score >= 40)
|
|
488
|
+
result.securityGrade = "D";
|
|
489
|
+
else
|
|
490
|
+
result.securityGrade = "F";
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
async function corsCheck(target) {
|
|
494
|
+
const result = {
|
|
495
|
+
target,
|
|
496
|
+
corsEnabled: false,
|
|
497
|
+
allowOrigin: "",
|
|
498
|
+
allowCredentials: false,
|
|
499
|
+
allowMethods: "",
|
|
500
|
+
wildcardOrigin: false,
|
|
501
|
+
originReflection: false,
|
|
502
|
+
criticalIssues: [],
|
|
503
|
+
recommendations: [],
|
|
504
|
+
};
|
|
505
|
+
// Send request with evil origin
|
|
506
|
+
const evilOriginCheck = await runCommand("curl", ["-sI", "-m", "5", "-H", "Origin: https://evil.com", target], 10_000);
|
|
507
|
+
if (evilOriginCheck.exitCode !== 0) {
|
|
508
|
+
result.recommendations.push("Target is not reachable — check URL and connectivity");
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
const headers = evilOriginCheck.stdout;
|
|
512
|
+
// Check Access-Control-Allow-Origin
|
|
513
|
+
const acaoMatch = headers.match(/access-control-allow-origin:\s*(.+)/i);
|
|
514
|
+
if (acaoMatch) {
|
|
515
|
+
result.corsEnabled = true;
|
|
516
|
+
result.allowOrigin = acaoMatch[1].trim();
|
|
517
|
+
if (result.allowOrigin === "*") {
|
|
518
|
+
result.wildcardOrigin = true;
|
|
519
|
+
result.recommendations.push("WARNING: Access-Control-Allow-Origin is set to * (wildcard) — restrict to specific origins");
|
|
520
|
+
}
|
|
521
|
+
if (result.allowOrigin === "https://evil.com") {
|
|
522
|
+
result.originReflection = true;
|
|
523
|
+
result.criticalIssues.push("CRITICAL: Origin reflection detected — server reflects any origin in ACAO header");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Check Access-Control-Allow-Credentials
|
|
527
|
+
const acacMatch = headers.match(/access-control-allow-credentials:\s*(.+)/i);
|
|
528
|
+
if (acacMatch) {
|
|
529
|
+
result.allowCredentials =
|
|
530
|
+
acacMatch[1].trim().toLowerCase() === "true";
|
|
531
|
+
if (result.allowCredentials && result.wildcardOrigin) {
|
|
532
|
+
result.criticalIssues.push("CRITICAL: Credentials allowed with wildcard origin — this is a severe security misconfiguration");
|
|
533
|
+
}
|
|
534
|
+
if (result.allowCredentials && result.originReflection) {
|
|
535
|
+
result.criticalIssues.push("CRITICAL: Credentials allowed with origin reflection — any site can steal authenticated data");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Check Access-Control-Allow-Methods
|
|
539
|
+
const acamMatch = headers.match(/access-control-allow-methods:\s*(.+)/i);
|
|
540
|
+
if (acamMatch) {
|
|
541
|
+
result.allowMethods = acamMatch[1].trim();
|
|
542
|
+
const dangerousMethods = ["DELETE", "PUT", "PATCH"];
|
|
543
|
+
for (const method of dangerousMethods) {
|
|
544
|
+
if (result.allowMethods.toUpperCase().includes(method)) {
|
|
545
|
+
result.recommendations.push(`WARNING: ${method} method allowed in CORS — ensure this is intentional`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Test with another origin to check for reflection
|
|
550
|
+
if (!result.originReflection) {
|
|
551
|
+
const anotherOriginCheck = await runCommand("curl", [
|
|
552
|
+
"-sI",
|
|
553
|
+
"-m",
|
|
554
|
+
"5",
|
|
555
|
+
"-H",
|
|
556
|
+
"Origin: https://attacker.example.com",
|
|
557
|
+
target,
|
|
558
|
+
], 10_000);
|
|
559
|
+
if (anotherOriginCheck.exitCode === 0) {
|
|
560
|
+
const anotherAcao = anotherOriginCheck.stdout.match(/access-control-allow-origin:\s*(.+)/i);
|
|
561
|
+
if (anotherAcao &&
|
|
562
|
+
anotherAcao[1].trim() === "https://attacker.example.com") {
|
|
563
|
+
result.originReflection = true;
|
|
564
|
+
result.criticalIssues.push("CRITICAL: Origin reflection confirmed — server reflects arbitrary origins");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!result.corsEnabled) {
|
|
569
|
+
result.recommendations.push("No CORS headers detected — CORS is not configured or not applicable");
|
|
570
|
+
}
|
|
571
|
+
if (result.criticalIssues.length === 0 &&
|
|
572
|
+
result.corsEnabled &&
|
|
573
|
+
!result.wildcardOrigin) {
|
|
574
|
+
result.recommendations.push("CORS configuration appears secure");
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
// ── Registration entry point ───────────────────────────────────────────────
|
|
579
|
+
export function registerApiSecurityTools(server) {
|
|
580
|
+
server.tool("api_security", "API security: discover local APIs, audit authentication, check rate limiting, verify TLS configuration, and analyze CORS policies.", {
|
|
581
|
+
action: z
|
|
582
|
+
.enum([
|
|
583
|
+
"scan_local_apis",
|
|
584
|
+
"audit_auth",
|
|
585
|
+
"check_rate_limiting",
|
|
586
|
+
"tls_verify",
|
|
587
|
+
"cors_check",
|
|
588
|
+
])
|
|
589
|
+
.describe("Action: scan_local_apis=discover local API services, audit_auth=audit authentication config, check_rate_limiting=test rate limits, tls_verify=verify TLS configuration, cors_check=check CORS policy"),
|
|
590
|
+
target: z
|
|
591
|
+
.string()
|
|
592
|
+
.optional()
|
|
593
|
+
.describe("URL or host:port to scan (default: http://localhost)"),
|
|
594
|
+
port_range: z
|
|
595
|
+
.string()
|
|
596
|
+
.optional()
|
|
597
|
+
.default(DEFAULT_PORT_RANGE)
|
|
598
|
+
.describe("Port range to scan for APIs (comma-separated, used with scan_local_apis)"),
|
|
599
|
+
output_format: z
|
|
600
|
+
.enum(["text", "json"])
|
|
601
|
+
.optional()
|
|
602
|
+
.default("text")
|
|
603
|
+
.describe("Output format: text or json (default: text)"),
|
|
604
|
+
}, async (params) => {
|
|
605
|
+
const { action } = params;
|
|
606
|
+
const outputFormat = params.output_format ?? "text";
|
|
607
|
+
const defaultTarget = "http://localhost";
|
|
608
|
+
switch (action) {
|
|
609
|
+
// ── scan_local_apis ────────────────────────────────────────────
|
|
610
|
+
case "scan_local_apis": {
|
|
611
|
+
try {
|
|
612
|
+
const portRange = params.port_range ?? DEFAULT_PORT_RANGE;
|
|
613
|
+
const scanResult = await scanLocalApis(portRange);
|
|
614
|
+
const output = {
|
|
615
|
+
action: "scan_local_apis",
|
|
616
|
+
listeningPorts: scanResult.listeningPorts,
|
|
617
|
+
discoveredApis: scanResult.discoveredApis,
|
|
618
|
+
totalDiscovered: scanResult.totalDiscovered,
|
|
619
|
+
recommendations: scanResult.recommendations,
|
|
620
|
+
};
|
|
621
|
+
if (outputFormat === "json") {
|
|
622
|
+
return { content: [formatToolOutput(output)] };
|
|
623
|
+
}
|
|
624
|
+
let text = "API Security — Local API Discovery\n\n";
|
|
625
|
+
text += `Listening Ports: ${scanResult.listeningPorts.length > 0 ? scanResult.listeningPorts.join(", ") : "none detected"}\n`;
|
|
626
|
+
text += `Scanned Port Range: ${portRange}\n\n`;
|
|
627
|
+
if (scanResult.discoveredApis.length > 0) {
|
|
628
|
+
text += `Discovered APIs (${scanResult.totalDiscovered}):\n`;
|
|
629
|
+
for (const api of scanResult.discoveredApis) {
|
|
630
|
+
text += ` • Port ${api.port}: HTTP ${api.httpStatus} — Framework: ${api.frameworkGuess}\n`;
|
|
631
|
+
if (api.apiPaths.length > 0) {
|
|
632
|
+
text += ` Active paths: ${api.apiPaths.join(", ")}\n`;
|
|
633
|
+
}
|
|
634
|
+
if (api.docsUrl) {
|
|
635
|
+
text += ` API Docs: ${api.docsUrl}\n`;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
text += "No API services discovered\n";
|
|
641
|
+
}
|
|
642
|
+
if (scanResult.recommendations.length > 0) {
|
|
643
|
+
text += `\nRecommendations:\n`;
|
|
644
|
+
for (const rec of scanResult.recommendations) {
|
|
645
|
+
text += ` • ${rec}\n`;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return { content: [createTextContent(text)] };
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
652
|
+
return {
|
|
653
|
+
content: [
|
|
654
|
+
createErrorContent(`scan_local_apis failed: ${msg}`),
|
|
655
|
+
],
|
|
656
|
+
isError: true,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// ── audit_auth ────────────────────────────────────────────────
|
|
661
|
+
case "audit_auth": {
|
|
662
|
+
try {
|
|
663
|
+
const target = params.target
|
|
664
|
+
? validateTarget(params.target)
|
|
665
|
+
: defaultTarget;
|
|
666
|
+
if (!target) {
|
|
667
|
+
return {
|
|
668
|
+
content: [createErrorContent("Invalid target URL")],
|
|
669
|
+
isError: true,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const authResult = await auditAuth(target);
|
|
673
|
+
const output = {
|
|
674
|
+
action: "audit_auth",
|
|
675
|
+
target: authResult.target,
|
|
676
|
+
accessible: authResult.accessible,
|
|
677
|
+
statusWithoutAuth: authResult.statusWithoutAuth,
|
|
678
|
+
statusWithAuth: authResult.statusWithAuth,
|
|
679
|
+
authRequired: authResult.authRequired,
|
|
680
|
+
authType: authResult.authType,
|
|
681
|
+
verboseErrors: authResult.verboseErrors,
|
|
682
|
+
errorDetails: authResult.errorDetails,
|
|
683
|
+
recommendations: authResult.recommendations,
|
|
684
|
+
};
|
|
685
|
+
if (outputFormat === "json") {
|
|
686
|
+
return { content: [formatToolOutput(output)] };
|
|
687
|
+
}
|
|
688
|
+
let text = "API Security — Authentication Audit\n\n";
|
|
689
|
+
text += `Target: ${authResult.target}\n`;
|
|
690
|
+
text += `Accessible: ${authResult.accessible ? "yes" : "no"}\n`;
|
|
691
|
+
text += `Auth Required: ${authResult.authRequired ? "YES" : "NO"}\n`;
|
|
692
|
+
text += `Auth Type: ${authResult.authType}\n`;
|
|
693
|
+
text += `Status without auth: ${authResult.statusWithoutAuth}\n`;
|
|
694
|
+
text += `Status with auth: ${authResult.statusWithAuth}\n`;
|
|
695
|
+
text += `Verbose Errors: ${authResult.verboseErrors ? "YES ⚠" : "no"}\n`;
|
|
696
|
+
if (authResult.errorDetails.length > 0) {
|
|
697
|
+
text += `\nError Details:\n`;
|
|
698
|
+
for (const detail of authResult.errorDetails) {
|
|
699
|
+
text += ` • ${detail}\n`;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (authResult.recommendations.length > 0) {
|
|
703
|
+
text += `\nRecommendations:\n`;
|
|
704
|
+
for (const rec of authResult.recommendations) {
|
|
705
|
+
text += ` • ${rec}\n`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return { content: [createTextContent(text)] };
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
712
|
+
return {
|
|
713
|
+
content: [
|
|
714
|
+
createErrorContent(`audit_auth failed: ${msg}`),
|
|
715
|
+
],
|
|
716
|
+
isError: true,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// ── check_rate_limiting ────────────────────────────────────────
|
|
721
|
+
case "check_rate_limiting": {
|
|
722
|
+
try {
|
|
723
|
+
const target = params.target
|
|
724
|
+
? validateTarget(params.target)
|
|
725
|
+
: defaultTarget;
|
|
726
|
+
if (!target) {
|
|
727
|
+
return {
|
|
728
|
+
content: [createErrorContent("Invalid target URL")],
|
|
729
|
+
isError: true,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const rateResult = await checkRateLimiting(target);
|
|
733
|
+
const output = {
|
|
734
|
+
action: "check_rate_limiting",
|
|
735
|
+
target: rateResult.target,
|
|
736
|
+
rateLimitDetected: rateResult.rateLimitDetected,
|
|
737
|
+
headers: rateResult.headers,
|
|
738
|
+
got429: rateResult.got429,
|
|
739
|
+
requestCount: rateResult.requestCount,
|
|
740
|
+
recommendations: rateResult.recommendations,
|
|
741
|
+
};
|
|
742
|
+
if (outputFormat === "json") {
|
|
743
|
+
return { content: [formatToolOutput(output)] };
|
|
744
|
+
}
|
|
745
|
+
let text = "API Security — Rate Limiting Check\n\n";
|
|
746
|
+
text += `Target: ${rateResult.target}\n`;
|
|
747
|
+
text += `Rate Limiting Detected: ${rateResult.rateLimitDetected ? "YES" : "NO"}\n`;
|
|
748
|
+
text += `429 Responses: ${rateResult.got429 ? "YES" : "no"}\n`;
|
|
749
|
+
text += `Requests Sent: ${rateResult.requestCount}\n`;
|
|
750
|
+
if (Object.keys(rateResult.headers).length > 0) {
|
|
751
|
+
text += `\nRate Limit Headers:\n`;
|
|
752
|
+
for (const [header, value] of Object.entries(rateResult.headers)) {
|
|
753
|
+
text += ` • ${header}: ${value}\n`;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (rateResult.recommendations.length > 0) {
|
|
757
|
+
text += `\nRecommendations:\n`;
|
|
758
|
+
for (const rec of rateResult.recommendations) {
|
|
759
|
+
text += ` • ${rec}\n`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return { content: [createTextContent(text)] };
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
766
|
+
return {
|
|
767
|
+
content: [
|
|
768
|
+
createErrorContent(`check_rate_limiting failed: ${msg}`),
|
|
769
|
+
],
|
|
770
|
+
isError: true,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// ── tls_verify ────────────────────────────────────────────────
|
|
775
|
+
case "tls_verify": {
|
|
776
|
+
try {
|
|
777
|
+
const target = params.target
|
|
778
|
+
? validateTarget(params.target)
|
|
779
|
+
: "https://localhost";
|
|
780
|
+
if (!target) {
|
|
781
|
+
return {
|
|
782
|
+
content: [createErrorContent("Invalid target URL")],
|
|
783
|
+
isError: true,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
const tlsResult = await tlsVerify(target);
|
|
787
|
+
const output = {
|
|
788
|
+
action: "tls_verify",
|
|
789
|
+
target: tlsResult.target,
|
|
790
|
+
host: tlsResult.host,
|
|
791
|
+
port: tlsResult.port,
|
|
792
|
+
certificateValid: tlsResult.certificateValid,
|
|
793
|
+
certSubject: tlsResult.certSubject,
|
|
794
|
+
certIssuer: tlsResult.certIssuer,
|
|
795
|
+
certExpiry: tlsResult.certExpiry,
|
|
796
|
+
deprecatedProtocols: tlsResult.deprecatedProtocols,
|
|
797
|
+
hstsEnabled: tlsResult.hstsEnabled,
|
|
798
|
+
hstsValue: tlsResult.hstsValue,
|
|
799
|
+
securityGrade: tlsResult.securityGrade,
|
|
800
|
+
recommendations: tlsResult.recommendations,
|
|
801
|
+
};
|
|
802
|
+
if (outputFormat === "json") {
|
|
803
|
+
return { content: [formatToolOutput(output)] };
|
|
804
|
+
}
|
|
805
|
+
let text = "API Security — TLS Verification\n\n";
|
|
806
|
+
text += `Target: ${tlsResult.host}:${tlsResult.port}\n`;
|
|
807
|
+
text += `Certificate Valid: ${tlsResult.certificateValid ? "YES" : "NO"}\n`;
|
|
808
|
+
text += `Subject: ${tlsResult.certSubject}\n`;
|
|
809
|
+
text += `Issuer: ${tlsResult.certIssuer}\n`;
|
|
810
|
+
text += `Expiry: ${tlsResult.certExpiry}\n`;
|
|
811
|
+
text += `Deprecated Protocols: ${tlsResult.deprecatedProtocols.length > 0 ? tlsResult.deprecatedProtocols.join(", ") : "none"}\n`;
|
|
812
|
+
text += `HSTS: ${tlsResult.hstsEnabled ? `enabled (${tlsResult.hstsValue})` : "NOT enabled"}\n`;
|
|
813
|
+
text += `Security Grade: ${tlsResult.securityGrade}\n`;
|
|
814
|
+
if (tlsResult.recommendations.length > 0) {
|
|
815
|
+
text += `\nRecommendations:\n`;
|
|
816
|
+
for (const rec of tlsResult.recommendations) {
|
|
817
|
+
text += ` • ${rec}\n`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return { content: [createTextContent(text)] };
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
824
|
+
return {
|
|
825
|
+
content: [
|
|
826
|
+
createErrorContent(`tls_verify failed: ${msg}`),
|
|
827
|
+
],
|
|
828
|
+
isError: true,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// ── cors_check ────────────────────────────────────────────────
|
|
833
|
+
case "cors_check": {
|
|
834
|
+
try {
|
|
835
|
+
const target = params.target
|
|
836
|
+
? validateTarget(params.target)
|
|
837
|
+
: defaultTarget;
|
|
838
|
+
if (!target) {
|
|
839
|
+
return {
|
|
840
|
+
content: [createErrorContent("Invalid target URL")],
|
|
841
|
+
isError: true,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const corsResult = await corsCheck(target);
|
|
845
|
+
const output = {
|
|
846
|
+
action: "cors_check",
|
|
847
|
+
target: corsResult.target,
|
|
848
|
+
corsEnabled: corsResult.corsEnabled,
|
|
849
|
+
allowOrigin: corsResult.allowOrigin,
|
|
850
|
+
allowCredentials: corsResult.allowCredentials,
|
|
851
|
+
allowMethods: corsResult.allowMethods,
|
|
852
|
+
wildcardOrigin: corsResult.wildcardOrigin,
|
|
853
|
+
originReflection: corsResult.originReflection,
|
|
854
|
+
criticalIssues: corsResult.criticalIssues,
|
|
855
|
+
recommendations: corsResult.recommendations,
|
|
856
|
+
};
|
|
857
|
+
if (outputFormat === "json") {
|
|
858
|
+
return { content: [formatToolOutput(output)] };
|
|
859
|
+
}
|
|
860
|
+
let text = "API Security — CORS Check\n\n";
|
|
861
|
+
text += `Target: ${corsResult.target}\n`;
|
|
862
|
+
text += `CORS Enabled: ${corsResult.corsEnabled ? "yes" : "no"}\n`;
|
|
863
|
+
if (corsResult.corsEnabled) {
|
|
864
|
+
text += `Allow-Origin: ${corsResult.allowOrigin}\n`;
|
|
865
|
+
text += `Allow-Credentials: ${corsResult.allowCredentials}\n`;
|
|
866
|
+
text += `Allow-Methods: ${corsResult.allowMethods || "not specified"}\n`;
|
|
867
|
+
text += `Wildcard Origin: ${corsResult.wildcardOrigin ? "YES ⚠" : "no"}\n`;
|
|
868
|
+
text += `Origin Reflection: ${corsResult.originReflection ? "YES ⚠" : "no"}\n`;
|
|
869
|
+
}
|
|
870
|
+
if (corsResult.criticalIssues.length > 0) {
|
|
871
|
+
text += `\nCritical Issues:\n`;
|
|
872
|
+
for (const issue of corsResult.criticalIssues) {
|
|
873
|
+
text += ` • ${issue}\n`;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (corsResult.recommendations.length > 0) {
|
|
877
|
+
text += `\nRecommendations:\n`;
|
|
878
|
+
for (const rec of corsResult.recommendations) {
|
|
879
|
+
text += ` • ${rec}\n`;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return { content: [createTextContent(text)] };
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
886
|
+
return {
|
|
887
|
+
content: [
|
|
888
|
+
createErrorContent(`cors_check failed: ${msg}`),
|
|
889
|
+
],
|
|
890
|
+
isError: true,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
default:
|
|
895
|
+
return {
|
|
896
|
+
content: [createErrorContent(`Unknown action: ${action}`)],
|
|
897
|
+
isError: true,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|