blitzstrike 1.0.12 → 1.0.13

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 (2) hide show
  1. package/dist/index.js +436 -253
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  var __create = Object.create;
3
4
  var __getProtoOf = Object.getPrototypeOf;
4
5
  var __defProp = Object.defineProperty;
@@ -43,6 +44,7 @@ var __esm = (fn, res, err) => () => {
43
44
  throw err[0];
44
45
  return res;
45
46
  };
47
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
46
48
 
47
49
  // node_modules/ajv/dist/compile/codegen/code.js
48
50
  var require_code = __commonJS(function(exports) {
@@ -6930,6 +6932,262 @@ var require_dist = __commonJS(function(exports, module) {
6930
6932
  exports.default = formatsPlugin;
6931
6933
  });
6932
6934
 
6935
+ // src/scanner.ts
6936
+ import { readFileSync, readdirSync, statSync } from "node:fs";
6937
+ import { join } from "node:path";
6938
+ function escapeRegExp(s) {
6939
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6940
+ }
6941
+ function lineno(text, pos) {
6942
+ let n = 1;
6943
+ for (let i = 0;i < pos; i++) {
6944
+ if (text.charCodeAt(i) === 10)
6945
+ n++;
6946
+ }
6947
+ return n;
6948
+ }
6949
+ function iterSourceFiles(root, maxFiles = 5000) {
6950
+ const out = [];
6951
+ const walk = (dir) => {
6952
+ if (out.length >= maxFiles)
6953
+ return;
6954
+ let entries;
6955
+ try {
6956
+ entries = readdirSync(dir);
6957
+ } catch {
6958
+ return;
6959
+ }
6960
+ for (const name of entries) {
6961
+ if (out.length >= maxFiles)
6962
+ return;
6963
+ const full = join(dir, name);
6964
+ let st;
6965
+ try {
6966
+ st = statSync(full);
6967
+ } catch {
6968
+ continue;
6969
+ }
6970
+ if (st.isDirectory()) {
6971
+ if (SKIP_PARTS.has(name) || LIB_PARTS.has(name))
6972
+ continue;
6973
+ walk(full);
6974
+ } else if (st.isFile()) {
6975
+ const dot = name.lastIndexOf(".");
6976
+ const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
6977
+ if (!TARGET_EXTS.has(ext))
6978
+ continue;
6979
+ out.push(full);
6980
+ }
6981
+ }
6982
+ };
6983
+ walk(root);
6984
+ return out;
6985
+ }
6986
+ function findFunctionBounds(text, funcStart) {
6987
+ const brace = text.indexOf("{", funcStart);
6988
+ if (brace === -1)
6989
+ return null;
6990
+ let depth = 0;
6991
+ for (let i = brace;i < text.length; i++) {
6992
+ const c = text[i];
6993
+ if (c === "{")
6994
+ depth++;
6995
+ else if (c === "}") {
6996
+ depth--;
6997
+ if (depth === 0)
6998
+ return [funcStart, i + 1];
6999
+ }
7000
+ }
7001
+ return null;
7002
+ }
7003
+ function scanFile(path) {
7004
+ let text;
7005
+ try {
7006
+ text = readFileSync(path, "utf8");
7007
+ } catch {
7008
+ return { file: path, endpoints: [], sinks: [], auth_gates_present: [], error: "unreadable" };
7009
+ }
7010
+ const endpoints = [];
7011
+ for (const hook of NOPRIV_HOOKS) {
7012
+ const re = new RegExp(escapeRegExp(hook), "g");
7013
+ let m;
7014
+ while ((m = re.exec(text)) !== null) {
7015
+ endpoints.push({ hook, line: lineno(text, m.index) });
7016
+ }
7017
+ }
7018
+ const sinks = [];
7019
+ for (const [sink, bugclass] of Object.entries(SINKS)) {
7020
+ const re = new RegExp(escapeRegExp(sink), "g");
7021
+ let m;
7022
+ while ((m = re.exec(text)) !== null) {
7023
+ sinks.push({ sink, class: bugclass, line: lineno(text, m.index) });
7024
+ }
7025
+ }
7026
+ const gates = AUTH_GATES.filter((g) => text.includes(g));
7027
+ const lines = text.split(`
7028
+ `).length;
7029
+ return { file: path, lines, endpoints, sinks, auth_gates_present: gates };
7030
+ }
7031
+ function traceFunction(path, symbol) {
7032
+ let text;
7033
+ try {
7034
+ text = readFileSync(path, "utf8");
7035
+ } catch {
7036
+ return { file: path, symbol, definitions: [], definition_count: 0, error: "unreadable" };
7037
+ }
7038
+ const pattern = new RegExp(`function\\s+&?${escapeRegExp(symbol)}\\s*\\(`, "g");
7039
+ const results = [];
7040
+ let m;
7041
+ while ((m = pattern.exec(text)) !== null) {
7042
+ const bounds = findFunctionBounds(text, m.index);
7043
+ if (!bounds)
7044
+ continue;
7045
+ const [start, end] = bounds;
7046
+ const body = text.slice(start, end);
7047
+ const bodyStartLine = lineno(text, start);
7048
+ const bodyEndLine = lineno(text, end);
7049
+ const innerSinks = [];
7050
+ for (const [sink, bugclass] of Object.entries(SINKS)) {
7051
+ const re = new RegExp(escapeRegExp(sink), "g");
7052
+ let sm;
7053
+ while ((sm = re.exec(body)) !== null) {
7054
+ innerSinks.push({
7055
+ sink,
7056
+ class: bugclass,
7057
+ line: bodyStartLine + body.slice(0, sm.index).split(`
7058
+ `).length - 1
7059
+ });
7060
+ }
7061
+ }
7062
+ const innerGates = AUTH_GATES.filter((g) => body.includes(g));
7063
+ results.push({
7064
+ line: bodyStartLine,
7065
+ line_end: bodyEndLine,
7066
+ length: end - start,
7067
+ sinks_in_scope: innerSinks,
7068
+ auth_gates_in_scope: innerGates,
7069
+ body_preview: body.slice(0, 4000)
7070
+ });
7071
+ }
7072
+ return {
7073
+ file: path,
7074
+ symbol,
7075
+ definitions: results,
7076
+ definition_count: results.length
7077
+ };
7078
+ }
7079
+ function grepInFunctions(root, sink, maxHits = 50) {
7080
+ const hits = [];
7081
+ for (const p of iterSourceFiles(root)) {
7082
+ if (hits.length >= maxHits)
7083
+ break;
7084
+ let text;
7085
+ try {
7086
+ text = readFileSync(p, "utf8");
7087
+ } catch {
7088
+ continue;
7089
+ }
7090
+ const re = new RegExp(escapeRegExp(sink), "g");
7091
+ let m;
7092
+ while ((m = re.exec(text)) !== null) {
7093
+ const line = lineno(text, m.index);
7094
+ const head = text.slice(0, m.index);
7095
+ const funcRe = /function\s+&?\w+\s*\(/g;
7096
+ const funcs = [];
7097
+ let fm;
7098
+ while ((fm = funcRe.exec(head)) !== null)
7099
+ funcs.push(fm);
7100
+ if (funcs.length === 0)
7101
+ continue;
7102
+ const fstart = funcs[funcs.length - 1].index;
7103
+ const bounds = findFunctionBounds(text, fstart);
7104
+ if (!bounds || !(bounds[0] <= m.index && m.index < bounds[1]))
7105
+ continue;
7106
+ const body = text.slice(bounds[0], bounds[1]);
7107
+ const gates = AUTH_GATES.filter((g) => body.includes(g));
7108
+ hits.push({
7109
+ file: p,
7110
+ line,
7111
+ sink,
7112
+ auth_gates_in_scope: gates,
7113
+ guarded: gates.length > 0
7114
+ });
7115
+ if (hits.length >= maxHits)
7116
+ break;
7117
+ }
7118
+ }
7119
+ return hits;
7120
+ }
7121
+ var NOPRIV_HOOKS, SINKS, AUTH_GATES, TARGET_EXTS, SKIP_PARTS, LIB_PARTS;
7122
+ var init_scanner = __esm(() => {
7123
+ NOPRIV_HOOKS = [
7124
+ "wp_ajax_nopriv_",
7125
+ "admin_post_nopriv_",
7126
+ "register_rest_route",
7127
+ "wp_ajax_"
7128
+ ];
7129
+ SINKS = {
7130
+ "eval(": "RCE (code execution)",
7131
+ "assert(": "RCE (code execution, PHP <8)",
7132
+ "system(": "RCE (command execution)",
7133
+ "exec(": "RCE (command execution)",
7134
+ "shell_exec(": "RCE (command execution)",
7135
+ "passthru(": "RCE (command execution)",
7136
+ "proc_open(": "RCE (command execution)",
7137
+ "popen(": "RCE (command execution)",
7138
+ "move_uploaded_file(": "File upload (-> RCE if .php lands in webroot)",
7139
+ "file_put_contents(": "Arbitrary file write (-> RCE via CF-003)",
7140
+ "fwrite(": "Arbitrary file write",
7141
+ "unserialize(": "PHP object injection (POP gadget chain)",
7142
+ "maybe_unserialize(": "PHP object injection (weak)",
7143
+ "include(": "Local file inclusion",
7144
+ "require(": "Local file inclusion",
7145
+ "include_once(": "Local file inclusion",
7146
+ "require_once(": "Local file inclusion",
7147
+ "$wpdb->query(": "SQL injection (unprepared query)",
7148
+ "$wpdb->get_var(": "SQL injection (unprepared query)",
7149
+ "$wpdb->get_results(": "SQL injection (unprepared query)",
7150
+ "->query(": "SQL injection (query builder)",
7151
+ "->whereRaw(": "SQL injection (raw where)",
7152
+ "wp_remote_get(": "SSRF (unvalidated URL fetch)",
7153
+ "wp_remote_post(": "SSRF (unvalidated URL fetch)",
7154
+ "file_get_contents(": "SSRF / file read",
7155
+ "extract(": "Variable injection (-> LFI/RCE without EXTR_SKIP)",
7156
+ "call_user_func(": "Dynamic dispatch (attacker-controlled callback)",
7157
+ "call_user_func_array(": "Dynamic dispatch",
7158
+ "create_function(": "RCE (deprecated eval wrapper)",
7159
+ "preg_replace(": "RCE (if /e modifier or code in pattern)"
7160
+ };
7161
+ AUTH_GATES = [
7162
+ "check_ajax_referer",
7163
+ "check_admin_referer",
7164
+ "wp_verify_nonce",
7165
+ "current_user_can",
7166
+ "is_user_logged_in",
7167
+ "JSession::checkToken",
7168
+ "->authorise(",
7169
+ "->authorize(",
7170
+ "permission_callback"
7171
+ ];
7172
+ TARGET_EXTS = new Set([
7173
+ ".php",
7174
+ ".phtml",
7175
+ ".php5",
7176
+ ".php7",
7177
+ ".inc",
7178
+ ".module",
7179
+ ".install"
7180
+ ]);
7181
+ SKIP_PARTS = new Set(["vendor", "node_modules", ".git", "tests", "test"]);
7182
+ LIB_PARTS = new Set([
7183
+ "lib",
7184
+ "libraries",
7185
+ "third-party",
7186
+ "third_party",
7187
+ "libs"
7188
+ ]);
7189
+ });
7190
+
6933
7191
  // src/sync.ts
6934
7192
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync3, rmSync } from "node:fs";
6935
7193
  import { join as join8 } from "node:path";
@@ -21669,258 +21927,8 @@ class StdioServerTransport {
21669
21927
  }
21670
21928
  }
21671
21929
 
21672
- // src/scanner.ts
21673
- import { readFileSync, readdirSync, statSync } from "node:fs";
21674
- import { join } from "node:path";
21675
- var NOPRIV_HOOKS = [
21676
- "wp_ajax_nopriv_",
21677
- "admin_post_nopriv_",
21678
- "register_rest_route",
21679
- "wp_ajax_"
21680
- ];
21681
- var SINKS = {
21682
- "eval(": "RCE (code execution)",
21683
- "assert(": "RCE (code execution, PHP <8)",
21684
- "system(": "RCE (command execution)",
21685
- "exec(": "RCE (command execution)",
21686
- "shell_exec(": "RCE (command execution)",
21687
- "passthru(": "RCE (command execution)",
21688
- "proc_open(": "RCE (command execution)",
21689
- "popen(": "RCE (command execution)",
21690
- "move_uploaded_file(": "File upload (-> RCE if .php lands in webroot)",
21691
- "file_put_contents(": "Arbitrary file write (-> RCE via CF-003)",
21692
- "fwrite(": "Arbitrary file write",
21693
- "unserialize(": "PHP object injection (POP gadget chain)",
21694
- "maybe_unserialize(": "PHP object injection (weak)",
21695
- "include(": "Local file inclusion",
21696
- "require(": "Local file inclusion",
21697
- "include_once(": "Local file inclusion",
21698
- "require_once(": "Local file inclusion",
21699
- "$wpdb->query(": "SQL injection (unprepared query)",
21700
- "$wpdb->get_var(": "SQL injection (unprepared query)",
21701
- "$wpdb->get_results(": "SQL injection (unprepared query)",
21702
- "->query(": "SQL injection (query builder)",
21703
- "->whereRaw(": "SQL injection (raw where)",
21704
- "wp_remote_get(": "SSRF (unvalidated URL fetch)",
21705
- "wp_remote_post(": "SSRF (unvalidated URL fetch)",
21706
- "file_get_contents(": "SSRF / file read",
21707
- "extract(": "Variable injection (-> LFI/RCE without EXTR_SKIP)",
21708
- "call_user_func(": "Dynamic dispatch (attacker-controlled callback)",
21709
- "call_user_func_array(": "Dynamic dispatch",
21710
- "create_function(": "RCE (deprecated eval wrapper)",
21711
- "preg_replace(": "RCE (if /e modifier or code in pattern)"
21712
- };
21713
- var AUTH_GATES = [
21714
- "check_ajax_referer",
21715
- "check_admin_referer",
21716
- "wp_verify_nonce",
21717
- "current_user_can",
21718
- "is_user_logged_in",
21719
- "JSession::checkToken",
21720
- "->authorise(",
21721
- "->authorize(",
21722
- "permission_callback"
21723
- ];
21724
- var TARGET_EXTS = new Set([
21725
- ".php",
21726
- ".phtml",
21727
- ".php5",
21728
- ".php7",
21729
- ".inc",
21730
- ".module",
21731
- ".install"
21732
- ]);
21733
- var SKIP_PARTS = new Set(["vendor", "node_modules", ".git", "tests", "test"]);
21734
- var LIB_PARTS = new Set([
21735
- "lib",
21736
- "libraries",
21737
- "third-party",
21738
- "third_party",
21739
- "libs"
21740
- ]);
21741
- function escapeRegExp(s) {
21742
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21743
- }
21744
- function lineno(text, pos) {
21745
- let n = 1;
21746
- for (let i = 0;i < pos; i++) {
21747
- if (text.charCodeAt(i) === 10)
21748
- n++;
21749
- }
21750
- return n;
21751
- }
21752
- function iterSourceFiles(root, maxFiles = 5000) {
21753
- const out = [];
21754
- const walk = (dir) => {
21755
- if (out.length >= maxFiles)
21756
- return;
21757
- let entries;
21758
- try {
21759
- entries = readdirSync(dir);
21760
- } catch {
21761
- return;
21762
- }
21763
- for (const name of entries) {
21764
- if (out.length >= maxFiles)
21765
- return;
21766
- const full = join(dir, name);
21767
- let st;
21768
- try {
21769
- st = statSync(full);
21770
- } catch {
21771
- continue;
21772
- }
21773
- if (st.isDirectory()) {
21774
- if (SKIP_PARTS.has(name) || LIB_PARTS.has(name))
21775
- continue;
21776
- walk(full);
21777
- } else if (st.isFile()) {
21778
- const dot = name.lastIndexOf(".");
21779
- const ext = dot >= 0 ? name.slice(dot).toLowerCase() : "";
21780
- if (!TARGET_EXTS.has(ext))
21781
- continue;
21782
- out.push(full);
21783
- }
21784
- }
21785
- };
21786
- walk(root);
21787
- return out;
21788
- }
21789
- function findFunctionBounds(text, funcStart) {
21790
- const brace = text.indexOf("{", funcStart);
21791
- if (brace === -1)
21792
- return null;
21793
- let depth = 0;
21794
- for (let i = brace;i < text.length; i++) {
21795
- const c = text[i];
21796
- if (c === "{")
21797
- depth++;
21798
- else if (c === "}") {
21799
- depth--;
21800
- if (depth === 0)
21801
- return [funcStart, i + 1];
21802
- }
21803
- }
21804
- return null;
21805
- }
21806
- function scanFile(path) {
21807
- let text;
21808
- try {
21809
- text = readFileSync(path, "utf8");
21810
- } catch {
21811
- return { file: path, endpoints: [], sinks: [], auth_gates_present: [], error: "unreadable" };
21812
- }
21813
- const endpoints = [];
21814
- for (const hook of NOPRIV_HOOKS) {
21815
- const re = new RegExp(escapeRegExp(hook), "g");
21816
- let m;
21817
- while ((m = re.exec(text)) !== null) {
21818
- endpoints.push({ hook, line: lineno(text, m.index) });
21819
- }
21820
- }
21821
- const sinks = [];
21822
- for (const [sink, bugclass] of Object.entries(SINKS)) {
21823
- const re = new RegExp(escapeRegExp(sink), "g");
21824
- let m;
21825
- while ((m = re.exec(text)) !== null) {
21826
- sinks.push({ sink, class: bugclass, line: lineno(text, m.index) });
21827
- }
21828
- }
21829
- const gates = AUTH_GATES.filter((g) => text.includes(g));
21830
- const lines = text.split(`
21831
- `).length;
21832
- return { file: path, lines, endpoints, sinks, auth_gates_present: gates };
21833
- }
21834
- function traceFunction(path, symbol) {
21835
- let text;
21836
- try {
21837
- text = readFileSync(path, "utf8");
21838
- } catch {
21839
- return { file: path, symbol, definitions: [], definition_count: 0, error: "unreadable" };
21840
- }
21841
- const pattern = new RegExp(`function\\s+&?${escapeRegExp(symbol)}\\s*\\(`, "g");
21842
- const results = [];
21843
- let m;
21844
- while ((m = pattern.exec(text)) !== null) {
21845
- const bounds = findFunctionBounds(text, m.index);
21846
- if (!bounds)
21847
- continue;
21848
- const [start, end] = bounds;
21849
- const body = text.slice(start, end);
21850
- const bodyStartLine = lineno(text, start);
21851
- const bodyEndLine = lineno(text, end);
21852
- const innerSinks = [];
21853
- for (const [sink, bugclass] of Object.entries(SINKS)) {
21854
- const re = new RegExp(escapeRegExp(sink), "g");
21855
- let sm;
21856
- while ((sm = re.exec(body)) !== null) {
21857
- innerSinks.push({
21858
- sink,
21859
- class: bugclass,
21860
- line: bodyStartLine + body.slice(0, sm.index).split(`
21861
- `).length - 1
21862
- });
21863
- }
21864
- }
21865
- const innerGates = AUTH_GATES.filter((g) => body.includes(g));
21866
- results.push({
21867
- line: bodyStartLine,
21868
- line_end: bodyEndLine,
21869
- length: end - start,
21870
- sinks_in_scope: innerSinks,
21871
- auth_gates_in_scope: innerGates,
21872
- body_preview: body.slice(0, 4000)
21873
- });
21874
- }
21875
- return {
21876
- file: path,
21877
- symbol,
21878
- definitions: results,
21879
- definition_count: results.length
21880
- };
21881
- }
21882
- function grepInFunctions(root, sink, maxHits = 50) {
21883
- const hits = [];
21884
- for (const p of iterSourceFiles(root)) {
21885
- if (hits.length >= maxHits)
21886
- break;
21887
- let text;
21888
- try {
21889
- text = readFileSync(p, "utf8");
21890
- } catch {
21891
- continue;
21892
- }
21893
- const re = new RegExp(escapeRegExp(sink), "g");
21894
- let m;
21895
- while ((m = re.exec(text)) !== null) {
21896
- const line = lineno(text, m.index);
21897
- const head = text.slice(0, m.index);
21898
- const funcRe = /function\s+&?\w+\s*\(/g;
21899
- const funcs = [];
21900
- let fm;
21901
- while ((fm = funcRe.exec(head)) !== null)
21902
- funcs.push(fm);
21903
- if (funcs.length === 0)
21904
- continue;
21905
- const fstart = funcs[funcs.length - 1].index;
21906
- const bounds = findFunctionBounds(text, fstart);
21907
- if (!bounds || !(bounds[0] <= m.index && m.index < bounds[1]))
21908
- continue;
21909
- const body = text.slice(bounds[0], bounds[1]);
21910
- const gates = AUTH_GATES.filter((g) => body.includes(g));
21911
- hits.push({
21912
- file: p,
21913
- line,
21914
- sink,
21915
- auth_gates_in_scope: gates,
21916
- guarded: gates.length > 0
21917
- });
21918
- if (hits.length >= maxHits)
21919
- break;
21920
- }
21921
- }
21922
- return hits;
21923
- }
21930
+ // src/server.ts
21931
+ init_scanner();
21924
21932
 
21925
21933
  // src/recon.ts
21926
21934
  var TIMEOUT_MS = 25000;
@@ -22002,6 +22010,7 @@ async function nvdLookup(cveId) {
22002
22010
  }
22003
22011
 
22004
22012
  // src/orchestrator.ts
22013
+ init_scanner();
22005
22014
  import { readFileSync as readFileSync4, existsSync as existsSync3, statSync as statSync2 } from "node:fs";
22006
22015
  import { join as join4 } from "node:path";
22007
22016
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -22294,7 +22303,7 @@ var LIFECYCLE_TRANSITIONS = {
22294
22303
  detected: ["triaged", "rejected"],
22295
22304
  triaged: ["hypothesis", "rejected", "out_of_scope"],
22296
22305
  hypothesis: ["validating", "false_positive", "rejected", "blocked"],
22297
- validating: ["confirmed", "likely", "unconfirmed", "false_positive", "blocked", "out_of_scope"],
22306
+ validating: ["confirmed", "false_positive", "blocked", "out_of_scope"],
22298
22307
  confirmed: [],
22299
22308
  false_positive: [],
22300
22309
  rejected: [],
@@ -23146,6 +23155,151 @@ async function activeScan(target, scope = "", mode = "bug-bounty", allowed = und
23146
23155
  return result;
23147
23156
  }
23148
23157
 
23158
+ // src/dataflow.ts
23159
+ init_scanner();
23160
+ var SOURCES = {
23161
+ $_GET: { id: "http_get", label: "HTTP GET parameter", attacker_controlled: true },
23162
+ $_POST: { id: "http_post", label: "HTTP POST body", attacker_controlled: true },
23163
+ $_REQUEST: { id: "http_request", label: "HTTP request (merged)", attacker_controlled: true },
23164
+ $_COOKIE: { id: "http_cookie", label: "HTTP cookie", attacker_controlled: true },
23165
+ $_FILES: { id: "uploaded_file", label: "Uploaded file", attacker_controlled: true },
23166
+ $_SERVER: { id: "http_header", label: "HTTP header / server env", attacker_controlled: true },
23167
+ "file_get_contents('php://input')": { id: "raw_body", label: "Raw request body", attacker_controlled: true },
23168
+ "php://input": { id: "raw_body", label: "Raw request body", attacker_controlled: true },
23169
+ json_decode: { id: "json_input", label: "JSON input", attacker_controlled: true },
23170
+ getallheaders: { id: "http_header", label: "HTTP header", attacker_controlled: true },
23171
+ $argv: { id: "cli_argument", label: "CLI argument", attacker_controlled: false },
23172
+ getenv: { id: "environment_variable", label: "Environment variable", attacker_controlled: false },
23173
+ "$wpdb->get_results": { id: "database_value", label: "Database value", attacker_controlled: false },
23174
+ wp_remote_get: { id: "external_api", label: "External API data", attacker_controlled: false }
23175
+ };
23176
+ function classifySource(line) {
23177
+ for (const [token, cls] of Object.entries(SOURCES)) {
23178
+ if (line.includes(token))
23179
+ return cls;
23180
+ }
23181
+ return null;
23182
+ }
23183
+ var SINK_CLASS_MAP = [
23184
+ [/->query\(|\$wpdb->query|\$wpdb->get_var|\$wpdb->get_results|->whereRaw|->selectRaw/, { id: "sql_execution", category: "SQL execution", cwe: "CWE-89" }],
23185
+ [/eval\(|assert\(|create_function\(|call_user_func\(|preg_replace\(/, { id: "code_execution", category: "Dynamic evaluation / code execution", cwe: "CWE-94" }],
23186
+ [/system\(|exec\(|shell_exec\(|passthru\(|proc_open\(|popen\(/, { id: "command_execution", category: "Command execution", cwe: "CWE-78" }],
23187
+ [/move_uploaded_file\(|file_put_contents\(|fwrite\(|fopen\(|unlink\(/, { id: "file_operations", category: "File operations", cwe: "CWE-434" }],
23188
+ [/include\(|require\(|include_once\(|require_once\(/, { id: "file_inclusion", category: "File inclusion", cwe: "CWE-98" }],
23189
+ [/unserialize\(|maybe_unserialize\(/, { id: "deserialization", category: "Deserialization", cwe: "CWE-502" }],
23190
+ [/wp_remote_get\(|wp_remote_post\(|file_get_contents\(|curl_exec\(/, { id: "http_request", category: "HTTP request (SSRF)", cwe: "CWE-918" }],
23191
+ [/header\(|wp_redirect\(|wp_safe_redirect\(/, { id: "redirect", category: "Redirect handling", cwe: "CWE-601" }],
23192
+ [/echo\s|print\s|printf\(/, { id: "html_render", category: "HTML rendering (XSS)", cwe: "CWE-79" }],
23193
+ [/simplexml_load_string\(|new SimpleXMLElement|DOMDocument/, { id: "xml_processing", category: "XML processing (XXE)", cwe: "CWE-611" }],
23194
+ [/ZipArchive|PharData|->extractTo\(/, { id: "archive_extraction", category: "Archive extraction (zip slip)", cwe: "CWE-22" }]
23195
+ ];
23196
+ function classifySink2(sink) {
23197
+ for (const [re, cls] of SINK_CLASS_MAP) {
23198
+ if (re.test(sink))
23199
+ return cls;
23200
+ }
23201
+ return null;
23202
+ }
23203
+ var SANITIZERS = {
23204
+ htmlspecialchars: { id: "html_escape", label: "HTML entity encoding", neutralizes: ["html_render"] },
23205
+ esc_html: { id: "html_escape", label: "HTML escape", neutralizes: ["html_render"] },
23206
+ esc_attr: { id: "html_escape", label: "HTML attribute escape", neutralizes: ["html_render"] },
23207
+ htmlentities: { id: "html_escape", label: "HTML entity encoding", neutralizes: ["html_render"] },
23208
+ esc_sql: { id: "sql_escape", label: "SQL escape", neutralizes: ["sql_execution"] },
23209
+ "$wpdb->prepare": { id: "sql_prepare", label: "Prepared SQL statement", neutralizes: ["sql_execution"] },
23210
+ "->prepare(": { id: "sql_prepare", label: "Prepared statement", neutralizes: ["sql_execution"] },
23211
+ filter_var: { id: "filter_var", label: "Filter input", neutralizes: ["html_render", "sql_execution", "command_execution"] },
23212
+ filter_input: { id: "filter_var", label: "Filter input", neutralizes: ["html_render", "sql_execution", "command_execution"] },
23213
+ sanitize_text_field: { id: "wp_sanitize", label: "WordPress sanitize", neutralizes: ["html_render"] },
23214
+ sanitize_file_name: { id: "wp_sanitize_file", label: "File-name sanitize", neutralizes: ["file_operations", "file_inclusion"] },
23215
+ wp_verify_nonce: { id: "nonce_check", label: "Nonce verification", neutralizes: ["authorization"] },
23216
+ check_ajax_referer: { id: "nonce_check", label: "Nonce verification", neutralizes: ["authorization"] },
23217
+ escapeshellarg: { id: "shell_escape", label: "Shell argument escape", neutralizes: ["command_execution"] },
23218
+ escapeshellcmd: { id: "shell_escape", label: "Shell command escape", neutralizes: ["command_execution"] },
23219
+ intval: { id: "int_cast", label: "Integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
23220
+ absint: { id: "int_cast", label: "Absolute integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
23221
+ "preg_replace.*FILTER": { id: "regex_filter", label: "Regex filter", neutralizes: ["html_render"] }
23222
+ };
23223
+ function findSanitizers(line) {
23224
+ const out = [];
23225
+ for (const [token, san] of Object.entries(SANITIZERS)) {
23226
+ if (line.includes(token))
23227
+ out.push(san);
23228
+ }
23229
+ return out;
23230
+ }
23231
+ function isSanitized(sanitizers, sinkCategory) {
23232
+ return sanitizers.some((s) => s.neutralizes.includes(sinkCategory) || s.neutralizes.includes("authorization"));
23233
+ }
23234
+ function traceDataFlow(path) {
23235
+ const scan = scanFile(path);
23236
+ const result = { file: path, candidates: [], sanitized: [], authorized: [] };
23237
+ if (scan.error)
23238
+ return result;
23239
+ let text;
23240
+ try {
23241
+ const { readFileSync } = __require("node:fs");
23242
+ text = readFileSync(path, "utf8");
23243
+ } catch {
23244
+ return result;
23245
+ }
23246
+ const lines = text.split(`
23247
+ `);
23248
+ for (const s of scan.sinks) {
23249
+ const sinkClass = classifySink2(s.sink);
23250
+ if (!sinkClass)
23251
+ continue;
23252
+ const start = Math.max(0, s.line - 15);
23253
+ const window = lines.slice(start, s.line).join(`
23254
+ `);
23255
+ const source = classifySource(window);
23256
+ const sanitizers = findSanitizers(window);
23257
+ const authGates = AUTH_GATES.filter((g) => window.includes(g));
23258
+ const edge = {
23259
+ source,
23260
+ sanitizers,
23261
+ auth_gates: authGates,
23262
+ sink: sinkClass,
23263
+ sink_line: s.line,
23264
+ sink_token: s.sink
23265
+ };
23266
+ if (authGates.length > 0) {
23267
+ result.authorized.push(edge);
23268
+ continue;
23269
+ }
23270
+ if (isSanitized(sanitizers, sinkClass.category)) {
23271
+ result.sanitized.push(edge);
23272
+ continue;
23273
+ }
23274
+ result.candidates.push(edge);
23275
+ }
23276
+ return result;
23277
+ }
23278
+ function groupVariants(results) {
23279
+ const groups = new Map;
23280
+ for (const r of results) {
23281
+ for (const c of r.candidates) {
23282
+ const sig = `${c.sink?.category ?? "?"}|${c.source?.id ?? "unknown"}`;
23283
+ const g = groups.get(sig);
23284
+ if (g) {
23285
+ g.occurrences += 1;
23286
+ if (!g.files.includes(r.file))
23287
+ g.files.push(r.file);
23288
+ } else {
23289
+ groups.set(sig, {
23290
+ signature: sig,
23291
+ root_cause: `unsanitized ${c.source?.label ?? "unknown"} reaching ${c.sink?.category ?? "sink"}`,
23292
+ sink_category: c.sink?.category ?? "?",
23293
+ cwe: c.sink?.cwe,
23294
+ occurrences: 1,
23295
+ files: [r.file]
23296
+ });
23297
+ }
23298
+ }
23299
+ }
23300
+ return [...groups.values()].sort((a, b) => b.occurrences - a.occurrences);
23301
+ }
23302
+
23149
23303
  // src/server.ts
23150
23304
  function createServer() {
23151
23305
  const server = new McpServer({
@@ -23665,6 +23819,35 @@ function createServer() {
23665
23819
  }, async ({ text }) => {
23666
23820
  return { content: [{ type: "text", text: JSON.stringify({ redacted: redactSecrets(text) }) }] };
23667
23821
  });
23822
+ server.registerTool("trace_data_flow", {
23823
+ title: "Trace source-to-sink data flow",
23824
+ description: "EAGLE-EYE: trace data flow in a file — classify sources, sinks, sanitizers, and authorization gates. Returns candidates (reachable+unsanitized), sanitized (not findings), and authorized (lower priority).",
23825
+ inputSchema: { path: string2().describe("Source file path") }
23826
+ }, async ({ path }) => {
23827
+ const r = traceDataFlow(path);
23828
+ return { content: [{ type: "text", text: JSON.stringify(r) }] };
23829
+ });
23830
+ server.registerTool("variant_analysis", {
23831
+ title: "Group findings by root cause",
23832
+ description: "EAGLE-EYE: variant analysis — group data-flow candidates by root cause (sink category + source), deduplicating identical patterns across files.",
23833
+ inputSchema: { path: string2().describe("Source directory path") }
23834
+ }, async ({ path }) => {
23835
+ await Promise.resolve().then(() => init_scanner());
23836
+ const files = iterSourceFiles(path, 5000);
23837
+ const results = files.map((f) => traceDataFlow(f));
23838
+ const groups = groupVariants(results);
23839
+ return {
23840
+ content: [{
23841
+ type: "text",
23842
+ text: JSON.stringify({
23843
+ files_analyzed: files.length,
23844
+ total_candidates: results.reduce((n, r) => n + r.candidates.length, 0),
23845
+ variant_groups: groups.length,
23846
+ groups
23847
+ })
23848
+ }]
23849
+ };
23850
+ });
23668
23851
  return server;
23669
23852
  }
23670
23853
  async function serve() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzstrike",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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": {