blitzstrike 1.0.39 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bench/corpus/java-cmd-processbuilder.md +8 -0
  3. package/bench/corpus/java-deser-readunshared.md +8 -0
  4. package/bench/corpus/java-sqli-execute.md +8 -0
  5. package/bench/corpus/java-ssrf-httpurl.md +8 -0
  6. package/bench/corpus/java-xss-escape.md +8 -0
  7. package/bench/corpus/java-xss-println.md +8 -0
  8. package/bench/corpus/js-cmd-execsync.md +8 -0
  9. package/bench/corpus/js-filewrite.md +8 -0
  10. package/bench/corpus/js-pathtraversal-readsync.md +8 -0
  11. package/bench/corpus/js-redirect-encodeuri.md +8 -0
  12. package/bench/corpus/js-ssrf-axios.md +8 -0
  13. package/bench/corpus/js-ssrf-fetch.md +8 -0
  14. package/bench/corpus/js-xss-documentwrite.md +8 -0
  15. package/bench/corpus/js-xss-innerhtml.md +8 -0
  16. package/bench/corpus/php-cmd-absint.md +9 -0
  17. package/bench/corpus/php-cmd-popen.md +9 -0
  18. package/bench/corpus/php-deser-json.md +9 -0
  19. package/bench/corpus/php-include-once.md +9 -0
  20. package/bench/corpus/php-include-sanitized.md +9 -0
  21. package/bench/corpus/php-sqli-filterinput.md +9 -0
  22. package/bench/corpus/php-sqli-getresults.md +9 -0
  23. package/bench/corpus/php-ssrf-wppost.md +9 -0
  24. package/bench/corpus/php-xss-escurl.md +9 -0
  25. package/bench/corpus/php-xss-print.md +9 -0
  26. package/bench/corpus/php-xss-sanitizetext.md +9 -0
  27. package/bench/corpus/py-cmd-popen.md +9 -0
  28. package/bench/corpus/py-cmd-shlex.md +9 -0
  29. package/bench/corpus/py-deser-yaml.md +9 -0
  30. package/bench/corpus/py-sqli-executescript.md +9 -0
  31. package/bench/corpus/py-ssrf-urllib.md +9 -0
  32. package/bench/corpus/py-xss-escape.md +8 -0
  33. package/dist/index.js +201 -8
  34. package/package.json +12 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ 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
+
25
+ ## [1.0.40] — 2026-09-13
26
+
27
+ ### Changed (review + spec alignment)
28
+
29
+ - **Coverage Matrix 2.0 (§57)**: `coverageMatrix()` now also tracks the framework
30
+ dimension — per-framework sinks/sources/sanitizers/auth counts + CWE set.
31
+ - **Benchmark 100+ (§65)**: corpus grown 70 → **101 fixtures** (70 vulnerable +
32
+ 31 patched), still scoring detection 1.0 / FP 0 / FN 0 / precision 1.0.
33
+
34
+ ### Fixed
35
+
36
+ - JS adapter now detects `el.innerHTML = <tainted>` (property assignment) as a
37
+ DOM XSS sink — previously only `innerHTML` inside call expressions was caught.
38
+
8
39
  ## [1.0.39] — 2026-09-13
9
40
 
10
41
  ### Added
