blitzstrike 1.0.40 → 1.0.41
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 +17 -0
- package/dist/index.js +168 -0
- package/package.json +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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
|
+
## [1.0.41] — 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Phase 6 — Browser Validation** (`src/browser.ts` + MCP tool `browser_validate`):
|
|
13
|
+
- Playwright-based validation of static-analysis hypotheses (not a scanner —
|
|
14
|
+
gated, "use only when necessary").
|
|
15
|
+
- Checks: `dom_xss` (marker-payload execution), `open_redirect` (unexpected
|
|
16
|
+
redirect), `auth_bypass` (missing auth enforcement), `csrf` (tokenless
|
|
17
|
+
state-changing form).
|
|
18
|
+
- Each check returns structured evidence (verdict + final_url + dom/console/
|
|
19
|
+
page-error signals) suitable for attaching to a finding.
|
|
20
|
+
- `playwright-core` is an **optional peer dependency**; `browser_validate`
|
|
21
|
+
degrades to a clear `unavailable` result when it is not installed.
|
|
22
|
+
- Hermetic browser test suite (`bun run test:browser`, 8 checks) against a
|
|
23
|
+
local vulnerable server.
|
|
24
|
+
|
|
8
25
|
## [1.0.40] — 2026-09-13
|
|
9
26
|
|
|
10
27
|
### Changed (review + spec alignment)
|
package/dist/index.js
CHANGED
|
@@ -51040,6 +51040,162 @@ function scanFramework(code, frameworkId) {
|
|
|
51040
51040
|
};
|
|
51041
51041
|
}
|
|
51042
51042
|
|
|
51043
|
+
// src/browser.ts
|
|
51044
|
+
var chromiumCache = null;
|
|
51045
|
+
async function ensureChromium() {
|
|
51046
|
+
if (chromiumCache)
|
|
51047
|
+
return chromiumCache;
|
|
51048
|
+
try {
|
|
51049
|
+
chromiumCache = await import("playwright-core");
|
|
51050
|
+
return chromiumCache;
|
|
51051
|
+
} catch {
|
|
51052
|
+
return null;
|
|
51053
|
+
}
|
|
51054
|
+
}
|
|
51055
|
+
function unavailable(check, target, reason) {
|
|
51056
|
+
return {
|
|
51057
|
+
check,
|
|
51058
|
+
target: redactSecrets(target),
|
|
51059
|
+
verdict: "unavailable",
|
|
51060
|
+
detail: reason,
|
|
51061
|
+
evidence: { console_errors: [], page_errors: [] }
|
|
51062
|
+
};
|
|
51063
|
+
}
|
|
51064
|
+
function errorEvidence(check, target, e) {
|
|
51065
|
+
return {
|
|
51066
|
+
check,
|
|
51067
|
+
target: redactSecrets(target),
|
|
51068
|
+
verdict: "error",
|
|
51069
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
51070
|
+
evidence: { console_errors: [], page_errors: [] }
|
|
51071
|
+
};
|
|
51072
|
+
}
|
|
51073
|
+
async function validateDomXss(target, payload = '<img src=x onerror="window.__blitz_xss__=true">') {
|
|
51074
|
+
const chr = await ensureChromium();
|
|
51075
|
+
if (!chr)
|
|
51076
|
+
return unavailable("dom_xss", target, "playwright-core not installed (optional peer dependency)");
|
|
51077
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51078
|
+
try {
|
|
51079
|
+
const page = await browser.newPage();
|
|
51080
|
+
const consoleErrors = [];
|
|
51081
|
+
const pageErrors = [];
|
|
51082
|
+
page.on("console", (m) => {
|
|
51083
|
+
if (m.type() === "error")
|
|
51084
|
+
consoleErrors.push(m.text());
|
|
51085
|
+
});
|
|
51086
|
+
page.on("pageerror", (e) => pageErrors.push(String(e)));
|
|
51087
|
+
const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
|
|
51088
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51089
|
+
await page.waitForTimeout(600);
|
|
51090
|
+
const marker = await page.evaluate(() => window.__blitz_xss__ === true);
|
|
51091
|
+
return {
|
|
51092
|
+
check: "dom_xss",
|
|
51093
|
+
target: redactSecrets(target),
|
|
51094
|
+
verdict: marker ? "confirmed" : "not_confirmed",
|
|
51095
|
+
detail: marker ? "payload executed (marker set)" : "payload did not execute (no marker)",
|
|
51096
|
+
evidence: { final_url: page.url(), dom_marker: marker, console_errors: consoleErrors, page_errors: pageErrors }
|
|
51097
|
+
};
|
|
51098
|
+
} catch (e) {
|
|
51099
|
+
return errorEvidence("dom_xss", target, e);
|
|
51100
|
+
} finally {
|
|
51101
|
+
await browser.close();
|
|
51102
|
+
}
|
|
51103
|
+
}
|
|
51104
|
+
async function validateOpenRedirect(target, payload) {
|
|
51105
|
+
const chr = await ensureChromium();
|
|
51106
|
+
if (!chr)
|
|
51107
|
+
return unavailable("open_redirect", target, "playwright-core not installed (optional peer dependency)");
|
|
51108
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51109
|
+
try {
|
|
51110
|
+
const page = await browser.newPage();
|
|
51111
|
+
const url = target.includes("{{payload}}") ? target.replace("{{payload}}", encodeURIComponent(payload)) : target;
|
|
51112
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51113
|
+
await page.waitForTimeout(400);
|
|
51114
|
+
const finalUrl = page.url();
|
|
51115
|
+
const redirected = finalUrl !== url && finalUrl.includes(payload);
|
|
51116
|
+
return {
|
|
51117
|
+
check: "open_redirect",
|
|
51118
|
+
target: redactSecrets(target),
|
|
51119
|
+
verdict: redirected ? "confirmed" : "not_confirmed",
|
|
51120
|
+
detail: redirected ? `browser redirected to ${finalUrl}` : `no redirect to attacker URL (final: ${finalUrl})`,
|
|
51121
|
+
evidence: { final_url: finalUrl, console_errors: [], page_errors: [] }
|
|
51122
|
+
};
|
|
51123
|
+
} catch (e) {
|
|
51124
|
+
return errorEvidence("open_redirect", target, e);
|
|
51125
|
+
} finally {
|
|
51126
|
+
await browser.close();
|
|
51127
|
+
}
|
|
51128
|
+
}
|
|
51129
|
+
async function validateAuthBypass(target) {
|
|
51130
|
+
const chr = await ensureChromium();
|
|
51131
|
+
if (!chr)
|
|
51132
|
+
return unavailable("auth_bypass", target, "playwright-core not installed (optional peer dependency)");
|
|
51133
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51134
|
+
try {
|
|
51135
|
+
const page = await browser.newPage();
|
|
51136
|
+
const resp = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51137
|
+
const status = resp?.status() ?? 0;
|
|
51138
|
+
const finalUrl = page.url();
|
|
51139
|
+
const enforced = status === 401 || status === 403 || /login|signin|auth/.test(finalUrl);
|
|
51140
|
+
return {
|
|
51141
|
+
check: "auth_bypass",
|
|
51142
|
+
target: redactSecrets(target),
|
|
51143
|
+
verdict: enforced ? "not_confirmed" : "confirmed",
|
|
51144
|
+
detail: enforced ? `auth enforced (status ${status}, final ${finalUrl})` : `no auth enforcement observed (status ${status}, final ${finalUrl})`,
|
|
51145
|
+
evidence: { final_url: finalUrl, status, console_errors: [], page_errors: [] }
|
|
51146
|
+
};
|
|
51147
|
+
} catch (e) {
|
|
51148
|
+
return errorEvidence("auth_bypass", target, e);
|
|
51149
|
+
} finally {
|
|
51150
|
+
await browser.close();
|
|
51151
|
+
}
|
|
51152
|
+
}
|
|
51153
|
+
async function validateCsrf(target) {
|
|
51154
|
+
const chr = await ensureChromium();
|
|
51155
|
+
if (!chr)
|
|
51156
|
+
return unavailable("csrf", target, "playwright-core not installed (optional peer dependency)");
|
|
51157
|
+
const browser = await chr.chromium.launch({ headless: true });
|
|
51158
|
+
try {
|
|
51159
|
+
const page = await browser.newPage();
|
|
51160
|
+
await page.goto(target, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
51161
|
+
const result = await page.evaluate(() => {
|
|
51162
|
+
const forms = Array.from(document.querySelectorAll("form"));
|
|
51163
|
+
const stateChanging = forms.filter((f) => /post/i.test(f.method) || f.method === "");
|
|
51164
|
+
if (stateChanging.length === 0)
|
|
51165
|
+
return { token: true, count: 0 };
|
|
51166
|
+
for (const f of stateChanging) {
|
|
51167
|
+
const token = f.querySelector('input[name*="csrf"], input[name*="token"], input[name*="_token"], input[name*="nonce"]');
|
|
51168
|
+
if (!token)
|
|
51169
|
+
return { token: false, count: stateChanging.length };
|
|
51170
|
+
}
|
|
51171
|
+
return { token: true, count: stateChanging.length };
|
|
51172
|
+
});
|
|
51173
|
+
return {
|
|
51174
|
+
check: "csrf",
|
|
51175
|
+
target: redactSecrets(target),
|
|
51176
|
+
verdict: result.token ? "not_confirmed" : "confirmed",
|
|
51177
|
+
detail: result.token ? `CSRF token present (${result.count} form(s))` : `state-changing form missing CSRF token (${result.count} form(s))`,
|
|
51178
|
+
evidence: { csrf_token_present: result.token, forms_count: result.count, console_errors: [], page_errors: [] }
|
|
51179
|
+
};
|
|
51180
|
+
} catch (e) {
|
|
51181
|
+
return errorEvidence("csrf", target, e);
|
|
51182
|
+
} finally {
|
|
51183
|
+
await browser.close();
|
|
51184
|
+
}
|
|
51185
|
+
}
|
|
51186
|
+
async function browserValidate(check, target, payload) {
|
|
51187
|
+
switch (check) {
|
|
51188
|
+
case "dom_xss":
|
|
51189
|
+
return validateDomXss(target, payload);
|
|
51190
|
+
case "open_redirect":
|
|
51191
|
+
return validateOpenRedirect(target, payload ?? "https://attacker.example/");
|
|
51192
|
+
case "auth_bypass":
|
|
51193
|
+
return validateAuthBypass(target);
|
|
51194
|
+
case "csrf":
|
|
51195
|
+
return validateCsrf(target);
|
|
51196
|
+
}
|
|
51197
|
+
}
|
|
51198
|
+
|
|
51043
51199
|
// src/guidance.ts
|
|
51044
51200
|
var SINK_DIRECTIVES = {
|
|
51045
51201
|
sql_execution: "Validate the SQL injection live with strike_verify (use a unique MARKER and a NEGATIVE CONTROL that stays inert), or read_tool_manual for sqlmap. Only a reflected marker + inert control = confirmed.",
|
|
@@ -52843,6 +52999,18 @@ function createServer() {
|
|
|
52843
52999
|
out.scan = scanFramework(code, framework);
|
|
52844
53000
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
52845
53001
|
});
|
|
53002
|
+
server.registerTool("browser_validate", {
|
|
53003
|
+
title: "Browser validation (Playwright)",
|
|
53004
|
+
description: "PHASE 6: validate a static-analysis HYPOTHESIS by driving a real headless Chromium. Checks: dom_xss, open_redirect, auth_bypass, csrf. Use only when necessary — to confirm a hypothesis with browser evidence, not as a first-pass scan. Requires playwright-core (optional peer dependency).",
|
|
53005
|
+
inputSchema: {
|
|
53006
|
+
check: _enum(["dom_xss", "open_redirect", "auth_bypass", "csrf"]).describe("Validation check to run"),
|
|
53007
|
+
target: string2().describe("Target URL (use {{payload}} placeholder for dom_xss/open_redirect injection)"),
|
|
53008
|
+
payload: string2().optional().describe("Payload to inject (defaults to an XSS marker for dom_xss)")
|
|
53009
|
+
}
|
|
53010
|
+
}, async ({ check, target, payload }) => {
|
|
53011
|
+
const result = await browserValidate(check, target, payload);
|
|
53012
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
53013
|
+
});
|
|
52846
53014
|
server.registerTool("taint_tree", {
|
|
52847
53015
|
title: "Whole-tree taint scan",
|
|
52848
53016
|
description: "EAGLE-EYE: run inter-procedural taint analysis across an entire source tree.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzstrike",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
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": {
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"tools-catalog.json"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "bun build src/index.ts --outdir dist --target node --format esm",
|
|
28
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --external playwright-core",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"serve": "bun run src/index.ts serve",
|
|
31
31
|
"doctor": "bun run src/index.ts doctor",
|
|
32
32
|
"compile": "bun build src/index.ts --compile --outfile dist/blitzstrike",
|
|
33
33
|
"test": "bun run test/verify.ts",
|
|
34
|
+
"test:browser": "bun run test/browser-verify.ts",
|
|
34
35
|
"prepublishOnly": "bun run typecheck && bun run build",
|
|
35
36
|
"bench": "bun run test/verify.ts"
|
|
36
37
|
},
|
|
@@ -74,6 +75,15 @@
|
|
|
74
75
|
},
|
|
75
76
|
"devDependencies": {
|
|
76
77
|
"@types/node": "^22.20.2",
|
|
78
|
+
"playwright-core": "1.61.0",
|
|
77
79
|
"typescript": "^5.5.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"playwright-core": ">=1.61.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"playwright-core": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
}
|