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.
- package/dist/index.js +436 -253
- 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/
|
|
21673
|
-
|
|
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", "
|
|
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.
|
|
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": {
|