blitzstrike 1.0.11 → 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 +682 -258
- package/docs/findings.md +95 -0
- 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";
|
|
@@ -22225,6 +22234,159 @@ function manualForTool(toolName) {
|
|
|
22225
22234
|
return null;
|
|
22226
22235
|
}
|
|
22227
22236
|
|
|
22237
|
+
// src/evidence.ts
|
|
22238
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
22239
|
+
var REDACTION_PATTERNS = [
|
|
22240
|
+
[/(authorization\s*[:=]\s*)(bearer\s+)?[A-Za-z0-9._~+/=-]{8,}/gi, "$1$2[REDACTED]"],
|
|
22241
|
+
[/(api[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
22242
|
+
[/(access[_-]?token\s*[:=]\s*)["']?[A-Za-z0-9._-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
22243
|
+
[/(secret\s*[:=]\s*)["']?[A-Za-z0-9._/-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
22244
|
+
[/(password\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
|
|
22245
|
+
[/(passwd\s*[:=]\s*)["']?[^"'\s,}&]{4,}["']?/gi, "$1[REDACTED]"],
|
|
22246
|
+
[/(private[_-]?key\s*[:=]\s*)["']?[A-Za-z0-9+/=_-]{16,}["']?/gi, "$1[REDACTED]"],
|
|
22247
|
+
[/(session\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
|
|
22248
|
+
[/(cookie\s*[:=]\s*)["']?[A-Za-z0-9._-]{12,}["']?/gi, "$1[REDACTED]"],
|
|
22249
|
+
[/(set-cookie\s*:\s*)[^\r\n]+/gi, "$1[REDACTED]"],
|
|
22250
|
+
[/AKIA[0-9A-Z]{16}/g, "[REDACTED]"],
|
|
22251
|
+
[/ghp_[A-Za-z0-9]{20,}/g, "[REDACTED]"],
|
|
22252
|
+
[/github_pat_[A-Za-z0-9_]{20,}/g, "[REDACTED]"],
|
|
22253
|
+
[/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]"],
|
|
22254
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"]
|
|
22255
|
+
];
|
|
22256
|
+
function redactSecrets(text) {
|
|
22257
|
+
let out = text;
|
|
22258
|
+
for (const [re, repl] of REDACTION_PATTERNS) {
|
|
22259
|
+
out = out.replace(re, repl);
|
|
22260
|
+
}
|
|
22261
|
+
return out;
|
|
22262
|
+
}
|
|
22263
|
+
function redactObject(value) {
|
|
22264
|
+
if (typeof value === "string")
|
|
22265
|
+
return redactSecrets(value);
|
|
22266
|
+
if (Array.isArray(value))
|
|
22267
|
+
return value.map((v) => redactObject(v));
|
|
22268
|
+
if (value && typeof value === "object") {
|
|
22269
|
+
const out = {};
|
|
22270
|
+
for (const [k, v] of Object.entries(value)) {
|
|
22271
|
+
out[k] = redactObject(v);
|
|
22272
|
+
}
|
|
22273
|
+
return out;
|
|
22274
|
+
}
|
|
22275
|
+
return value;
|
|
22276
|
+
}
|
|
22277
|
+
function sha256(text) {
|
|
22278
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
22279
|
+
}
|
|
22280
|
+
var evidenceCounter = 0;
|
|
22281
|
+
function nextEvidenceId() {
|
|
22282
|
+
evidenceCounter += 1;
|
|
22283
|
+
return `EV-${String(evidenceCounter).padStart(6, "0")}`;
|
|
22284
|
+
}
|
|
22285
|
+
function makeEvidence(input) {
|
|
22286
|
+
const artifacts = (input.artifacts ?? []).map((a) => {
|
|
22287
|
+
const content = redactSecrets(a.content);
|
|
22288
|
+
return { name: a.name, kind: a.kind, content, sha256: sha256(content) };
|
|
22289
|
+
});
|
|
22290
|
+
return {
|
|
22291
|
+
evidence_id: nextEvidenceId(),
|
|
22292
|
+
type: input.type,
|
|
22293
|
+
description: redactSecrets(input.description),
|
|
22294
|
+
source: input.source ? redactObject(input.source) : undefined,
|
|
22295
|
+
sink: input.sink ? redactObject(input.sink) : undefined,
|
|
22296
|
+
artifacts,
|
|
22297
|
+
recorded: new Date().toISOString()
|
|
22298
|
+
};
|
|
22299
|
+
}
|
|
22300
|
+
|
|
22301
|
+
// src/finding.ts
|
|
22302
|
+
var LIFECYCLE_TRANSITIONS = {
|
|
22303
|
+
detected: ["triaged", "rejected"],
|
|
22304
|
+
triaged: ["hypothesis", "rejected", "out_of_scope"],
|
|
22305
|
+
hypothesis: ["validating", "false_positive", "rejected", "blocked"],
|
|
22306
|
+
validating: ["confirmed", "false_positive", "blocked", "out_of_scope"],
|
|
22307
|
+
confirmed: [],
|
|
22308
|
+
false_positive: [],
|
|
22309
|
+
rejected: [],
|
|
22310
|
+
blocked: [],
|
|
22311
|
+
out_of_scope: []
|
|
22312
|
+
};
|
|
22313
|
+
function canTransition(from, to) {
|
|
22314
|
+
const allowed = LIFECYCLE_TRANSITIONS[from] ?? [];
|
|
22315
|
+
return allowed.includes(to);
|
|
22316
|
+
}
|
|
22317
|
+
var DEFAULT_WEIGHTS = {
|
|
22318
|
+
static_analysis: 0.2,
|
|
22319
|
+
data_flow: 0.25,
|
|
22320
|
+
reachability: 0.15,
|
|
22321
|
+
preconditions: 0.1,
|
|
22322
|
+
runtime_validation: 0.2,
|
|
22323
|
+
negative_control: 0.1
|
|
22324
|
+
};
|
|
22325
|
+
var confidenceWeights = { ...DEFAULT_WEIGHTS };
|
|
22326
|
+
function computeConfidence(factors) {
|
|
22327
|
+
let score = 0;
|
|
22328
|
+
for (const [key, weight] of Object.entries(confidenceWeights)) {
|
|
22329
|
+
if (factors[key] === true)
|
|
22330
|
+
score += weight;
|
|
22331
|
+
}
|
|
22332
|
+
return Math.round(score * 100) / 100;
|
|
22333
|
+
}
|
|
22334
|
+
function confidenceLevel(score) {
|
|
22335
|
+
if (score >= 0.9)
|
|
22336
|
+
return "confirmed";
|
|
22337
|
+
if (score >= 0.7)
|
|
22338
|
+
return "high_confidence";
|
|
22339
|
+
if (score >= 0.5)
|
|
22340
|
+
return "likely";
|
|
22341
|
+
if (score >= 0.3)
|
|
22342
|
+
return "suspected";
|
|
22343
|
+
return "informational";
|
|
22344
|
+
}
|
|
22345
|
+
var findingCounter = 0;
|
|
22346
|
+
function nextFindingId() {
|
|
22347
|
+
findingCounter += 1;
|
|
22348
|
+
return `BS-${new Date().getUTCFullYear()}-${String(findingCounter).padStart(6, "0")}`;
|
|
22349
|
+
}
|
|
22350
|
+
function makeFinding(input) {
|
|
22351
|
+
const now = new Date().toISOString();
|
|
22352
|
+
const status = input.status ?? "detected";
|
|
22353
|
+
const factors = confidenceFactorsFor(input);
|
|
22354
|
+
const confidence = computeConfidence(factors);
|
|
22355
|
+
return {
|
|
22356
|
+
id: nextFindingId(),
|
|
22357
|
+
status,
|
|
22358
|
+
title: input.title,
|
|
22359
|
+
target: input.target,
|
|
22360
|
+
classification: {
|
|
22361
|
+
severity: input.severity,
|
|
22362
|
+
cwe: input.cwe,
|
|
22363
|
+
cwe_name: input.cweName,
|
|
22364
|
+
cwe_confidence: input.cweConfidence
|
|
22365
|
+
},
|
|
22366
|
+
confidence,
|
|
22367
|
+
confidence_level: confidenceLevel(confidence),
|
|
22368
|
+
source: input.source,
|
|
22369
|
+
flow: input.flow ?? [],
|
|
22370
|
+
sink: input.sink,
|
|
22371
|
+
validation: { performed: false },
|
|
22372
|
+
evidence: [],
|
|
22373
|
+
chain: { id: input.chainId ?? null, name: input.chainName },
|
|
22374
|
+
impact: {},
|
|
22375
|
+
remediation: {},
|
|
22376
|
+
timestamps: { created: now, updated: now }
|
|
22377
|
+
};
|
|
22378
|
+
}
|
|
22379
|
+
function confidenceFactorsFor(_input) {
|
|
22380
|
+
return {
|
|
22381
|
+
static_analysis: true,
|
|
22382
|
+
data_flow: false,
|
|
22383
|
+
reachability: false,
|
|
22384
|
+
preconditions: false,
|
|
22385
|
+
runtime_validation: false,
|
|
22386
|
+
negative_control: false
|
|
22387
|
+
};
|
|
22388
|
+
}
|
|
22389
|
+
|
|
22228
22390
|
// src/orchestrator.ts
|
|
22229
22391
|
var _chainsCache = null;
|
|
22230
22392
|
function loadChains() {
|
|
@@ -22393,15 +22555,41 @@ function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000,
|
|
|
22393
22555
|
}
|
|
22394
22556
|
report.tiers = { ...report.tiers, eagle_eye: { traced_chains: eagle } };
|
|
22395
22557
|
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
22396
|
-
const
|
|
22558
|
+
const matchedSorted = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
|
|
22559
|
+
const findings = matchedSorted.map((f) => {
|
|
22560
|
+
const chain = chainById.get(f.chain_id);
|
|
22561
|
+
const severity = f.severity ?? "medium";
|
|
22562
|
+
const finding = makeFinding({
|
|
22563
|
+
title: f.name,
|
|
22564
|
+
target: { type: "source", path: target },
|
|
22565
|
+
severity,
|
|
22566
|
+
source: { type: "static_sink_signal", name: f.chain_id },
|
|
22567
|
+
sink: { type: f.chain_id, symbol: f.name },
|
|
22568
|
+
chainId: f.chain_id,
|
|
22569
|
+
chainName: f.name,
|
|
22570
|
+
status: "hypothesis"
|
|
22571
|
+
});
|
|
22572
|
+
const ev = makeEvidence({
|
|
22573
|
+
type: "sink_location",
|
|
22574
|
+
description: `Static sink signal matched escalation chain '${f.chain_id}' (${severity}). Hypothesis only — not yet validated.`,
|
|
22575
|
+
sink: { chain_id: f.chain_id, name: f.name, severity },
|
|
22576
|
+
artifacts: [{ name: "chain_match", kind: "json", content: JSON.stringify({ chain_id: f.chain_id, severity, steps: chain?.steps.length ?? 0 }) }]
|
|
22577
|
+
});
|
|
22578
|
+
finding.evidence.push(ev);
|
|
22579
|
+
return finding;
|
|
22580
|
+
});
|
|
22397
22581
|
report.findings = findings.map((f) => ({
|
|
22398
|
-
|
|
22399
|
-
|
|
22400
|
-
|
|
22401
|
-
|
|
22582
|
+
id: f.id,
|
|
22583
|
+
status: f.status,
|
|
22584
|
+
title: f.title,
|
|
22585
|
+
severity: f.classification.severity,
|
|
22586
|
+
confidence: f.confidence,
|
|
22587
|
+
confidence_level: f.confidence_level,
|
|
22588
|
+
chain_id: f.chain.id,
|
|
22589
|
+
evidence_count: f.evidence.length
|
|
22402
22590
|
}));
|
|
22403
22591
|
report.status = "COMPLETE";
|
|
22404
|
-
report.note = "All findings are HYPOTHESES until verified live with strike_verify. " + "Apply each chain's negative_control before reporting.";
|
|
22592
|
+
report.note = "All findings are HYPOTHESES until verified live with strike_verify. " + "Apply each chain's negative_control before reporting. Confidence reflects static evidence only.";
|
|
22405
22593
|
if (remember) {
|
|
22406
22594
|
let captured = 0;
|
|
22407
22595
|
for (const mc of blitz.matched_chains) {
|
|
@@ -22967,6 +23155,151 @@ async function activeScan(target, scope = "", mode = "bug-bounty", allowed = und
|
|
|
22967
23155
|
return result;
|
|
22968
23156
|
}
|
|
22969
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
|
+
|
|
22970
23303
|
// src/server.ts
|
|
22971
23304
|
function createServer() {
|
|
22972
23305
|
const server = new McpServer({
|
|
@@ -23424,6 +23757,97 @@ function createServer() {
|
|
|
23424
23757
|
}, async ({ topic, limit }) => {
|
|
23425
23758
|
return { content: [{ type: "text", text: JSON.stringify(templateLookup(topic, limit ?? 10)) }] };
|
|
23426
23759
|
});
|
|
23760
|
+
server.registerTool("finding_create", {
|
|
23761
|
+
title: "Create a canonical finding",
|
|
23762
|
+
description: "FINDINGS: create a canonical, evidence-first finding. Severity describes impact; confidence (deterministic) describes certainty. Default status=detected (hypothesis-pending).",
|
|
23763
|
+
inputSchema: {
|
|
23764
|
+
title: string2().describe("Finding title"),
|
|
23765
|
+
target_type: string2().optional().describe("Target type (web/api/source/mobile/network/other)"),
|
|
23766
|
+
host: string2().optional().describe("Target host"),
|
|
23767
|
+
endpoint: string2().optional().describe("Target endpoint/path"),
|
|
23768
|
+
severity: string2().describe("Severity: critical/high/medium/low/informational"),
|
|
23769
|
+
cwe: string2().optional().describe("CWE id (e.g. CWE-89)"),
|
|
23770
|
+
source_type: string2().describe("Source type (e.g. request_parameter, post_body, header, cookie, file)"),
|
|
23771
|
+
source_name: string2().describe("Source name"),
|
|
23772
|
+
sink_type: string2().describe("Sink type (e.g. sql_execution, command_execution, file_operations)"),
|
|
23773
|
+
sink_symbol: string2().optional().describe("Sink symbol (e.g. '->query(')"),
|
|
23774
|
+
chain_id: string2().optional().describe("Escalation chain id")
|
|
23775
|
+
}
|
|
23776
|
+
}, async ({ title, target_type, host, endpoint, severity, cwe, source_type, source_name, sink_type, sink_symbol, chain_id }) => {
|
|
23777
|
+
const f = makeFinding({
|
|
23778
|
+
title,
|
|
23779
|
+
target: { type: target_type ?? "web", host, endpoint },
|
|
23780
|
+
severity: severity ?? "medium",
|
|
23781
|
+
cwe,
|
|
23782
|
+
source: { type: source_type, name: source_name },
|
|
23783
|
+
sink: { type: sink_type, symbol: sink_symbol },
|
|
23784
|
+
chainId: chain_id ?? null,
|
|
23785
|
+
status: "detected"
|
|
23786
|
+
});
|
|
23787
|
+
return { content: [{ type: "text", text: JSON.stringify(f) }] };
|
|
23788
|
+
});
|
|
23789
|
+
server.registerTool("finding_transition", {
|
|
23790
|
+
title: "Advance a finding lifecycle",
|
|
23791
|
+
description: "FINDINGS: advance a finding through its strict lifecycle (detected->triaged->hypothesis->validating->confirmed). Rejects illegal transitions.",
|
|
23792
|
+
inputSchema: {
|
|
23793
|
+
status: string2().describe("Current status"),
|
|
23794
|
+
to: string2().describe("Target status (triaged/hypothesis/validating/confirmed/false_positive/rejected/blocked/out_of_scope)")
|
|
23795
|
+
}
|
|
23796
|
+
}, async ({ status, to }) => {
|
|
23797
|
+
const ok = canTransition(status, to);
|
|
23798
|
+
return { content: [{ type: "text", text: JSON.stringify({ from: status, to, legal: ok }) }] };
|
|
23799
|
+
});
|
|
23800
|
+
server.registerTool("confidence_score", {
|
|
23801
|
+
title: "Compute deterministic confidence",
|
|
23802
|
+
description: "FINDINGS: compute a deterministic weighted confidence score (static/data-flow/reachability/preconditions/validation/negative-control). Never an AI opinion.",
|
|
23803
|
+
inputSchema: {
|
|
23804
|
+
static_analysis: boolean2().optional(),
|
|
23805
|
+
data_flow: boolean2().optional(),
|
|
23806
|
+
reachability: boolean2().optional(),
|
|
23807
|
+
preconditions: boolean2().optional(),
|
|
23808
|
+
runtime_validation: boolean2().optional(),
|
|
23809
|
+
negative_control: boolean2().optional()
|
|
23810
|
+
}
|
|
23811
|
+
}, async (factors) => {
|
|
23812
|
+
const score = computeConfidence(factors);
|
|
23813
|
+
return { content: [{ type: "text", text: JSON.stringify({ score, level: confidenceLevel(score) }) }] };
|
|
23814
|
+
});
|
|
23815
|
+
server.registerTool("redact", {
|
|
23816
|
+
title: "Redact secrets from text",
|
|
23817
|
+
description: "EVIDENCE: redact passwords/API keys/tokens/cookies/private keys from text before persisting evidence or reporting.",
|
|
23818
|
+
inputSchema: { text: string2().describe("Text to redact") }
|
|
23819
|
+
}, async ({ text }) => {
|
|
23820
|
+
return { content: [{ type: "text", text: JSON.stringify({ redacted: redactSecrets(text) }) }] };
|
|
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
|
+
});
|
|
23427
23851
|
return server;
|
|
23428
23852
|
}
|
|
23429
23853
|
async function serve() {
|
package/docs/findings.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Findings & Evidence Engine
|
|
2
|
+
|
|
3
|
+
Blitz Strike follows one principle: **a scanner hit is a hypothesis; evidence is
|
|
4
|
+
the verdict.**
|
|
5
|
+
|
|
6
|
+
This document describes the canonical finding model, the strict lifecycle, and
|
|
7
|
+
the deterministic confidence engine. No other part of the codebase invents its
|
|
8
|
+
own finding shape — every producer funnels into these modules.
|
|
9
|
+
|
|
10
|
+
## Finding schema
|
|
11
|
+
|
|
12
|
+
`src/finding.ts` defines the single `Finding` model:
|
|
13
|
+
|
|
14
|
+
| Field | Meaning |
|
|
15
|
+
| :--- | :--- |
|
|
16
|
+
| `id` | Canonical id, `BS-YYYY-NNNNNN` |
|
|
17
|
+
| `status` | Lifecycle state (see below) |
|
|
18
|
+
| `title` | Human-readable title |
|
|
19
|
+
| `target` | `type` (web/api/source/mobile/network/other) + host/endpoint/path |
|
|
20
|
+
| `classification` | severity, CWE id/name/confidence, CVSS (evidence-based) |
|
|
21
|
+
| `confidence` | 0.0–1.0 deterministic score (NOT an AI opinion) |
|
|
22
|
+
| `confidence_level` | informational / suspected / likely / high_confidence / confirmed |
|
|
23
|
+
| `source` | attacker-controlled source (type + name + location) |
|
|
24
|
+
| `flow` | data-flow path (source → transforms → sink) |
|
|
25
|
+
| `sink` | security-sensitive operation (type + symbol + location) |
|
|
26
|
+
| `validation` | performed + baseline + negative_control |
|
|
27
|
+
| `evidence` | array of integrity-tagged evidence records |
|
|
28
|
+
| `chain` | escalation chain id/name |
|
|
29
|
+
| `impact` / `remediation` | impact + fix |
|
|
30
|
+
| `timestamps` | created / updated |
|
|
31
|
+
|
|
32
|
+
## Lifecycle
|
|
33
|
+
|
|
34
|
+
Strict, machine-readable state machine (`transition()` rejects illegal moves):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
detected → triaged → hypothesis → validating → confirmed
|
|
38
|
+
↘ rejected ↘ false_positive
|
|
39
|
+
↘ out_of_scope ↘ blocked
|
|
40
|
+
↘ out_of_scope
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`confirmed`, `false_positive`, `rejected`, `blocked`, `out_of_scope` are
|
|
44
|
+
terminal.
|
|
45
|
+
|
|
46
|
+
## Severity vs confidence
|
|
47
|
+
|
|
48
|
+
- **Severity** = impact (`critical` / `high` / `medium` / `low` / `informational`).
|
|
49
|
+
- **Confidence** = certainty (deterministic 0.0–1.0).
|
|
50
|
+
|
|
51
|
+
They are never combined. "Severity CRITICAL + confidence 0.41" = *potentially
|
|
52
|
+
critical, insufficient evidence*. "Severity MEDIUM + confidence 0.98" = *medium
|
|
53
|
+
impact, highly reliable*.
|
|
54
|
+
|
|
55
|
+
## Confidence engine
|
|
56
|
+
|
|
57
|
+
`computeConfidence()` is deterministic and weighted (configurable via
|
|
58
|
+
`setConfidenceWeights()`):
|
|
59
|
+
|
|
60
|
+
| Factor | Default weight |
|
|
61
|
+
| :--- | :--- |
|
|
62
|
+
| static_analysis | 0.20 |
|
|
63
|
+
| data_flow | 0.25 |
|
|
64
|
+
| reachability | 0.15 |
|
|
65
|
+
| preconditions | 0.10 |
|
|
66
|
+
| runtime_validation | 0.20 |
|
|
67
|
+
| negative_control | 0.10 |
|
|
68
|
+
|
|
69
|
+
Levels: `0.00–0.29` informational · `0.30–0.49` suspected · `0.50–0.69` likely ·
|
|
70
|
+
`0.70–0.89` high_confidence · `0.90–1.00` confirmed.
|
|
71
|
+
|
|
72
|
+
A high confidence score is **never** proof by itself — confirmation requires
|
|
73
|
+
validation + evidence.
|
|
74
|
+
|
|
75
|
+
## Evidence engine
|
|
76
|
+
|
|
77
|
+
`src/evidence.ts`:
|
|
78
|
+
|
|
79
|
+
- **Schema** — `evidence_id`, `type`, `description`, `source`, `sink`, `artifacts[]`, `recorded`.
|
|
80
|
+
- **Integrity** — every artifact carries a SHA-256 (`sha256()`), verified by `verifyEvidence()`.
|
|
81
|
+
- **Secret redaction** — `redactSecrets()` strips passwords, API keys, tokens,
|
|
82
|
+
cookies, `Authorization` headers, private keys before persistence/report/log/context.
|
|
83
|
+
- **Append-only** — confirmed evidence is never silently rewritten.
|
|
84
|
+
|
|
85
|
+
## MCP tools
|
|
86
|
+
|
|
87
|
+
| Tool | Purpose |
|
|
88
|
+
| :--- | :--- |
|
|
89
|
+
| `finding_create` | create a canonical finding (default `detected`) |
|
|
90
|
+
| `finding_transition` | advance lifecycle (rejects illegal moves) |
|
|
91
|
+
| `confidence_score` | deterministic weighted confidence |
|
|
92
|
+
| `redact` | redact secrets from arbitrary text |
|
|
93
|
+
|
|
94
|
+
`run_engagement` now emits canonical findings (status `hypothesis`, carrying
|
|
95
|
+
static + sink evidence) instead of ad-hoc `{chain_id, name, severity}` tuples.
|
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": {
|