@@ -0,0 +1,8 @@
1
+ - id: java-cmd-processbuilder
2
+ - language: java
3
+ - vulnerable: true
4
+ - sink_type: command_execution
5
+ ```java
6
+ String cmd = request.getParameter("cmd");
7
+ new ProcessBuilder(cmd).start();
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: java-deser-readunshared
2
+ - language: java
3
+ - vulnerable: true
4
+ - sink_type: deserialization
5
+ ```java
6
+ ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
7
+ ois.readUnshared();
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: java-sqli-execute
2
+ - language: java
3
+ - vulnerable: true
4
+ - sink_type: sql_execution
5
+ ```java
6
+ String id = request.getParameter("id");
7
+ st.execute("DELETE FROM t WHERE id=" + id);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: java-ssrf-httpurl
2
+ - language: java
3
+ - vulnerable: true
4
+ - sink_type: http_request
5
+ ```java
6
+ String url = request.getParameter("url");
7
+ HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: java-xss-escape
2
+ - language: java
3
+ - vulnerable: false
4
+ - sink_type: html_render
5
+ ```java
6
+ String x = request.getParameter("x");
7
+ response.getWriter().write(HtmlUtils.htmlEscape(x));
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: java-xss-println
2
+ - language: java
3
+ - vulnerable: true
4
+ - sink_type: html_render
5
+ ```java
6
+ String x = request.getParameter("x");
7
+ response.getWriter().println(x);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-cmd-execsync
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: command_execution
5
+ ```javascript
6
+ const c = req.query.cmd;
7
+ execSync("ls " + c);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-filewrite
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: file_operations
5
+ ```javascript
6
+ const d = req.body.data;
7
+ fs.writeFile("/tmp/x", d);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-pathtraversal-readsync
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: path_traversal
5
+ ```javascript
6
+ const f = req.query.file;
7
+ fs.readFileSync("/data/" + f);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-redirect-encodeuri
2
+ - language: javascript
3
+ - vulnerable: false
4
+ - sink_type: redirect
5
+ ```javascript
6
+ const u = req.query.u;
7
+ res.redirect(encodeURIComponent(u));
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-ssrf-axios
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: http_request
5
+ ```javascript
6
+ const u = req.query.url;
7
+ axios.get(u);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-ssrf-fetch
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: http_request
5
+ ```javascript
6
+ const u = req.query.url;
7
+ fetch(u);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-xss-documentwrite
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: html_render
5
+ ```javascript
6
+ const x = req.query.x;
7
+ document.write(x);
8
+ ```
@@ -0,0 +1,8 @@
1
+ - id: js-xss-innerhtml
2
+ - language: javascript
3
+ - vulnerable: true
4
+ - sink_type: html_render
5
+ ```javascript
6
+ const x = req.query.x;
7
+ el.innerHTML = x;
8
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-cmd-absint
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: command_execution
5
+ ```php
6
+ <?php
7
+ $n = $_GET["n"];
8
+ system("ping -c " . absint($n));
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-cmd-popen
2
+ - language: php
3
+ - vulnerable: true
4
+ - sink_type: command_execution
5
+ ```php
6
+ <?php
7
+ $cmd = $_GET["cmd"];
8
+ popen($cmd, "r");
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-deser-json
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: deserialization
5
+ ```php
6
+ <?php
7
+ $data = $_COOKIE["data"];
8
+ json_decode($data);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-include-once
2
+ - language: php
3
+ - vulnerable: true
4
+ - sink_type: file_inclusion
5
+ ```php
6
+ <?php
7
+ $page = $_GET["page"];
8
+ include_once($page . ".php");
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-include-sanitized
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: file_inclusion
5
+ ```php
6
+ <?php
7
+ $f = $_GET["f"];
8
+ include(sanitize_file_name($f));
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-sqli-filterinput
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: sql_execution
5
+ ```php
6
+ <?php
7
+ $id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);
8
+ $wpdb->query("SELECT * FROM t WHERE id=$id");
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-sqli-getresults
2
+ - language: php
3
+ - vulnerable: true
4
+ - sink_type: sql_execution
5
+ ```php
6
+ <?php
7
+ $id = $_GET["id"];
8
+ $wpdb->get_results("SELECT * FROM t WHERE id=$id");
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-ssrf-wppost
2
+ - language: php
3
+ - vulnerable: true
4
+ - sink_type: http_request
5
+ ```php
6
+ <?php
7
+ $url = $_GET["url"];
8
+ wp_remote_post($url, []);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-xss-escurl
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: redirect
5
+ ```php
6
+ <?php
7
+ $url = $_GET["url"];
8
+ wp_safe_redirect(esc_url($url));
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-xss-print
2
+ - language: php
3
+ - vulnerable: true
4
+ - sink_type: html_render
5
+ ```php
6
+ <?php
7
+ $name = $_GET["name"];
8
+ print $name;
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: php-xss-sanitizetext
2
+ - language: php
3
+ - vulnerable: false
4
+ - sink_type: html_render
5
+ ```php
6
+ <?php
7
+ $v = $_GET["v"];
8
+ echo sanitize_text_field($v);
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: py-cmd-popen
2
+ - language: python
3
+ - vulnerable: true
4
+ - sink_type: command_execution
5
+ ```python
6
+ import os
7
+ cmd = request.args.get("cmd")
8
+ os.popen(cmd)
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: py-cmd-shlex
2
+ - language: python
3
+ - vulnerable: false
4
+ - sink_type: command_execution
5
+ ```python
6
+ import shlex, subprocess
7
+ cmd = request.args.get("cmd")
8
+ subprocess.run(shlex.quote(cmd))
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: py-deser-yaml
2
+ - language: python
3
+ - vulnerable: true
4
+ - sink_type: deserialization
5
+ ```python
6
+ import yaml
7
+ data = request.get_data()
8
+ yaml.load(data)
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: py-sqli-executescript
2
+ - language: python
3
+ - vulnerable: true
4
+ - sink_type: sql_execution
5
+ ```python
6
+ import sqlite3
7
+ id = request.args.get("id")
8
+ cursor.executescript(f"DELETE FROM t WHERE id={id}")
9
+ ```
@@ -0,0 +1,9 @@
1
+ - id: py-ssrf-urllib
2
+ - language: python
3
+ - vulnerable: true
4
+ - sink_type: http_request
5
+ ```python
6
+ import urllib.request
7
+ url = request.args.get("url")
8
+ urllib.request.urlopen(url)
9
+ ```
@@ -0,0 +1,8 @@
1
+ - id: py-xss-escape
2
+ - language: python
3
+ - vulnerable: false
4
+ - sink_type: html_render
5
+ ```python
6
+ name = request.args.get("name")
7
+ return render_template_string(escape(name))
8
+ ```
package/dist/index.js CHANGED
@@ -50492,13 +50492,23 @@ var jsAdapter = {
50492
50492
  rhsVars.push(c.name);
50493
50493
  assigns.push({ target: name, sources: rhsVars, line: n.loc?.start?.line ?? 0 });
50494
50494
  }
50495
- if (n.type === "AssignmentExpression" && n.left?.type === "Identifier") {
50496
- const name = n.left.name;
50497
- const rhsVars = [];
50498
- for (const c of jsWalk(n.right, []))
50499
- if (c.type === "Identifier" && c.name)
50500
- rhsVars.push(c.name);
50501
- assigns.push({ target: name, sources: rhsVars, line: n.loc?.start?.line ?? 0 });
50495
+ if (n.type === "AssignmentExpression") {
50496
+ if (n.left?.type === "MemberExpression" && n.left.property?.name === "innerHTML") {
50497
+ const rhsVars = [];
50498
+ for (const c of jsWalk(n.right, []))
50499
+ if (c.type === "Identifier" && c.name)
50500
+ rhsVars.push(c.name);
50501
+ for (const v of rhsVars)
50502
+ sinks.push({ variable: v, kind: "html_render", line: n.loc?.start?.line ?? 0, cwe: "CWE-79" });
50503
+ }
50504
+ if (n.left?.type === "Identifier") {
50505
+ const name = n.left.name;
50506
+ const rhsVars = [];
50507
+ for (const c of jsWalk(n.right, []))
50508
+ if (c.type === "Identifier" && c.name)
50509
+ rhsVars.push(c.name);
50510
+ assigns.push({ target: name, sources: rhsVars, line: n.loc?.start?.line ?? 0 });
50511
+ }
50502
50512
  }
50503
50513
  if (n.type === "CallExpression") {
50504
50514
  const calleeText = srcText(n.callee);
@@ -51030,6 +51040,162 @@ function scanFramework(code, frameworkId) {
51030
51040
  };
51031
51041
  }
51032
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
+
51033
51199
  // src/guidance.ts
51034
51200
  var SINK_DIRECTIVES = {
51035
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.",
@@ -52039,6 +52205,19 @@ function coverageMatrix() {
52039
52205
  const covered = langs.map((l) => l.language).filter((lang) => SINK_BY_LANG[lang]?.[sink] === true);
52040
52206
  return { sink, languages: covered };
52041
52207
  });
52208
+ const frameworkCoverage = listFrameworks().map(({ id, language }) => {
52209
+ const p = frameworkProfile(id);
52210
+ const cwe = p ? [...new Set(p.sinks.map((s) => s.cwe).filter((c) => !!c))].sort() : [];
52211
+ return {
52212
+ framework: id,
52213
+ language,
52214
+ sinks: p?.sinks.length ?? 0,
52215
+ sources: p?.sources.length ?? 0,
52216
+ sanitizers: p?.sanitizers.length ?? 0,
52217
+ auth: p?.auth.length ?? 0,
52218
+ cwe
52219
+ };
52220
+ });
52042
52221
  let coveredPairs = 0;
52043
52222
  for (const lang of langs) {
52044
52223
  for (const sink of SINK_CLASSES) {
@@ -52050,11 +52229,13 @@ function coverageMatrix() {
52050
52229
  return {
52051
52230
  languages: langs,
52052
52231
  sink_coverage: sinkCoverage,
52232
+ framework_coverage: frameworkCoverage,
52053
52233
  counts: {
52054
52234
  languages: langs.length,
52055
52235
  sink_classes: SINK_CLASSES.length,
52056
52236
  covered_pairs: coveredPairs,
52057
- total_pairs: totalPairs
52237
+ total_pairs: totalPairs,
52238
+ frameworks: frameworkCoverage.length
52058
52239
  },
52059
52240
  coverage_ratio: Number((coveredPairs / totalPairs).toFixed(3))
52060
52241
  };
@@ -52818,6 +52999,18 @@ function createServer() {
52818
52999
  out.scan = scanFramework(code, framework);
52819
53000
  return { content: [{ type: "text", text: JSON.stringify(out) }] };
52820
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
+ });
52821
53014
  server.registerTool("taint_tree", {
52822
53015
  title: "Whole-tree taint scan",
52823
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.39",
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
  }