blitzstrike 2.4.5 → 2.4.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/CHANGELOG.md +24 -0
- package/dist/index.js +121 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to Blitz Strike are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [2.4.7] — 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added — deterministic arbitrary-file-read verification (no bare hypotheses)
|
|
11
|
+
|
|
12
|
+
- New `verifyFileRead` (STRIKE): verifies a CWE-22 / path-traversal / LFI
|
|
13
|
+
hypothesis by reading a marker file (`/etc/passwd`) vs a non-existent
|
|
14
|
+
negative-control path, then comparing. Confirmed only when the marker returns
|
|
15
|
+
file content and the control does not — never from reasoning alone.
|
|
16
|
+
- Supports raw POST body (`bodyRaw`) and named query/body param injection.
|
|
17
|
+
- New MCP tool `verify_file_read` + chain hints `verify_file_read` /
|
|
18
|
+
`path_traversal` / `lfi` / `file_read` route to it; `strike_verify` now
|
|
19
|
+
auto-routes file-read findings to the file-read verifier.
|
|
20
|
+
- A gated endpoint (e.g. identical 500 auth wall) now returns an explicit
|
|
21
|
+
`unconfirmed` verdict with reason instead of leaving a bare hypothesis.
|
|
22
|
+
|
|
23
|
+
## [2.4.6] — 2026-09-14
|
|
24
|
+
|
|
25
|
+
### Added — chrome-devtools-mcp auto-install (npx zero-install fallback)
|
|
26
|
+
|
|
27
|
+
- `drive_devtools` / `browser_devtools` now auto-fetch chrome-devtools-mcp via
|
|
28
|
+
`npx -y chrome-devtools-mcp@latest` when no global binary is present — no
|
|
29
|
+
manual `npm i -g` required. (burp-suite-mcp still can't be auto-installed:
|
|
30
|
+
it's a Java/GUI app that must be built + run manually.)
|
|
31
|
+
|
|
8
32
|
## [2.4.5] — 2026-09-14
|
|
9
33
|
|
|
10
34
|
### Added — responsible-disclosure header (X-HackerOne-Research)
|
package/dist/index.js
CHANGED
|
@@ -15300,7 +15300,7 @@ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync6, readFileSync
|
|
|
15300
15300
|
import { join as join16 } from "node:path";
|
|
15301
15301
|
import { homedir as homedir5 } from "node:os";
|
|
15302
15302
|
import { fileURLToPath as fileURLToPath9 } from "node:url";
|
|
15303
|
-
import { spawnSync as
|
|
15303
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
15304
15304
|
function dirExists(p) {
|
|
15305
15305
|
try {
|
|
15306
15306
|
return statSync4(p).isDirectory();
|
|
@@ -15351,14 +15351,14 @@ function syncData() {
|
|
|
15351
15351
|
copyDir(localTemplates, templatesPath);
|
|
15352
15352
|
templatesSynced = true;
|
|
15353
15353
|
}
|
|
15354
|
-
const gitOk =
|
|
15354
|
+
const gitOk = spawnSync3("git", ["--version"]).status === 0;
|
|
15355
15355
|
if (gitOk && (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0 || !dirExists(templatesPath) || countFiles(templatesPath) === 0)) {
|
|
15356
15356
|
const tmp = join16(DATA_ROOT2, ".tmp-clone");
|
|
15357
15357
|
if (dirExists(tmp))
|
|
15358
15358
|
rmSync4(tmp, { recursive: true, force: true });
|
|
15359
|
-
const r =
|
|
15359
|
+
const r = spawnSync3("git", ["clone", "--depth", "1", "--filter=blob:none", "--sparse", GITHUB_REPO, tmp], { timeout: 120000 });
|
|
15360
15360
|
if (r.status === 0) {
|
|
15361
|
-
const sparse =
|
|
15361
|
+
const sparse = spawnSync3("git", ["-C", tmp, "sparse-checkout", "set", "payloads", "templates"], { timeout: 120000 });
|
|
15362
15362
|
if (sparse.status === 0) {
|
|
15363
15363
|
if (!dirExists(payloadsPath) || countFiles(payloadsPath) === 0) {
|
|
15364
15364
|
copyDir(join16(tmp, "payloads"), payloadsPath);
|
|
@@ -15379,7 +15379,7 @@ function syncData() {
|
|
|
15379
15379
|
};
|
|
15380
15380
|
}
|
|
15381
15381
|
function copyDir(src, dst) {
|
|
15382
|
-
const r =
|
|
15382
|
+
const r = spawnSync3("cp", ["-r", src, dst], { timeout: 120000 });
|
|
15383
15383
|
if (r.status !== 0) {
|
|
15384
15384
|
manualCopy(src, dst);
|
|
15385
15385
|
}
|
|
@@ -15529,9 +15529,9 @@ import { readFileSync as readFileSync22, existsSync as existsSync10, writeFileSy
|
|
|
15529
15529
|
import { join as join19, dirname as dirname2 } from "node:path";
|
|
15530
15530
|
import { homedir as homedir6 } from "node:os";
|
|
15531
15531
|
import { fileURLToPath as fileURLToPath12 } from "node:url";
|
|
15532
|
-
import { spawnSync as
|
|
15532
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
15533
15533
|
function which(cmd) {
|
|
15534
|
-
return
|
|
15534
|
+
return spawnSync4(`command -v ${cmd}`, { shell: true }).status === 0;
|
|
15535
15535
|
}
|
|
15536
15536
|
function installedCount() {
|
|
15537
15537
|
const tools = loadTools();
|
|
@@ -31291,6 +31291,53 @@ ${controlResp.body}` }]
|
|
|
31291
31291
|
evidence
|
|
31292
31292
|
};
|
|
31293
31293
|
}
|
|
31294
|
+
function buildFileReadRequest(url, path, param, bodyRaw) {
|
|
31295
|
+
if (bodyRaw)
|
|
31296
|
+
return { url, data: path };
|
|
31297
|
+
return { url: inject(url, path, param ?? "path") };
|
|
31298
|
+
}
|
|
31299
|
+
async function verifyFileRead(input) {
|
|
31300
|
+
const method = input.method ?? "POST";
|
|
31301
|
+
const markerPath = input.markerPath ?? "/etc/passwd";
|
|
31302
|
+
const controlPath = `/bs-nonexistent-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
31303
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
31304
|
+
const markerReq = buildFileReadRequest(input.url, markerPath, input.param, input.bodyRaw);
|
|
31305
|
+
const controlReq = buildFileReadRequest(input.url, controlPath, input.param, input.bodyRaw);
|
|
31306
|
+
const markerResp = await httpRequest(markerReq.url, method, markerReq.data, input.headers, timeoutMs);
|
|
31307
|
+
const controlResp = await httpRequest(controlReq.url, method, controlReq.data, input.headers, timeoutMs);
|
|
31308
|
+
const passwdRe = /(^|\n)root:[x*!][^:]*:0:0:/m;
|
|
31309
|
+
const markerFile = passwdRe.test(markerResp.body);
|
|
31310
|
+
const controlFile = passwdRe.test(controlResp.body);
|
|
31311
|
+
let status;
|
|
31312
|
+
let reason;
|
|
31313
|
+
if (markerResp.status === 0 && controlResp.status === 0) {
|
|
31314
|
+
status = "blocked";
|
|
31315
|
+
reason = "both marker and control requests failed — target unreachable";
|
|
31316
|
+
} else if (markerFile && !controlFile) {
|
|
31317
|
+
status = "confirmed";
|
|
31318
|
+
reason = "marker file (/etc/passwd) content returned while the non-existent negative control did not — arbitrary file read confirmed";
|
|
31319
|
+
} else if (markerFile && controlFile) {
|
|
31320
|
+
status = "unconfirmed";
|
|
31321
|
+
reason = "marker and negative control both returned passwd-like content — indistinguishable from a fixed response";
|
|
31322
|
+
} else if (markerResp.status === controlResp.status && markerResp.body === controlResp.body) {
|
|
31323
|
+
status = "unconfirmed";
|
|
31324
|
+
reason = `marker and control returned identical responses (HTTP ${markerResp.status}) — likely an auth/session gate; file read not reachable without credentials`;
|
|
31325
|
+
} else {
|
|
31326
|
+
status = "unconfirmed";
|
|
31327
|
+
reason = "marker file content not returned — arbitrary file read could not be demonstrated live";
|
|
31328
|
+
}
|
|
31329
|
+
return {
|
|
31330
|
+
status,
|
|
31331
|
+
file_read: markerFile && !controlFile,
|
|
31332
|
+
marker_file_content: markerFile,
|
|
31333
|
+
control_file_content: controlFile,
|
|
31334
|
+
marker_status: markerResp.status,
|
|
31335
|
+
control_status: controlResp.status,
|
|
31336
|
+
marker_preview: redactSecrets(markerResp.body.slice(0, 300)),
|
|
31337
|
+
control_preview: redactSecrets(controlResp.body.slice(0, 300)),
|
|
31338
|
+
reason
|
|
31339
|
+
};
|
|
31340
|
+
}
|
|
31294
31341
|
function resolveFinding(finding, verdict) {
|
|
31295
31342
|
let f = finding;
|
|
31296
31343
|
if (f.status === "hypothesis") {
|
|
@@ -31587,7 +31634,7 @@ async function browserValidate(check, target, payload) {
|
|
|
31587
31634
|
}
|
|
31588
31635
|
|
|
31589
31636
|
// src/devtools.ts
|
|
31590
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
31637
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
31591
31638
|
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
31592
31639
|
import { join as join7 } from "node:path";
|
|
31593
31640
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -31678,11 +31725,14 @@ function findChromium() {
|
|
|
31678
31725
|
}
|
|
31679
31726
|
return null;
|
|
31680
31727
|
}
|
|
31681
|
-
function
|
|
31728
|
+
function chromeDevtoolsCommand() {
|
|
31682
31729
|
for (const c of ["/usr/bin/chrome-devtools-mcp", "/usr/local/bin/chrome-devtools-mcp"]) {
|
|
31683
31730
|
if (existsSync4(c))
|
|
31684
31731
|
return { command: c, args: [] };
|
|
31685
31732
|
}
|
|
31733
|
+
const npx = spawnSync2("npx", ["--version"], { timeout: 8000 });
|
|
31734
|
+
if (npx.error === undefined)
|
|
31735
|
+
return { command: "npx", args: ["-y", "chrome-devtools-mcp@latest"] };
|
|
31686
31736
|
return null;
|
|
31687
31737
|
}
|
|
31688
31738
|
function parsePageId(text) {
|
|
@@ -31690,17 +31740,12 @@ function parsePageId(text) {
|
|
|
31690
31740
|
return m ? parseInt(m[1], 10) : 1;
|
|
31691
31741
|
}
|
|
31692
31742
|
async function driveChromeDevtools(target, timeoutMs = 25000) {
|
|
31693
|
-
const bin =
|
|
31743
|
+
const bin = chromeDevtoolsCommand();
|
|
31694
31744
|
if (!bin) {
|
|
31695
|
-
return { ok: false, error: "chrome-devtools-mcp not
|
|
31745
|
+
return { ok: false, error: "chrome-devtools-mcp not available (no global binary and no npx) — install Node/npm first" };
|
|
31696
31746
|
}
|
|
31697
31747
|
const chrome = findChromium();
|
|
31698
|
-
const args = [
|
|
31699
|
-
"--headless",
|
|
31700
|
-
"--isolated",
|
|
31701
|
-
"--chromeArg=--no-sandbox",
|
|
31702
|
-
"--chromeArg=--disable-gpu"
|
|
31703
|
-
];
|
|
31748
|
+
const args = [...bin.args, "--headless", "--isolated", "--chromeArg=--no-sandbox", "--chromeArg=--disable-gpu"];
|
|
31704
31749
|
if (chrome)
|
|
31705
31750
|
args.push(`--executablePath=${chrome}`);
|
|
31706
31751
|
const client = mcpClient(bin.command, args);
|
|
@@ -31828,6 +31873,10 @@ async function driveBurpMCP(baseUrl = DEFAULT_BURP_URL, timeoutMs = 1e4) {
|
|
|
31828
31873
|
// src/chain-executor.ts
|
|
31829
31874
|
var BLITZ_FINDING_TOOLS = {
|
|
31830
31875
|
strike_verify: verifyFinding,
|
|
31876
|
+
verify_file_read: fileReadFinding,
|
|
31877
|
+
path_traversal: fileReadFinding,
|
|
31878
|
+
lfi: fileReadFinding,
|
|
31879
|
+
file_read: fileReadFinding,
|
|
31831
31880
|
crack_hash: crackFinding,
|
|
31832
31881
|
hashcat: crackFinding,
|
|
31833
31882
|
hashid: crackFinding,
|
|
@@ -31861,9 +31910,15 @@ function findingText(f) {
|
|
|
31861
31910
|
function findingUrl(f) {
|
|
31862
31911
|
return (f.target?.host ?? "") + (f.target?.endpoint ?? "");
|
|
31863
31912
|
}
|
|
31913
|
+
function isFileReadFinding(f) {
|
|
31914
|
+
const text = `${f.classification?.cwe ?? ""} ${f.classification?.cwe_name ?? ""} ${f.sink?.type ?? ""} ${f.chain?.id ?? ""} ${f.chain?.name ?? ""} ${f.title ?? ""}`.toLowerCase();
|
|
31915
|
+
return /(cwe-?22|path.?traversal|arbitrary.?file|file.?read|directory.?traversal|lfi|\btraversal\b)/.test(text);
|
|
31916
|
+
}
|
|
31864
31917
|
async function verifyFinding(f) {
|
|
31865
31918
|
const url = findingUrl(f);
|
|
31866
31919
|
const param = f.source?.name ?? "q";
|
|
31920
|
+
if (isFileReadFinding(f))
|
|
31921
|
+
return fileReadFinding(f);
|
|
31867
31922
|
const v = await strikeVerify({ url, method: "GET", param });
|
|
31868
31923
|
return {
|
|
31869
31924
|
status: v.status,
|
|
@@ -31873,6 +31928,16 @@ async function verifyFinding(f) {
|
|
|
31873
31928
|
leaked_sinks: v.leaked_sinks ?? []
|
|
31874
31929
|
};
|
|
31875
31930
|
}
|
|
31931
|
+
async function fileReadFinding(f) {
|
|
31932
|
+
const url = findingUrl(f);
|
|
31933
|
+
if (!url)
|
|
31934
|
+
return { executed: false, note: "no target URL on finding" };
|
|
31935
|
+
const srcType = (f.source?.type ?? "").toLowerCase();
|
|
31936
|
+
const bodyRaw = /(body|post|raw)/.test(srcType);
|
|
31937
|
+
const method = bodyRaw ? "POST" : "GET";
|
|
31938
|
+
const verdict = await verifyFileRead({ url, method, bodyRaw, param: f.source?.name });
|
|
31939
|
+
return { verify_kind: "file_read", ...verdict };
|
|
31940
|
+
}
|
|
31876
31941
|
async function crackFinding(f) {
|
|
31877
31942
|
const text = findingText(f);
|
|
31878
31943
|
const hashRe = /(\$2[aby]\$\d+\$[./A-Za-z0-9]{53}|[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64})/g;
|
|
@@ -56281,6 +56346,45 @@ function createServer() {
|
|
|
56281
56346
|
}]
|
|
56282
56347
|
};
|
|
56283
56348
|
});
|
|
56349
|
+
server.registerTool("verify_file_read", {
|
|
56350
|
+
title: "Verify arbitrary file read / path traversal (marker file vs negative control)",
|
|
56351
|
+
description: "STRIKE: deterministic verification of an arbitrary-file-read / path-traversal hypothesis (CWE-22). Reads a marker file (/etc/passwd) vs a non-existent negative-control path and compares: confirmed (marker returns file content, control does not) / unconfirmed (indistinguishable or gated by auth) / blocked (unreachable). Supports raw POST body (bodyRaw=true) or a named query/body param. Never leaves a bare hypothesis — always returns a verdict + reason.",
|
|
56352
|
+
inputSchema: {
|
|
56353
|
+
url: string2().describe("Target URL (e.g. the file-read endpoint)"),
|
|
56354
|
+
method: _enum(["GET", "POST"]).optional().describe("HTTP method (default POST)"),
|
|
56355
|
+
param: string2().optional().describe("Query/body param name that carries the path (default 'path')"),
|
|
56356
|
+
bodyRaw: boolean2().optional().describe("Treat the raw POST body as the filesystem path (default false)"),
|
|
56357
|
+
headers: string2().optional().describe("JSON object of extra headers"),
|
|
56358
|
+
markerPath: string2().optional().describe("Marker file to read (default /etc/passwd)"),
|
|
56359
|
+
timeout: number2().int().optional().describe("Timeout seconds (default 15)")
|
|
56360
|
+
}
|
|
56361
|
+
}, async ({ url, method, param, bodyRaw, headers, markerPath, timeout }) => {
|
|
56362
|
+
let hdrs = {};
|
|
56363
|
+
try {
|
|
56364
|
+
hdrs = headers ? JSON.parse(headers) : {};
|
|
56365
|
+
} catch {}
|
|
56366
|
+
const verdict = await verifyFileRead({
|
|
56367
|
+
url,
|
|
56368
|
+
method: method ?? "POST",
|
|
56369
|
+
param,
|
|
56370
|
+
bodyRaw: bodyRaw ?? false,
|
|
56371
|
+
headers: hdrs,
|
|
56372
|
+
markerPath,
|
|
56373
|
+
timeoutMs: timeout ? timeout * 1000 : undefined
|
|
56374
|
+
});
|
|
56375
|
+
return {
|
|
56376
|
+
content: [{
|
|
56377
|
+
type: "text",
|
|
56378
|
+
text: JSON.stringify({
|
|
56379
|
+
...verdict,
|
|
56380
|
+
next_steps: [
|
|
56381
|
+
`Verdict: ${verdict.status} — ${verdict.reason}`,
|
|
56382
|
+
verdict.status === "confirmed" ? "Confirmed arbitrary file read. Record with finding_create + attach the marker/control evidence." : verdict.status === "blocked" ? "Blocked. Target unreachable — retry or re-check scope." : "Unconfirmed. The response is gated (auth/session) or the file is not read — obtain a low-priv session and re-run with a valid session, or mark the finding unconfirmed with this evidence."
|
|
56383
|
+
]
|
|
56384
|
+
})
|
|
56385
|
+
}]
|
|
56386
|
+
};
|
|
56387
|
+
});
|
|
56284
56388
|
server.registerTool("strike_resolve", {
|
|
56285
56389
|
title: "Resolve a finding from a STRIKE verdict",
|
|
56286
56390
|
description: "STRIKE+FINDINGS: take a canonical Finding and a STRIKE verdict, and advance the lifecycle end-to-end — hypothesis->validating->confirmed (marker reflected, control inert) or ->false_positive/blocked/unconfirmed. Returns the updated finding with evidence + recomputed confidence.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.7",
|
|
4
4
|
"description": "Blitz Strike — a universal MCP security-audit toolbelt. BLITZ sweeps the attack surface, EAGLE-EYE traces source-to-sink, STRIKE verifies live. 57 attack chains, 130-tool catalog, intelligence data layer. One server, every agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|