@authtrack/secura 0.1.1 → 0.1.4
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/bin/secura-mcp.mjs +62 -43
- package/package.json +2 -2
- package/src/config.mjs +33 -33
package/bin/secura-mcp.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* }
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { Server }
|
|
33
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
34
34
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
35
35
|
import {
|
|
36
36
|
CallToolRequestSchema,
|
|
@@ -39,11 +39,11 @@ import {
|
|
|
39
39
|
|
|
40
40
|
// ── Config ───────────────────────────────────────────────────────────────────
|
|
41
41
|
|
|
42
|
-
const DEFAULT_API = "https://
|
|
42
|
+
const DEFAULT_API = "https://52.91.8.40.nip.io/api/v1";
|
|
43
43
|
|
|
44
44
|
function resolveConfig() {
|
|
45
|
-
const api
|
|
46
|
-
const token = process.env.SECURA_TOKEN
|
|
45
|
+
const api = (process.env.SECURA_API || DEFAULT_API).replace(/\/+$/, "");
|
|
46
|
+
const token = process.env.SECURA_TOKEN || "";
|
|
47
47
|
return { api, token };
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -57,8 +57,8 @@ function authHeaders(token, extra = {}) {
|
|
|
57
57
|
|
|
58
58
|
async function* readSse(res) {
|
|
59
59
|
const decoder = new TextDecoder();
|
|
60
|
-
const reader
|
|
61
|
-
let buffer
|
|
60
|
+
const reader = res.body.getReader();
|
|
61
|
+
let buffer = "";
|
|
62
62
|
|
|
63
63
|
const emit = function* (frame) {
|
|
64
64
|
for (const line of frame.split("\n")) {
|
|
@@ -89,7 +89,7 @@ async function drainStream(url, token, headers, body) {
|
|
|
89
89
|
let res;
|
|
90
90
|
try {
|
|
91
91
|
res = await fetch(url, {
|
|
92
|
-
method:
|
|
92
|
+
method: "POST",
|
|
93
93
|
headers: authHeaders(token, headers),
|
|
94
94
|
body,
|
|
95
95
|
});
|
|
@@ -103,8 +103,8 @@ async function drainStream(url, token, headers, body) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const accumulated = {};
|
|
106
|
-
let
|
|
107
|
-
let
|
|
106
|
+
let scanId = null;
|
|
107
|
+
let lastError = null;
|
|
108
108
|
|
|
109
109
|
for await (const evt of readSse(res)) {
|
|
110
110
|
if (evt.event === "node_update" && evt.state) {
|
|
@@ -127,10 +127,10 @@ async function drainStream(url, token, headers, body) {
|
|
|
127
127
|
async function runStaticScan(args) {
|
|
128
128
|
const { api, token } = resolveConfig();
|
|
129
129
|
const body = JSON.stringify({
|
|
130
|
-
target_path:
|
|
131
|
-
include_codeql:
|
|
130
|
+
target_path: args.target_path,
|
|
131
|
+
include_codeql: args.include_codeql ?? true,
|
|
132
132
|
codeql_language: args.codeql_language ?? "",
|
|
133
|
-
max_triage:
|
|
133
|
+
max_triage: args.max_triage ?? 25,
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
const { finalState, scanId, error } = await drainStream(
|
|
@@ -148,7 +148,7 @@ async function runStaticScan(args) {
|
|
|
148
148
|
async function runRecon(args) {
|
|
149
149
|
const { api, token } = resolveConfig();
|
|
150
150
|
const body = JSON.stringify({
|
|
151
|
-
url:
|
|
151
|
+
url: args.url,
|
|
152
152
|
max_depth: args.max_depth ?? 2,
|
|
153
153
|
max_pages: args.max_pages ?? 30,
|
|
154
154
|
});
|
|
@@ -160,7 +160,7 @@ async function runRecon(args) {
|
|
|
160
160
|
|
|
161
161
|
if (error) throw new Error(error);
|
|
162
162
|
|
|
163
|
-
const report
|
|
163
|
+
const report = finalState.surface_report ?? {};
|
|
164
164
|
const endpoints = finalState.classified_endpoints ?? report.endpoints ?? [];
|
|
165
165
|
return formatReconReport(report, endpoints, scanId);
|
|
166
166
|
}
|
|
@@ -169,7 +169,7 @@ async function runRecon(args) {
|
|
|
169
169
|
async function runHeaderAudit(args) {
|
|
170
170
|
const { api, token } = resolveConfig();
|
|
171
171
|
const body = JSON.stringify({
|
|
172
|
-
url:
|
|
172
|
+
url: args.url,
|
|
173
173
|
max_depth: args.max_depth ?? 2,
|
|
174
174
|
max_pages: args.max_pages ?? 30,
|
|
175
175
|
});
|
|
@@ -181,8 +181,8 @@ async function runHeaderAudit(args) {
|
|
|
181
181
|
|
|
182
182
|
if (error) throw new Error(error);
|
|
183
183
|
|
|
184
|
-
const auditReport
|
|
185
|
-
const surfaceReport = finalState.surface_report
|
|
184
|
+
const auditReport = finalState.audit_report ?? {};
|
|
185
|
+
const surfaceReport = finalState.surface_report ?? {};
|
|
186
186
|
return formatHeaderReport(auditReport, surfaceReport, scanId);
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -212,10 +212,10 @@ function formatStaticReport(report, scanId) {
|
|
|
212
212
|
if (confirmed.length > 0) {
|
|
213
213
|
lines.push("### Confirmed Vulnerabilities", "");
|
|
214
214
|
confirmed.slice(0, 20).forEach((f, i) => {
|
|
215
|
-
const sev
|
|
216
|
-
const evidence = f.taint_path
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
const sev = (f.final_severity || f.severity || "info").toUpperCase();
|
|
216
|
+
const evidence = f.taint_path ? "taint path"
|
|
217
|
+
: f.multi_tool_confirmed ? "multi-tool"
|
|
218
|
+
: "single tool";
|
|
219
219
|
lines.push(
|
|
220
220
|
`**${i + 1}. [${sev}] ${f.category ?? f.rule_id}**`,
|
|
221
221
|
`- File: \`${f.file}:${f.line}\``,
|
|
@@ -296,77 +296,77 @@ function formatHeaderReport(audit, surface, scanId) {
|
|
|
296
296
|
|
|
297
297
|
const TOOLS = [
|
|
298
298
|
{
|
|
299
|
-
name:
|
|
299
|
+
name: "run_static_scan",
|
|
300
300
|
description: "Run SAST static analysis on a local code repository using Semgrep, Bearer, OSV-Scanner, Gitleaks and CodeQL. Returns confirmed vulnerabilities, false positives, and generated fixes.",
|
|
301
301
|
inputSchema: {
|
|
302
302
|
type: "object",
|
|
303
303
|
properties: {
|
|
304
304
|
target_path: {
|
|
305
|
-
type:
|
|
305
|
+
type: "string",
|
|
306
306
|
description: "Absolute or relative path to the local repository/folder to scan. Can also be a GitHub URL (https://github.com/owner/repo) or 'owner/repo' shorthand.",
|
|
307
307
|
},
|
|
308
308
|
include_codeql: {
|
|
309
|
-
type:
|
|
309
|
+
type: "boolean",
|
|
310
310
|
description: "Include CodeQL taint analysis (slower but finds deeper data-flow issues). Default: true.",
|
|
311
|
-
default:
|
|
311
|
+
default: true,
|
|
312
312
|
},
|
|
313
313
|
codeql_language: {
|
|
314
|
-
type:
|
|
314
|
+
type: "string",
|
|
315
315
|
description: "CodeQL extractor language (javascript, python, java, go, ruby, csharp, cpp). Leave empty to auto-detect.",
|
|
316
|
-
default:
|
|
316
|
+
default: "",
|
|
317
317
|
},
|
|
318
318
|
max_triage: {
|
|
319
|
-
type:
|
|
319
|
+
type: "number",
|
|
320
320
|
description: "Maximum number of merged findings to AI-triage. Default: 25.",
|
|
321
|
-
default:
|
|
321
|
+
default: 25,
|
|
322
322
|
},
|
|
323
323
|
},
|
|
324
324
|
required: ["target_path"],
|
|
325
325
|
},
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
|
-
name:
|
|
328
|
+
name: "run_recon",
|
|
329
329
|
description: "Crawl a web application and map its attack surface — discovering endpoints, classifying them (API, auth, static, etc.), and producing a surface report with an LLM-written summary.",
|
|
330
330
|
inputSchema: {
|
|
331
331
|
type: "object",
|
|
332
332
|
properties: {
|
|
333
333
|
url: {
|
|
334
|
-
type:
|
|
334
|
+
type: "string",
|
|
335
335
|
description: "The target URL to crawl (e.g. https://example.com).",
|
|
336
336
|
},
|
|
337
337
|
max_depth: {
|
|
338
|
-
type:
|
|
338
|
+
type: "number",
|
|
339
339
|
description: "Maximum crawl depth from the seed URL. Default: 2.",
|
|
340
|
-
default:
|
|
340
|
+
default: 2,
|
|
341
341
|
},
|
|
342
342
|
max_pages: {
|
|
343
|
-
type:
|
|
343
|
+
type: "number",
|
|
344
344
|
description: "Maximum number of pages to visit. Default: 30.",
|
|
345
|
-
default:
|
|
345
|
+
default: 30,
|
|
346
346
|
},
|
|
347
347
|
},
|
|
348
348
|
required: ["url"],
|
|
349
349
|
},
|
|
350
350
|
},
|
|
351
351
|
{
|
|
352
|
-
name:
|
|
352
|
+
name: "run_header_audit",
|
|
353
353
|
description: "Crawl a web application and audit its HTTP security headers and cookies against OWASP best practices (CSP, HSTS, X-Frame-Options, SameSite cookies, CORS, etc.).",
|
|
354
354
|
inputSchema: {
|
|
355
355
|
type: "object",
|
|
356
356
|
properties: {
|
|
357
357
|
url: {
|
|
358
|
-
type:
|
|
358
|
+
type: "string",
|
|
359
359
|
description: "The target URL to audit (e.g. https://example.com).",
|
|
360
360
|
},
|
|
361
361
|
max_depth: {
|
|
362
|
-
type:
|
|
362
|
+
type: "number",
|
|
363
363
|
description: "Maximum crawl depth. Default: 2.",
|
|
364
|
-
default:
|
|
364
|
+
default: 2,
|
|
365
365
|
},
|
|
366
366
|
max_pages: {
|
|
367
|
-
type:
|
|
367
|
+
type: "number",
|
|
368
368
|
description: "Maximum pages to audit. Default: 30.",
|
|
369
|
-
default:
|
|
369
|
+
default: 30,
|
|
370
370
|
},
|
|
371
371
|
},
|
|
372
372
|
required: ["url"],
|
|
@@ -388,8 +388,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
388
388
|
|
|
389
389
|
try {
|
|
390
390
|
switch (name) {
|
|
391
|
-
case "run_static_scan":
|
|
392
|
-
case "run_recon":
|
|
391
|
+
case "run_static_scan": return await runStaticScan(args);
|
|
392
|
+
case "run_recon": return await runRecon(args);
|
|
393
393
|
case "run_header_audit": return await runHeaderAudit(args);
|
|
394
394
|
default:
|
|
395
395
|
return {
|
|
@@ -409,4 +409,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
409
409
|
|
|
410
410
|
const transport = new StdioServerTransport();
|
|
411
411
|
await server.connect(transport);
|
|
412
|
+
|
|
413
|
+
// Print to stderr (not stdout — stdout is the MCP protocol wire)
|
|
414
|
+
process.stderr.write([
|
|
415
|
+
"",
|
|
416
|
+
" ╔══════════════════════════════════════════════╗",
|
|
417
|
+
" ║ SecuraAI MCP Server v0.1.1 ║",
|
|
418
|
+
" ║ Listening for MCP messages on stdin… ║",
|
|
419
|
+
" ╚══════════════════════════════════════════════╝",
|
|
420
|
+
"",
|
|
421
|
+
" ✓ 3 tools available: run_static_scan · run_recon · run_header_audit",
|
|
422
|
+
` ✓ Backend: ${resolveConfig().api}`,
|
|
423
|
+
"",
|
|
424
|
+
" NOTE: You don't run this manually.",
|
|
425
|
+
" Add it to your editor's MCP config and restart the editor.",
|
|
426
|
+
" The editor will spawn this process automatically.",
|
|
427
|
+
"",
|
|
428
|
+
" Press Ctrl+C to exit.",
|
|
429
|
+
"",
|
|
430
|
+
].join("\n"));
|
|
412
431
|
// Server runs until the editor closes stdin.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@authtrack/secura",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "SecuraAI security scanner CLI and MCP server — static analysis, recon, header audit and injection testing via AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,4 +51,4 @@
|
|
|
51
51
|
"secura": "node bin/secura.mjs",
|
|
52
52
|
"secura-mcp": "node bin/secura-mcp.mjs"
|
|
53
53
|
}
|
|
54
|
-
}
|
|
54
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve where the CLI talks to and how.
|
|
3
|
-
*
|
|
4
|
-
* The backend's scan router has no auth, so the token is optional; it is sent
|
|
5
|
-
* as a bearer header only when present, so this works unchanged if auth is
|
|
6
|
-
* added later.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Matches frontend/src/lib/api.ts (API_BASE) so the CLI and the web app default
|
|
10
|
-
// to the same backend.
|
|
11
|
-
const DEFAULT_API = "https://
|
|
12
|
-
|
|
13
|
-
export function resolveConfig(values) {
|
|
14
|
-
const raw = values.api || process.env.SECURA_API || DEFAULT_API;
|
|
15
|
-
const api = raw.replace(/\/+$/, ""); // trim trailing slashes
|
|
16
|
-
const token = values.token || process.env.SECURA_TOKEN || "";
|
|
17
|
-
return { api, token };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* True when the API base points at this machine. A local path target can then
|
|
22
|
-
* be scanned in place (sent as `target_path`); otherwise it must be uploaded.
|
|
23
|
-
*/
|
|
24
|
-
export function isLocalBackend(api) {
|
|
25
|
-
try {
|
|
26
|
-
const { hostname } = new URL(api);
|
|
27
|
-
return ["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"].includes(hostname);
|
|
28
|
-
} catch {
|
|
29
|
-
// An unparseable base is treated as local — the request will fail loudly
|
|
30
|
-
// either way, and this keeps the default (no upload) for odd inputs.
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Resolve where the CLI talks to and how.
|
|
3
|
+
*
|
|
4
|
+
* The backend's scan router has no auth, so the token is optional; it is sent
|
|
5
|
+
* as a bearer header only when present, so this works unchanged if auth is
|
|
6
|
+
* added later.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Matches frontend/src/lib/api.ts (API_BASE) so the CLI and the web app default
|
|
10
|
+
// to the same backend.
|
|
11
|
+
const DEFAULT_API = "https://52.91.8.40.nip.io/api/v1";
|
|
12
|
+
|
|
13
|
+
export function resolveConfig(values) {
|
|
14
|
+
const raw = values.api || process.env.SECURA_API || DEFAULT_API;
|
|
15
|
+
const api = raw.replace(/\/+$/, ""); // trim trailing slashes
|
|
16
|
+
const token = values.token || process.env.SECURA_TOKEN || "";
|
|
17
|
+
return { api, token };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* True when the API base points at this machine. A local path target can then
|
|
22
|
+
* be scanned in place (sent as `target_path`); otherwise it must be uploaded.
|
|
23
|
+
*/
|
|
24
|
+
export function isLocalBackend(api) {
|
|
25
|
+
try {
|
|
26
|
+
const { hostname } = new URL(api);
|
|
27
|
+
return ["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"].includes(hostname);
|
|
28
|
+
} catch {
|
|
29
|
+
// An unparseable base is treated as local — the request will fail loudly
|
|
30
|
+
// either way, and this keeps the default (no upload) for odd inputs.
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|