blitzstrike 1.1.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/dist/index.js +1806 -1686
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29762,9 +29762,9 @@ async function nvdLookup(cveId) {
|
|
|
29762
29762
|
|
|
29763
29763
|
// src/orchestrator.ts
|
|
29764
29764
|
init_scanner();
|
|
29765
|
-
import { readFileSync as
|
|
29766
|
-
import { join as
|
|
29767
|
-
import { fileURLToPath as
|
|
29765
|
+
import { readFileSync as readFileSync7, existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
29766
|
+
import { join as join7 } from "node:path";
|
|
29767
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
29768
29768
|
|
|
29769
29769
|
// src/memory.ts
|
|
29770
29770
|
import { readFileSync as readFileSync2, appendFileSync, mkdirSync, existsSync, writeFileSync } from "node:fs";
|
|
@@ -30786,1084 +30786,1626 @@ function resolveFinding(finding, verdict) {
|
|
|
30786
30786
|
}
|
|
30787
30787
|
}
|
|
30788
30788
|
|
|
30789
|
-
// src/
|
|
30790
|
-
|
|
30791
|
-
|
|
30792
|
-
|
|
30793
|
-
|
|
30794
|
-
|
|
30795
|
-
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
`);
|
|
30789
|
+
// src/intel.ts
|
|
30790
|
+
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "node:fs";
|
|
30791
|
+
import { join as join6 } from "node:path";
|
|
30792
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
30793
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
30794
|
+
import { homedir as homedir3 } from "node:os";
|
|
30795
|
+
var ROOT2 = join6(fileURLToPath3(new URL(".", import.meta.url)), "..");
|
|
30796
|
+
var INTEL = join6(ROOT2, "intelligence");
|
|
30797
|
+
function loadJson(name) {
|
|
30798
|
+
const p = join6(INTEL, `${name}.json`);
|
|
30799
|
+
if (!existsSync4(p))
|
|
30800
|
+
return null;
|
|
30801
|
+
try {
|
|
30802
|
+
return JSON.parse(readFileSync6(p, "utf8"));
|
|
30803
|
+
} catch {
|
|
30804
|
+
return null;
|
|
30805
|
+
}
|
|
30799
30806
|
}
|
|
30800
|
-
function
|
|
30801
|
-
const
|
|
30802
|
-
|
|
30803
|
-
|
|
30804
|
-
|
|
30805
|
-
|
|
30807
|
+
function detectWaf(headers, body = "") {
|
|
30808
|
+
const sigs = loadJson("waf_signatures");
|
|
30809
|
+
if (!sigs)
|
|
30810
|
+
return { detected: false, wafs: [], error: "waf_signatures.json not loaded" };
|
|
30811
|
+
const headerSigs = sigs.header_signatures ?? [];
|
|
30812
|
+
const bodySigs = sigs.body_signatures ?? [];
|
|
30813
|
+
const matches = [];
|
|
30814
|
+
const headerKeys = Object.keys(headers).map((k) => k.toLowerCase());
|
|
30815
|
+
for (const s of headerSigs) {
|
|
30816
|
+
const h = (s.header ?? "").toLowerCase();
|
|
30817
|
+
const pattern = (s.pattern ?? "").toLowerCase();
|
|
30818
|
+
if (!h)
|
|
30819
|
+
continue;
|
|
30820
|
+
if (pattern) {
|
|
30821
|
+
const val = (headers[h] ?? headers[s.header ?? ""] ?? "").toLowerCase();
|
|
30822
|
+
if (val.includes(pattern)) {
|
|
30823
|
+
matches.push({ waf: s.waf, header: s.header, pattern: s.pattern, confidence: s.confidence, via: "header-value" });
|
|
30824
|
+
}
|
|
30825
|
+
} else {
|
|
30826
|
+
if (headerKeys.includes(h)) {
|
|
30827
|
+
matches.push({ waf: s.waf, header: s.header, confidence: s.confidence, via: "header-name" });
|
|
30828
|
+
}
|
|
30829
|
+
}
|
|
30806
30830
|
}
|
|
30807
|
-
|
|
30808
|
-
const
|
|
30809
|
-
|
|
30831
|
+
for (const s of bodySigs) {
|
|
30832
|
+
const pattern = (s.pattern ?? "").toLowerCase();
|
|
30833
|
+
if (pattern && body.toLowerCase().includes(pattern)) {
|
|
30834
|
+
matches.push({ waf: s.waf, pattern: s.pattern, confidence: s.confidence, via: "body" });
|
|
30835
|
+
}
|
|
30810
30836
|
}
|
|
30811
|
-
|
|
30812
|
-
|
|
30837
|
+
const byWaf = new Map;
|
|
30838
|
+
for (const m of matches) {
|
|
30839
|
+
const key = m.waf;
|
|
30840
|
+
const prev = byWaf.get(key);
|
|
30841
|
+
if (!prev || m.confidence > prev.confidence)
|
|
30842
|
+
byWaf.set(key, m);
|
|
30813
30843
|
}
|
|
30814
|
-
|
|
30815
|
-
|
|
30844
|
+
const detected = [...byWaf.values()].sort((a, b) => b.confidence - a.confidence);
|
|
30845
|
+
return {
|
|
30846
|
+
detected: detected.length > 0,
|
|
30847
|
+
wafs: detected,
|
|
30848
|
+
block_status_codes: sigs.block_status_codes ?? []
|
|
30849
|
+
};
|
|
30816
30850
|
}
|
|
30817
|
-
function
|
|
30818
|
-
const
|
|
30819
|
-
|
|
30820
|
-
|
|
30821
|
-
|
|
30822
|
-
|
|
30851
|
+
function techCorrelation(tech) {
|
|
30852
|
+
const data = loadJson("tech_correlations");
|
|
30853
|
+
if (!data)
|
|
30854
|
+
return { found: false, error: "tech_correlations.json not loaded" };
|
|
30855
|
+
const key = tech.toLowerCase();
|
|
30856
|
+
const entry = data[key];
|
|
30857
|
+
if (!entry) {
|
|
30858
|
+
const matches = Object.keys(data).filter((k) => k.includes(key) || key.includes(k)).slice(0, 5);
|
|
30859
|
+
return { found: false, tech, suggestions: matches };
|
|
30823
30860
|
}
|
|
30824
|
-
return
|
|
30825
|
-
`);
|
|
30861
|
+
return { found: true, tech: key, vulns: entry.vulns ?? [], paths: entry.paths ?? [], detection: entry.detection ?? { vulns: entry.vulns, paths: entry.paths } };
|
|
30826
30862
|
}
|
|
30827
|
-
function
|
|
30828
|
-
|
|
30829
|
-
|
|
30830
|
-
|
|
30831
|
-
|
|
30832
|
-
|
|
30833
|
-
|
|
30834
|
-
|
|
30835
|
-
|
|
30836
|
-
|
|
30837
|
-
|
|
30863
|
+
function cveCorrelation(cveId) {
|
|
30864
|
+
const data = loadJson("cve_correlations");
|
|
30865
|
+
if (!data)
|
|
30866
|
+
return { error: "cve_correlations.json not loaded" };
|
|
30867
|
+
const key = cveId.toUpperCase();
|
|
30868
|
+
const entry = data[key];
|
|
30869
|
+
if (!entry)
|
|
30870
|
+
return { found: false, cve: key, total_known: Object.keys(data).length };
|
|
30871
|
+
return { found: true, cve: key, ...entry };
|
|
30872
|
+
}
|
|
30873
|
+
function portCorrelation(port) {
|
|
30874
|
+
const data = loadJson("port_correlations");
|
|
30875
|
+
if (!data)
|
|
30876
|
+
return { error: "port_correlations.json not loaded" };
|
|
30877
|
+
const key = String(port);
|
|
30878
|
+
const entry = data[key];
|
|
30879
|
+
if (!entry)
|
|
30880
|
+
return { found: false, port: key, total_known: Object.keys(data).length };
|
|
30881
|
+
return { found: true, port: key, ...entry };
|
|
30882
|
+
}
|
|
30883
|
+
function fuzzerPayloads(category) {
|
|
30884
|
+
const data = loadJson("fuzzer_data");
|
|
30885
|
+
if (!data)
|
|
30886
|
+
return { error: "fuzzer_data.json not loaded" };
|
|
30887
|
+
if (category) {
|
|
30888
|
+
const c = data[category] ?? data[category.toUpperCase()];
|
|
30889
|
+
return { category, found: Boolean(c), data: c ?? null };
|
|
30838
30890
|
}
|
|
30839
|
-
|
|
30840
|
-
return { kind: "live", target: host ? `https://${target}` : target };
|
|
30891
|
+
return { categories: Object.keys(data), fuzz_points: data.FUZZ_POINTS ?? null, payloads: data.FUZZ_PAYLOADS ?? null };
|
|
30841
30892
|
}
|
|
30842
|
-
|
|
30843
|
-
|
|
30844
|
-
|
|
30845
|
-
|
|
30893
|
+
function intelSummary() {
|
|
30894
|
+
const waf = loadJson("waf_signatures");
|
|
30895
|
+
const tech = loadJson("tech_correlations");
|
|
30896
|
+
const cve = loadJson("cve_correlations");
|
|
30897
|
+
const port = loadJson("port_correlations");
|
|
30898
|
+
const fuzz = loadJson("fuzzer_data");
|
|
30899
|
+
const av = loadJson("attack_vectors");
|
|
30900
|
+
return {
|
|
30901
|
+
waf_signatures: waf ? (waf.header_signatures?.length ?? 0) + (waf.body_signatures?.length ?? 0) : 0,
|
|
30902
|
+
tech_correlations: tech ? Object.keys(tech).length : 0,
|
|
30903
|
+
cve_correlations: cve ? Object.keys(cve).length : 0,
|
|
30904
|
+
port_correlations: port ? Object.keys(port).length : 0,
|
|
30905
|
+
fuzzer_categories: fuzz ? Object.keys(fuzz).length : 0,
|
|
30906
|
+
payload_categories: listPayloadCategories().length,
|
|
30907
|
+
nuclei_templates: countTemplates(),
|
|
30908
|
+
attack_vector_categories: av ? av.categories.length : 0,
|
|
30909
|
+
attack_vectors: av ? av.categories.reduce((n, c) => n + c.vectors.length, 0) : 0
|
|
30910
|
+
};
|
|
30911
|
+
}
|
|
30912
|
+
function listAttackVectors() {
|
|
30913
|
+
const data = loadJson("attack_vectors");
|
|
30914
|
+
if (!data)
|
|
30915
|
+
return { error: "attack_vectors.json not loaded" };
|
|
30916
|
+
return {
|
|
30917
|
+
total_categories: data.categories.length,
|
|
30918
|
+
total_vectors: data.categories.reduce((n, c) => n + c.vectors.length, 0),
|
|
30919
|
+
categories: data.categories.map((c) => ({ id: c.id, title: c.title, count: c.vectors.length, focus: c.focus ?? "" }))
|
|
30920
|
+
};
|
|
30921
|
+
}
|
|
30922
|
+
function attackVectors(category) {
|
|
30923
|
+
const data = loadJson("attack_vectors");
|
|
30924
|
+
if (!data)
|
|
30925
|
+
return { error: "attack_vectors.json not loaded" };
|
|
30926
|
+
const q = category.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
30927
|
+
const found = data.categories.find((c) => c.id === q || c.id.includes(q) || c.title.toLowerCase().includes(category.toLowerCase()));
|
|
30928
|
+
if (!found) {
|
|
30929
|
+
const matches = data.categories.filter((c) => c.title.toLowerCase().includes(category.toLowerCase()) || c.id.includes(q)).slice(0, 5).map((c) => c.id);
|
|
30930
|
+
return { found: false, query: category, suggestions: matches };
|
|
30931
|
+
}
|
|
30932
|
+
return { found: true, id: found.id, title: found.title, focus: found.focus ?? "", count: found.vectors.length, vectors: found.vectors };
|
|
30933
|
+
}
|
|
30934
|
+
var PACKAGE_PAYLOADS = join6(ROOT2, "payloads");
|
|
30935
|
+
var PACKAGE_TEMPLATES = join6(ROOT2, "templates");
|
|
30936
|
+
var DATA_ROOT = process.env.BLITZSTRIKE_DATA ?? join6(homedir3(), ".blitzstrike", "data");
|
|
30937
|
+
function payloadsDir() {
|
|
30938
|
+
if (existsSync4(PACKAGE_PAYLOADS))
|
|
30939
|
+
return PACKAGE_PAYLOADS;
|
|
30940
|
+
return join6(DATA_ROOT, "payloads");
|
|
30941
|
+
}
|
|
30942
|
+
function templatesDir() {
|
|
30943
|
+
if (existsSync4(PACKAGE_TEMPLATES))
|
|
30944
|
+
return PACKAGE_TEMPLATES;
|
|
30945
|
+
return join6(DATA_ROOT, "templates");
|
|
30946
|
+
}
|
|
30947
|
+
var PAYLOADS = payloadsDir();
|
|
30948
|
+
var TEMPLATES = templatesDir();
|
|
30949
|
+
function listPayloadCategories() {
|
|
30846
30950
|
try {
|
|
30847
|
-
|
|
30848
|
-
const path = join6(dir, "chains.json");
|
|
30849
|
-
const raw = JSON.parse(readFileSync6(path, "utf8"));
|
|
30850
|
-
_chainsCache = raw.chains ?? [];
|
|
30851
|
-
return _chainsCache;
|
|
30951
|
+
return readdirSync2(PAYLOADS, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
30852
30952
|
} catch {
|
|
30853
|
-
_chainsCache = [];
|
|
30854
30953
|
return [];
|
|
30855
30954
|
}
|
|
30856
30955
|
}
|
|
30857
|
-
function
|
|
30858
|
-
|
|
30859
|
-
|
|
30956
|
+
function payloadLookup(topic) {
|
|
30957
|
+
const cats = listPayloadCategories();
|
|
30958
|
+
const key = topic.toLowerCase();
|
|
30959
|
+
const ALIASES = {
|
|
30960
|
+
sqli: "sql injection",
|
|
30961
|
+
xss: "xss injection",
|
|
30962
|
+
ssrf: "server side request forgery",
|
|
30963
|
+
ssti: "server side template injection",
|
|
30964
|
+
jwt: "json web token",
|
|
30965
|
+
lfi: "file inclusion",
|
|
30966
|
+
rfi: "file inclusion",
|
|
30967
|
+
xxe: "xxe injection",
|
|
30968
|
+
idor: "insecure direct object references",
|
|
30969
|
+
csrf: "cross-site request forgery",
|
|
30970
|
+
cmdi: "command injection",
|
|
30971
|
+
rce: "command injection",
|
|
30972
|
+
cors: "cors misconfiguration",
|
|
30973
|
+
crlf: "crlf injection",
|
|
30974
|
+
prototype: "prototype pollution",
|
|
30975
|
+
nosql: "nosql injection",
|
|
30976
|
+
upload: "upload insecure files"
|
|
30977
|
+
};
|
|
30978
|
+
const normalized = ALIASES[key] ?? key;
|
|
30979
|
+
const exact = cats.find((c) => c.toLowerCase() === normalized);
|
|
30980
|
+
if (exact) {
|
|
30981
|
+
return { found: true, category: exact, files: listFiles(join6(PAYLOADS, exact)) };
|
|
30860
30982
|
}
|
|
30861
|
-
|
|
30862
|
-
|
|
30863
|
-
|
|
30864
|
-
|
|
30865
|
-
|
|
30866
|
-
|
|
30867
|
-
|
|
30868
|
-
|
|
30869
|
-
|
|
30870
|
-
|
|
30871
|
-
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30983
|
+
const keyTokens = normalized.split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
30984
|
+
const matches = cats.filter((c) => {
|
|
30985
|
+
const cl = c.toLowerCase();
|
|
30986
|
+
if (cl.includes(normalized) || normalized.includes(cl))
|
|
30987
|
+
return true;
|
|
30988
|
+
return keyTokens.some((tok) => new RegExp(`(^|[^a-z0-9])${tok}`).test(cl));
|
|
30989
|
+
});
|
|
30990
|
+
if (matches.length > 0) {
|
|
30991
|
+
return { found: true, category: matches[0], files: listFiles(join6(PAYLOADS, matches[0])), alternatives: matches.slice(0, 10) };
|
|
30992
|
+
}
|
|
30993
|
+
return { found: false, topic, suggestions: cats.slice(0, 20) };
|
|
30994
|
+
}
|
|
30995
|
+
function readPayload(category, file) {
|
|
30996
|
+
const cats = listPayloadCategories();
|
|
30997
|
+
const exact = cats.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
30998
|
+
if (!exact)
|
|
30999
|
+
return { found: false, category, suggestions: cats.slice(0, 20) };
|
|
31000
|
+
const dir = join6(PAYLOADS, exact);
|
|
31001
|
+
const files = listFiles(dir);
|
|
31002
|
+
if (!file) {
|
|
31003
|
+
const readme = files.find((f) => f.toLowerCase() === "readme.md");
|
|
31004
|
+
if (readme) {
|
|
31005
|
+
return { found: true, category: exact, file: readme, content: readFileSync6(join6(dir, readme), "utf8").slice(0, 20000) };
|
|
30877
31006
|
}
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
|
|
30883
|
-
|
|
30884
|
-
|
|
30885
|
-
|
|
30886
|
-
|
|
30887
|
-
|
|
31007
|
+
return { found: true, category: exact, files };
|
|
31008
|
+
}
|
|
31009
|
+
const f = files.find((x) => x.toLowerCase() === file.toLowerCase());
|
|
31010
|
+
if (!f)
|
|
31011
|
+
return { found: false, category: exact, file, suggestions: files };
|
|
31012
|
+
return { found: true, category: exact, file: f, content: readFileSync6(join6(dir, f), "utf8").slice(0, 20000) };
|
|
31013
|
+
}
|
|
31014
|
+
function listFiles(dir) {
|
|
31015
|
+
try {
|
|
31016
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md") || f.endsWith(".txt") || f.endsWith(".yaml")).sort();
|
|
31017
|
+
} catch {
|
|
31018
|
+
return [];
|
|
31019
|
+
}
|
|
31020
|
+
}
|
|
31021
|
+
function countTemplates() {
|
|
31022
|
+
try {
|
|
31023
|
+
let n = 0;
|
|
31024
|
+
const walk = (dir) => {
|
|
31025
|
+
for (const e of readdirSync2(dir, { withFileTypes: true })) {
|
|
31026
|
+
if (e.isDirectory())
|
|
31027
|
+
walk(join6(dir, e.name));
|
|
31028
|
+
else if (e.name.endsWith(".yaml"))
|
|
31029
|
+
n++;
|
|
30888
31030
|
}
|
|
31031
|
+
};
|
|
31032
|
+
walk(TEMPLATES);
|
|
31033
|
+
return n;
|
|
31034
|
+
} catch {
|
|
31035
|
+
return 0;
|
|
31036
|
+
}
|
|
31037
|
+
}
|
|
31038
|
+
function templateLookup(topic, limit = 10) {
|
|
31039
|
+
const key = topic.toLowerCase();
|
|
31040
|
+
const hits = [];
|
|
31041
|
+
const walk = (dir, depth = 0) => {
|
|
31042
|
+
if (depth > 6 || hits.length >= limit * 3)
|
|
31043
|
+
return;
|
|
31044
|
+
let entries;
|
|
31045
|
+
try {
|
|
31046
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
31047
|
+
} catch {
|
|
31048
|
+
return;
|
|
30889
31049
|
}
|
|
30890
|
-
|
|
30891
|
-
|
|
30892
|
-
|
|
30893
|
-
|
|
30894
|
-
|
|
30895
|
-
if (
|
|
30896
|
-
|
|
30897
|
-
|
|
30898
|
-
|
|
31050
|
+
for (const e of entries) {
|
|
31051
|
+
if (e.isDirectory())
|
|
31052
|
+
walk(join6(dir, e.name), depth + 1);
|
|
31053
|
+
else if (e.name.endsWith(".yaml")) {
|
|
31054
|
+
const name = e.name.toLowerCase();
|
|
31055
|
+
if (name.includes(key)) {
|
|
31056
|
+
const full = join6(dir, e.name);
|
|
31057
|
+
let id = "";
|
|
31058
|
+
try {
|
|
31059
|
+
const content = readFileSync6(full, "utf8");
|
|
31060
|
+
const m = content.match(/^\s*id:\s*(.+)$/m);
|
|
31061
|
+
if (m)
|
|
31062
|
+
id = m[1].trim();
|
|
31063
|
+
} catch {}
|
|
31064
|
+
hits.push({ path: full.replace(ROOT2 + "/", ""), name: e.name, id });
|
|
30899
31065
|
}
|
|
30900
31066
|
}
|
|
30901
31067
|
}
|
|
31068
|
+
};
|
|
31069
|
+
walk(TEMPLATES);
|
|
31070
|
+
return { found: hits.length > 0, topic, count: hits.length, templates: hits.slice(0, limit) };
|
|
31071
|
+
}
|
|
31072
|
+
var BYPASS_ALIASES = {
|
|
31073
|
+
"403": "auth",
|
|
31074
|
+
"401": "auth",
|
|
31075
|
+
forbidden: "auth",
|
|
31076
|
+
"auth bypass": "auth",
|
|
31077
|
+
authorization: "auth",
|
|
31078
|
+
acl: "auth",
|
|
31079
|
+
access: "auth",
|
|
31080
|
+
waf: "waf",
|
|
31081
|
+
firewall: "waf",
|
|
31082
|
+
modsecurity: "waf",
|
|
31083
|
+
cloudflare: "waf",
|
|
31084
|
+
filter: "filter",
|
|
31085
|
+
sanitizer: "filter",
|
|
31086
|
+
blacklist: "filter",
|
|
31087
|
+
whitelist: "filter",
|
|
31088
|
+
validation: "filter",
|
|
31089
|
+
input: "filter",
|
|
31090
|
+
"rate limit": "rate_limit",
|
|
31091
|
+
ratelimit: "rate_limit",
|
|
31092
|
+
captcha: "rate_limit",
|
|
31093
|
+
bot: "rate_limit",
|
|
31094
|
+
"bot protection": "rate_limit"
|
|
31095
|
+
};
|
|
31096
|
+
function bypassLookup(defense) {
|
|
31097
|
+
const data = loadJson("bypass_techniques");
|
|
31098
|
+
if (!data)
|
|
31099
|
+
return { found: false, error: "bypass_techniques.json not found" };
|
|
31100
|
+
const cats = data.categories;
|
|
31101
|
+
if (!defense) {
|
|
31102
|
+
return {
|
|
31103
|
+
found: true,
|
|
31104
|
+
categories: cats.map((c) => ({ id: c.id, defense: c.defense, techniques: c.techniques.length }))
|
|
31105
|
+
};
|
|
30902
31106
|
}
|
|
30903
|
-
|
|
30904
|
-
|
|
31107
|
+
const key = defense.toLowerCase();
|
|
31108
|
+
const norm = BYPASS_ALIASES[key] ?? key;
|
|
31109
|
+
const match = cats.find((c) => c.id === norm || c.defense.toLowerCase().includes(norm) || norm.includes(c.id));
|
|
31110
|
+
if (!match)
|
|
31111
|
+
return { found: false, defense, suggestions: cats.map((c) => c.id) };
|
|
31112
|
+
return { found: true, defense: match.defense, techniques: match.techniques };
|
|
30905
31113
|
}
|
|
30906
|
-
function
|
|
30907
|
-
|
|
30908
|
-
|
|
30909
|
-
|
|
30910
|
-
|
|
30911
|
-
if (/(exec\(|system\(|shell_exec|passthru|popen|proc_open)/.test(sink))
|
|
30912
|
-
return "command_injection";
|
|
30913
|
-
if (/(include|require)/.test(sink))
|
|
30914
|
-
return "lfi_unvalidated_include";
|
|
30915
|
-
if (/extract\(/.test(sink))
|
|
30916
|
-
return "extract_without_exstr_skip";
|
|
30917
|
-
if (/(\$wpdb|->query)/.test(sink))
|
|
30918
|
-
return "sql_injection";
|
|
30919
|
-
if (/(wp_remote_get|wp_remote_post|file_get_contents)/.test(sink))
|
|
30920
|
-
return "ssrf_unvalidated_url";
|
|
30921
|
-
if (/(eval\(|assert\(|create_function)/.test(sink))
|
|
30922
|
-
return "unsafe_template_render";
|
|
30923
|
-
return "";
|
|
31114
|
+
function chainLinks() {
|
|
31115
|
+
const data = loadJson("chain_links");
|
|
31116
|
+
if (!data)
|
|
31117
|
+
return { found: false, error: "chain_links.json not found" };
|
|
31118
|
+
return { found: true, total: data.links.length, links: data.links };
|
|
30924
31119
|
}
|
|
30925
|
-
function
|
|
30926
|
-
const
|
|
30927
|
-
|
|
30928
|
-
|
|
30929
|
-
|
|
30930
|
-
|
|
30931
|
-
|
|
30932
|
-
|
|
30933
|
-
|
|
30934
|
-
}
|
|
30935
|
-
for (const s of r.sinks) {
|
|
30936
|
-
hits.push({ type: "sink", file: r.file, line: s.line, detail: `${s.sink} -> ${s.class}` });
|
|
30937
|
-
const kind = classifySink(s.sink);
|
|
30938
|
-
if (kind)
|
|
30939
|
-
sinkKinds.add(kind);
|
|
30940
|
-
}
|
|
31120
|
+
function retryGuidance(signal) {
|
|
31121
|
+
const data = loadJson("retry_guidance");
|
|
31122
|
+
if (!data)
|
|
31123
|
+
return { found: false, error: "retry_guidance.json not found" };
|
|
31124
|
+
if (!signal) {
|
|
31125
|
+
return {
|
|
31126
|
+
found: true,
|
|
31127
|
+
modes: data.modes.map((m) => ({ id: m.id, signal: m.signal, actions: m.actions.length }))
|
|
31128
|
+
};
|
|
30941
31129
|
}
|
|
30942
|
-
const
|
|
30943
|
-
|
|
30944
|
-
|
|
30945
|
-
|
|
30946
|
-
|
|
30947
|
-
|
|
31130
|
+
const ALIASES = {
|
|
31131
|
+
empty: "empty_output",
|
|
31132
|
+
"no output": "empty_output",
|
|
31133
|
+
stale: "empty_output",
|
|
31134
|
+
blocked: "blocked",
|
|
31135
|
+
waf: "blocked",
|
|
31136
|
+
filter: "blocked",
|
|
31137
|
+
firewall: "blocked",
|
|
31138
|
+
sanitized: "sanitized",
|
|
31139
|
+
sanitizer: "sanitized",
|
|
31140
|
+
neutralized: "sanitized",
|
|
31141
|
+
subagent: "subagent_failed",
|
|
31142
|
+
"sub-agent": "subagent_failed",
|
|
31143
|
+
delegate: "subagent_failed",
|
|
31144
|
+
model: "model_failed",
|
|
31145
|
+
"rate limit": "model_failed",
|
|
31146
|
+
ratelimit: "model_failed",
|
|
31147
|
+
quota: "model_failed",
|
|
31148
|
+
context: "model_failed",
|
|
31149
|
+
unconfirmed: "unconfirmed",
|
|
31150
|
+
"not confirmed": "unconfirmed",
|
|
31151
|
+
low: "low_severity",
|
|
31152
|
+
"low severity": "low_severity"
|
|
31153
|
+
};
|
|
31154
|
+
const key = signal.toLowerCase().trim();
|
|
31155
|
+
const norm = ALIASES[key] ?? key.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
31156
|
+
const match = data.modes.find((m) => m.id === norm || norm.includes(m.id) || m.id.includes(norm));
|
|
31157
|
+
if (!match)
|
|
31158
|
+
return { found: false, signal, suggestions: data.modes.map((m) => m.id) };
|
|
31159
|
+
return { found: true, mode: match.id, signal: match.signal, actions: match.actions };
|
|
31160
|
+
}
|
|
31161
|
+
function orchestration(part) {
|
|
31162
|
+
const data = loadJson("orchestration");
|
|
31163
|
+
if (!data)
|
|
31164
|
+
return { found: false, error: "orchestration.json not found" };
|
|
31165
|
+
if (!part) {
|
|
31166
|
+
return {
|
|
31167
|
+
found: true,
|
|
31168
|
+
phases: data.lifecycle.map((p) => ({ phase: p.phase, name: p.name })),
|
|
31169
|
+
team: data.team.map((r) => ({ id: r.id, name: r.name }))
|
|
31170
|
+
};
|
|
30948
31171
|
}
|
|
31172
|
+
const q = part.toLowerCase();
|
|
31173
|
+
if (q === "lifecycle" || q === "phases")
|
|
31174
|
+
return { found: true, lifecycle: data.lifecycle };
|
|
31175
|
+
if (q === "team" || q === "roles")
|
|
31176
|
+
return { found: true, team: data.team };
|
|
31177
|
+
if (q === "handoff")
|
|
31178
|
+
return { found: true, handoff: data.handoff };
|
|
31179
|
+
if (q === "termination" || q === "done")
|
|
31180
|
+
return { found: true, termination: data.termination };
|
|
31181
|
+
const phase = data.lifecycle.find((p) => String(p.phase) === q);
|
|
31182
|
+
if (phase)
|
|
31183
|
+
return { found: true, ...phase };
|
|
31184
|
+
const role = data.team.find((r) => String(r.id) === q);
|
|
31185
|
+
if (role)
|
|
31186
|
+
return { found: true, ...role };
|
|
30949
31187
|
return {
|
|
30950
|
-
|
|
30951
|
-
|
|
30952
|
-
|
|
30953
|
-
sink_signals: [...sinkKinds].sort(),
|
|
30954
|
-
matched_chains: matched
|
|
31188
|
+
found: false,
|
|
31189
|
+
part,
|
|
31190
|
+
suggestions: ["lifecycle", "team", "handoff", "termination", ...data.lifecycle.map((p) => String(p.phase)), ...data.team.map((r) => String(r.id))]
|
|
30955
31191
|
};
|
|
30956
31192
|
}
|
|
30957
|
-
function
|
|
30958
|
-
const
|
|
30959
|
-
|
|
30960
|
-
|
|
30961
|
-
|
|
30962
|
-
|
|
30963
|
-
|
|
30964
|
-
|
|
30965
|
-
|
|
30966
|
-
|
|
30967
|
-
|
|
30968
|
-
try {
|
|
30969
|
-
return existsSync4(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
|
|
30970
|
-
} catch {
|
|
30971
|
-
return false;
|
|
30972
|
-
}
|
|
30973
|
-
})();
|
|
30974
|
-
if (isPath) {
|
|
30975
|
-
report.scope = { target, allowed: true, enforcement: "source-audit", reason: "local source path — no network scope gate" };
|
|
30976
|
-
} else {
|
|
30977
|
-
const sc = scopeCheck(target, scope, mode);
|
|
30978
|
-
report.scope = sc;
|
|
30979
|
-
if (!sc.allowed) {
|
|
30980
|
-
report.status = "BLOCKED";
|
|
30981
|
-
report.reason = sc.reason;
|
|
30982
|
-
return report;
|
|
30983
|
-
}
|
|
30984
|
-
}
|
|
30985
|
-
const blitz = enrichScan(target, maxFiles);
|
|
30986
|
-
report.tiers = {
|
|
30987
|
-
blitz: {
|
|
30988
|
-
files_scanned: blitz.files_scanned,
|
|
30989
|
-
hits: blitz.hits.length,
|
|
30990
|
-
sink_signals: blitz.sink_signals,
|
|
30991
|
-
matched_chains: blitz.matched_chains
|
|
30992
|
-
}
|
|
30993
|
-
};
|
|
30994
|
-
const chains = loadChains();
|
|
30995
|
-
const chainById = new Map(chains.map((c) => [c.id, c]));
|
|
30996
|
-
const eagle = [];
|
|
30997
|
-
for (const mc of blitz.matched_chains.slice(0, 10)) {
|
|
30998
|
-
const chain = chainById.get(mc.chain_id);
|
|
30999
|
-
if (!chain)
|
|
31000
|
-
continue;
|
|
31001
|
-
eagle.push({
|
|
31002
|
-
chain_id: chain.id,
|
|
31003
|
-
name: chain.name,
|
|
31004
|
-
severity: chain.severity,
|
|
31005
|
-
prerequisites: chain.required_findings,
|
|
31006
|
-
recommended_tools: chain.tools ?? [],
|
|
31007
|
-
tool_manuals: (chain.tools ?? []).map((t) => {
|
|
31008
|
-
const m = findManual(t);
|
|
31009
|
-
return m.found ? { tool: t, manual_name: m.name, preview: m.content.slice(0, 600) } : { tool: t, manual_name: null, preview: null };
|
|
31010
|
-
}),
|
|
31011
|
-
steps: chain.steps.map((s) => ({
|
|
31012
|
-
order: s.order,
|
|
31013
|
-
action: s.action,
|
|
31014
|
-
tool_hint: s.tool_hint,
|
|
31015
|
-
success_criteria: s.success_criteria,
|
|
31016
|
-
invariant_check: s.invariant_check,
|
|
31017
|
-
negative_control: s.negative_control
|
|
31018
|
-
}))
|
|
31019
|
-
});
|
|
31193
|
+
function modelFallback(signal) {
|
|
31194
|
+
const data = loadJson("model_fallback");
|
|
31195
|
+
if (!data)
|
|
31196
|
+
return { found: false, error: "model_fallback.json not found" };
|
|
31197
|
+
if (!signal) {
|
|
31198
|
+
return {
|
|
31199
|
+
found: true,
|
|
31200
|
+
error_classes: data.error_classes.map((c) => ({ id: c.id, retryable: c.retryable, backoff: c.backoff })),
|
|
31201
|
+
fallback_chain: data.fallback_chain,
|
|
31202
|
+
backoff: data.backoff
|
|
31203
|
+
};
|
|
31020
31204
|
}
|
|
31021
|
-
|
|
31022
|
-
const
|
|
31023
|
-
|
|
31024
|
-
|
|
31025
|
-
|
|
31026
|
-
|
|
31027
|
-
|
|
31028
|
-
|
|
31029
|
-
|
|
31030
|
-
|
|
31031
|
-
|
|
31032
|
-
|
|
31033
|
-
chainId: f.chain_id,
|
|
31034
|
-
chainName: f.name,
|
|
31035
|
-
status: "hypothesis"
|
|
31036
|
-
});
|
|
31037
|
-
const ev = makeEvidence({
|
|
31038
|
-
type: "sink_location",
|
|
31039
|
-
description: `Static sink signal matched escalation chain '${f.chain_id}' (${severity}). Hypothesis only — not yet validated.`,
|
|
31040
|
-
sink: { chain_id: f.chain_id, name: f.name, severity },
|
|
31041
|
-
artifacts: [{ name: "chain_match", kind: "json", content: JSON.stringify({ chain_id: f.chain_id, severity, steps: chain?.steps.length ?? 0 }) }]
|
|
31042
|
-
});
|
|
31043
|
-
finding.evidence.push(ev);
|
|
31044
|
-
return finding;
|
|
31045
|
-
});
|
|
31046
|
-
report.findings = findings.map((f) => ({
|
|
31047
|
-
id: f.id,
|
|
31048
|
-
status: f.status,
|
|
31049
|
-
title: f.title,
|
|
31050
|
-
severity: f.classification.severity,
|
|
31051
|
-
confidence: f.confidence,
|
|
31052
|
-
confidence_level: f.confidence_level,
|
|
31053
|
-
chain_id: f.chain.id,
|
|
31054
|
-
evidence_count: f.evidence.length
|
|
31055
|
-
}));
|
|
31056
|
-
const reportTitle = `Blitz Strike — Engagement Report (${target})`;
|
|
31057
|
-
const reportMarkdownText = reportMarkdown(findings, { title: reportTitle, scope: target, version: VERSION });
|
|
31058
|
-
const reportJsonText = reportJson(findings, { title: reportTitle, scope: target, version: VERSION });
|
|
31059
|
-
const sarifDoc = toSarif(findings.map((f) => ({
|
|
31060
|
-
ruleId: f.chain.id || f.classification.severity,
|
|
31061
|
-
level: sarifLevel(f.classification.severity),
|
|
31062
|
-
message: f.title,
|
|
31063
|
-
file: typeof f.target.path === "string" ? f.target.path : target,
|
|
31064
|
-
line: 1
|
|
31065
|
-
})));
|
|
31066
|
-
report.report = {
|
|
31067
|
-
summary: JSON.parse(reportJsonText).summary ?? null,
|
|
31068
|
-
markdown: reportMarkdownText,
|
|
31069
|
-
json: reportJsonText,
|
|
31070
|
-
sarif: sarifDoc
|
|
31071
|
-
};
|
|
31072
|
-
report.status = "COMPLETE";
|
|
31073
|
-
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.";
|
|
31074
|
-
const executedState = "select_chain";
|
|
31075
|
-
const scopeAllowed = report.scope?.allowed ?? false;
|
|
31076
|
-
const plan = buildPlan(scopeAllowed, mode, executedState);
|
|
31077
|
-
report.state = executedState;
|
|
31078
|
-
report.state_description = stateDefinition(executedState).description;
|
|
31079
|
-
report.plan = plan;
|
|
31080
|
-
report.guidance = computeGuidance(executedState, scopeAllowed);
|
|
31081
|
-
report.risk_policy = {
|
|
31082
|
-
note: "High-risk actions (strike_verify, confirm_finding) require scope + are never run server-side automatically.",
|
|
31083
|
-
gated: plan.filter((p) => !p.permitted).map((p) => p.tool)
|
|
31084
|
-
};
|
|
31085
|
-
if (remember) {
|
|
31086
|
-
let captured = 0;
|
|
31087
|
-
for (const mc of blitz.matched_chains) {
|
|
31088
|
-
const chain = chainById.get(mc.chain_id);
|
|
31089
|
-
const content = `Chain '${mc.chain_id}' (${mc.severity}) matched on target '${target}'. ` + `Detected sink signals: ${blitz.sink_signals.join(", ")}. ` + `Prerequisites: ${(chain?.required_findings ?? []).join(", ")}.`;
|
|
31090
|
-
if (rememberIfAbsent(mc.chain_id, content, "pattern", [mc.severity, "chain"], "engagement", false)) {
|
|
31091
|
-
captured++;
|
|
31092
|
-
}
|
|
31205
|
+
const q = signal.toLowerCase();
|
|
31206
|
+
for (const c of data.error_classes) {
|
|
31207
|
+
if (c.signals.some((s) => q.includes(s.toLowerCase()))) {
|
|
31208
|
+
return {
|
|
31209
|
+
found: true,
|
|
31210
|
+
class: c.id,
|
|
31211
|
+
retryable: c.retryable,
|
|
31212
|
+
backoff: c.backoff,
|
|
31213
|
+
action: c.action,
|
|
31214
|
+
fallback_chain: data.fallback_chain,
|
|
31215
|
+
backoff_schedule: c.backoff ? data.backoff : null
|
|
31216
|
+
};
|
|
31093
31217
|
}
|
|
31094
|
-
report.memory = { captured, deduped: blitz.matched_chains.length - captured };
|
|
31095
31218
|
}
|
|
31096
|
-
return
|
|
31219
|
+
return { found: false, signal, suggestions: data.error_classes.map((c) => c.id) };
|
|
31097
31220
|
}
|
|
31098
|
-
function
|
|
31099
|
-
const
|
|
31221
|
+
function doctrineMap(part) {
|
|
31222
|
+
const data = loadJson("doctrine_map");
|
|
31223
|
+
if (!data)
|
|
31224
|
+
return { found: false, error: "doctrine_map.json not found" };
|
|
31225
|
+
if (!part) {
|
|
31226
|
+
return {
|
|
31227
|
+
found: true,
|
|
31228
|
+
phases: data.phases.map((p) => ({ phase: p.phase, skill: p.skill, wstg: p.wstg })),
|
|
31229
|
+
cross_cutting: data.cross_cutting
|
|
31230
|
+
};
|
|
31231
|
+
}
|
|
31232
|
+
const q = part.toLowerCase();
|
|
31233
|
+
const phase = data.phases.find((p) => String(p.phase) === q);
|
|
31234
|
+
if (phase)
|
|
31235
|
+
return { found: true, ...phase };
|
|
31236
|
+
if (q === "cross_cutting" || q === "cross" || q === "moments") {
|
|
31237
|
+
return { found: true, cross_cutting: data.cross_cutting };
|
|
31238
|
+
}
|
|
31100
31239
|
return {
|
|
31101
|
-
|
|
31102
|
-
|
|
31103
|
-
|
|
31104
|
-
name: c.name,
|
|
31105
|
-
severity: c.severity,
|
|
31106
|
-
steps: c.steps.length,
|
|
31107
|
-
triggers: c.triggers
|
|
31108
|
-
}))
|
|
31240
|
+
found: false,
|
|
31241
|
+
part,
|
|
31242
|
+
suggestions: ["cross_cutting", ...data.phases.map((p) => String(p.phase))]
|
|
31109
31243
|
};
|
|
31110
31244
|
}
|
|
31111
|
-
|
|
31112
|
-
|
|
31113
|
-
|
|
31114
|
-
|
|
31115
|
-
|
|
31116
|
-
|
|
31117
|
-
|
|
31118
|
-
|
|
31119
|
-
|
|
31120
|
-
title: `Reflected input in parameter '${param}'`,
|
|
31121
|
-
target: { type: "web", host: url, endpoint: `?${param}=` },
|
|
31122
|
-
severity: "medium",
|
|
31123
|
-
cwe: "CWE-79",
|
|
31124
|
-
cweName: "Cross-site Scripting",
|
|
31125
|
-
source: { type: "request_parameter", name: param },
|
|
31126
|
-
sink: { type: "reflected_input", symbol: param },
|
|
31127
|
-
chainId: "reflected_input",
|
|
31128
|
-
chainName: "Reflected input",
|
|
31129
|
-
status: "hypothesis"
|
|
31130
|
-
});
|
|
31131
|
-
findings.push(resolveFinding(finding, verdict));
|
|
31245
|
+
function wstgMap(category) {
|
|
31246
|
+
const data = loadJson("wstg_map");
|
|
31247
|
+
if (!data)
|
|
31248
|
+
return { found: false, error: "wstg_map.json not found" };
|
|
31249
|
+
if (!category) {
|
|
31250
|
+
return {
|
|
31251
|
+
found: true,
|
|
31252
|
+
categories: data.categories.map((c) => ({ wstg: c.wstg, name: c.name, phase: c.phase, skill: c.skill }))
|
|
31253
|
+
};
|
|
31132
31254
|
}
|
|
31133
|
-
|
|
31134
|
-
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
confidence: f.confidence,
|
|
31139
|
-
confidence_level: f.confidence_level,
|
|
31140
|
-
chain_id: f.chain.id,
|
|
31141
|
-
validation: f.validation
|
|
31142
|
-
}));
|
|
31143
|
-
const title = `Blitz Strike — Live Engagement (${url})`;
|
|
31144
|
-
const markdown = reportMarkdown(findings, { title, scope: url, version: VERSION });
|
|
31145
|
-
const json = reportJson(findings, { title, scope: url, version: VERSION });
|
|
31146
|
-
report.report = {
|
|
31147
|
-
summary: JSON.parse(json).summary ?? null,
|
|
31148
|
-
markdown,
|
|
31149
|
-
json,
|
|
31150
|
-
sarif: toSarif(findings.map((f) => ({
|
|
31151
|
-
ruleId: f.chain.id || f.classification.cwe || f.classification.severity,
|
|
31152
|
-
level: sarifLevel(f.classification.severity),
|
|
31153
|
-
message: f.title,
|
|
31154
|
-
file: url,
|
|
31155
|
-
line: 1
|
|
31156
|
-
})))
|
|
31157
|
-
};
|
|
31158
|
-
report.status = "COMPLETE";
|
|
31159
|
-
report.note = "Autonomous live marker sweep. Each parameter was validated with a marker + " + "negative control; confirmed reflections are real findings, everything else " + "is a hypothesis or false positive. Re-run with explicit strike_verify for " + "deeper per-endpoint validation.";
|
|
31160
|
-
return report;
|
|
31255
|
+
const q = category.toUpperCase();
|
|
31256
|
+
const c = data.categories.find((x) => String(x.wstg).toUpperCase() === q || String(x.name).toLowerCase().includes(category.toLowerCase()) || String(x.wstg).toUpperCase().includes(q));
|
|
31257
|
+
if (c)
|
|
31258
|
+
return { found: true, ...c };
|
|
31259
|
+
return { found: false, category, suggestions: data.categories.map((x) => String(x.wstg)) };
|
|
31161
31260
|
}
|
|
31162
|
-
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
|
|
31167
|
-
|
|
31168
|
-
|
|
31169
|
-
|
|
31170
|
-
|
|
31171
|
-
function loadTools() {
|
|
31172
|
-
if (_toolsCache)
|
|
31173
|
-
return _toolsCache;
|
|
31174
|
-
try {
|
|
31175
|
-
const raw = JSON.parse(readFileSync7(join7(ROOT2, "tools-catalog.json"), "utf8"));
|
|
31176
|
-
_toolsCache = raw.tools ?? [];
|
|
31177
|
-
} catch {
|
|
31178
|
-
_toolsCache = [];
|
|
31261
|
+
function techniqueLookup(cls) {
|
|
31262
|
+
const data = loadJson("techniques");
|
|
31263
|
+
if (!data)
|
|
31264
|
+
return { found: false, error: "techniques.json not found" };
|
|
31265
|
+
if (!cls) {
|
|
31266
|
+
return {
|
|
31267
|
+
found: true,
|
|
31268
|
+
classes: data.classes.map((c) => ({ id: c.id, name: c.name }))
|
|
31269
|
+
};
|
|
31179
31270
|
}
|
|
31180
|
-
|
|
31271
|
+
const q = cls.toLowerCase();
|
|
31272
|
+
const c = data.classes.find((x) => String(x.id).toLowerCase() === q || String(x.id).toLowerCase().includes(q) || String(x.name).toLowerCase().includes(q));
|
|
31273
|
+
if (c)
|
|
31274
|
+
return { found: true, ...c };
|
|
31275
|
+
return { found: false, cls, suggestions: data.classes.map((x) => String(x.id)) };
|
|
31181
31276
|
}
|
|
31182
|
-
function
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
|
|
31193
|
-
|
|
31194
|
-
out.push({
|
|
31195
|
-
name: f.replace(/\.md$/, ""),
|
|
31196
|
-
file: f,
|
|
31197
|
-
description: (descMatch?.[1] ?? "").trim().slice(0, 200),
|
|
31198
|
-
size: text.length
|
|
31199
|
-
});
|
|
31200
|
-
}
|
|
31201
|
-
} catch {}
|
|
31202
|
-
_skillsCache = out;
|
|
31203
|
-
return out;
|
|
31277
|
+
function frameworkTricks(framework) {
|
|
31278
|
+
const data = loadJson("framework_tricks");
|
|
31279
|
+
if (!data)
|
|
31280
|
+
return { found: false, error: "framework_tricks.json not found" };
|
|
31281
|
+
if (!framework) {
|
|
31282
|
+
return { found: true, frameworks: data.frameworks.map((f) => ({ framework: f.framework })) };
|
|
31283
|
+
}
|
|
31284
|
+
const q = framework.toLowerCase();
|
|
31285
|
+
const f = data.frameworks.find((x) => String(x.framework).toLowerCase().includes(q));
|
|
31286
|
+
if (f)
|
|
31287
|
+
return { found: true, ...f };
|
|
31288
|
+
return { found: false, framework, suggestions: data.frameworks.map((x) => String(x.framework)) };
|
|
31204
31289
|
}
|
|
31205
|
-
function
|
|
31206
|
-
const
|
|
31207
|
-
|
|
31208
|
-
|
|
31290
|
+
function resourceLookup(category) {
|
|
31291
|
+
const data = loadJson("resources");
|
|
31292
|
+
if (!data)
|
|
31293
|
+
return { found: false, error: "resources.json not found" };
|
|
31294
|
+
if (!category) {
|
|
31209
31295
|
return {
|
|
31210
|
-
found:
|
|
31211
|
-
|
|
31212
|
-
suggestions: tools.filter((x) => x.name.includes(name.toLowerCase()) || name.toLowerCase().includes(x.name)).slice(0, 5).map((x) => x.name)
|
|
31213
|
-
};
|
|
31214
|
-
}
|
|
31215
|
-
const manual = manualForTool(t.name);
|
|
31216
|
-
const result = { found: true, tool: t };
|
|
31217
|
-
if (manual && manual.content) {
|
|
31218
|
-
result.manual = {
|
|
31219
|
-
name: manual.name,
|
|
31220
|
-
category: manual.category,
|
|
31221
|
-
preview: manual.content.slice(0, 1500),
|
|
31222
|
-
full_length: manual.content.length
|
|
31296
|
+
found: true,
|
|
31297
|
+
resources: data.resources.map((r) => ({ id: r.id, name: r.name, category: r.category, use_when: r.use_when }))
|
|
31223
31298
|
};
|
|
31224
|
-
} else {
|
|
31225
|
-
result.manual = null;
|
|
31226
31299
|
}
|
|
31227
|
-
|
|
31300
|
+
const q = category.toLowerCase();
|
|
31301
|
+
const matches = data.resources.filter((r) => String(r.category).toLowerCase() === q || String(r.id).toLowerCase().includes(q) || String(r.name).toLowerCase().includes(q) || String(r.use_when).toLowerCase().includes(q));
|
|
31302
|
+
if (matches.length > 0)
|
|
31303
|
+
return { found: true, matches };
|
|
31304
|
+
return { found: false, category, suggestions: [...new Set(data.resources.map((r) => String(r.category)))] };
|
|
31228
31305
|
}
|
|
31229
|
-
function
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31306
|
+
function taxonomy(kind, query) {
|
|
31307
|
+
if (kind === "api" || kind === "api_top10" || kind === "owasp-api") {
|
|
31308
|
+
const data = loadJson("api_top10");
|
|
31309
|
+
if (!data)
|
|
31310
|
+
return { found: false, error: "api_top10.json not found" };
|
|
31311
|
+
if (!query)
|
|
31312
|
+
return { found: true, risks: data.risks };
|
|
31313
|
+
const q = query.toLowerCase();
|
|
31314
|
+
const r = data.risks.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31315
|
+
if (r)
|
|
31316
|
+
return { found: true, ...r };
|
|
31317
|
+
return { found: false, query, suggestions: data.risks.map((x) => String(x.id)) };
|
|
31234
31318
|
}
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
31238
|
-
|
|
31239
|
-
|
|
31240
|
-
|
|
31241
|
-
const
|
|
31242
|
-
const
|
|
31243
|
-
|
|
31244
|
-
|
|
31245
|
-
return {
|
|
31246
|
-
}
|
|
31247
|
-
if (
|
|
31248
|
-
|
|
31319
|
+
if (kind === "cwe" || kind === "weakness") {
|
|
31320
|
+
const data = loadJson("cwe_map");
|
|
31321
|
+
if (!data)
|
|
31322
|
+
return { found: false, error: "cwe_map.json not found" };
|
|
31323
|
+
if (!query)
|
|
31324
|
+
return { found: true, mapping: data.mapping };
|
|
31325
|
+
const q = query.toLowerCase();
|
|
31326
|
+
const m = data.mapping.filter((x) => String(x.id).toLowerCase().includes(q) || String(x.cwe).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31327
|
+
if (m.length > 0)
|
|
31328
|
+
return { found: true, matches: m };
|
|
31329
|
+
return { found: false, query, suggestions: data.mapping.map((x) => String(x.id)) };
|
|
31330
|
+
}
|
|
31331
|
+
if (kind === "web" || kind === "owasp" || kind === "top10" || kind === "web_top10") {
|
|
31332
|
+
const data = loadJson("owasp_top10");
|
|
31333
|
+
if (!data)
|
|
31334
|
+
return { found: false, error: "owasp_top10.json not found" };
|
|
31335
|
+
if (!query)
|
|
31336
|
+
return { found: true, risks: data.risks };
|
|
31337
|
+
const q = query.toLowerCase();
|
|
31338
|
+
const r = data.risks.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31339
|
+
if (r)
|
|
31340
|
+
return { found: true, ...r };
|
|
31341
|
+
return { found: false, query, suggestions: data.risks.map((x) => String(x.id)) };
|
|
31342
|
+
}
|
|
31343
|
+
if (kind === "asvs" || kind === "verification") {
|
|
31344
|
+
const data = loadJson("asvs");
|
|
31345
|
+
if (!data)
|
|
31346
|
+
return { found: false, error: "asvs.json not found" };
|
|
31347
|
+
if (!query)
|
|
31348
|
+
return { found: true, chapters: data.chapters };
|
|
31349
|
+
const q = query.toLowerCase();
|
|
31350
|
+
const c = data.chapters.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31351
|
+
if (c)
|
|
31352
|
+
return { found: true, ...c };
|
|
31353
|
+
return { found: false, query, suggestions: data.chapters.map((x) => String(x.id)) };
|
|
31249
31354
|
}
|
|
31250
31355
|
return {
|
|
31251
|
-
found:
|
|
31252
|
-
|
|
31356
|
+
found: false,
|
|
31357
|
+
error: "unknown taxonomy kind",
|
|
31358
|
+
usage: "taxonomy(kind='web'|'api'|'cwe'|'asvs', query?)"
|
|
31253
31359
|
};
|
|
31254
31360
|
}
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
}
|
|
31259
|
-
|
|
31260
|
-
|
|
31261
|
-
const
|
|
31262
|
-
|
|
31263
|
-
return { found: false, query: name };
|
|
31361
|
+
|
|
31362
|
+
// src/live-recon.ts
|
|
31363
|
+
import { resolve4 } from "node:dns/promises";
|
|
31364
|
+
import { connect } from "node:net";
|
|
31365
|
+
var HTTP_TIMEOUT_MS = 20000;
|
|
31366
|
+
async function httpGet(url, opts = {}) {
|
|
31367
|
+
const controller = new AbortController;
|
|
31368
|
+
const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
31264
31369
|
try {
|
|
31265
|
-
const
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31370
|
+
const res = await fetch(url, { signal: controller.signal, redirect: opts.redirect ?? "follow" });
|
|
31371
|
+
const body = await res.text();
|
|
31372
|
+
const headers = {};
|
|
31373
|
+
res.headers.forEach((v, k) => {
|
|
31374
|
+
headers[k] = v;
|
|
31375
|
+
});
|
|
31376
|
+
return { status: res.status, headers, body: body.slice(0, 50000), ok: res.ok };
|
|
31377
|
+
} catch (e) {
|
|
31378
|
+
return { status: 0, headers: {}, body: String(e), ok: false };
|
|
31379
|
+
} finally {
|
|
31380
|
+
clearTimeout(t);
|
|
31269
31381
|
}
|
|
31270
31382
|
}
|
|
31271
|
-
function
|
|
31272
|
-
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31383
|
+
function normalizeTarget(target) {
|
|
31384
|
+
let t = target.trim();
|
|
31385
|
+
if (!/^https?:\/\//i.test(t))
|
|
31386
|
+
t = `https://${t}`;
|
|
31387
|
+
return t.replace(/\/+$/, "");
|
|
31388
|
+
}
|
|
31389
|
+
function crawlLinks(base, body) {
|
|
31390
|
+
const links = [];
|
|
31391
|
+
const scripts = [];
|
|
31392
|
+
const hrefRe = /<a[^>]+href=["']([^"'#]+)["']/gi;
|
|
31393
|
+
const scriptRe = /<script[^>]+src=["']([^"'#]+)["']/gi;
|
|
31394
|
+
let m;
|
|
31395
|
+
while ((m = hrefRe.exec(body)) !== null) {
|
|
31396
|
+
let u = m[1];
|
|
31397
|
+
if (u.startsWith("/"))
|
|
31398
|
+
u = base + u;
|
|
31399
|
+
else if (!/^https?:\/\//i.test(u))
|
|
31400
|
+
u = base + "/" + u;
|
|
31401
|
+
links.push(u);
|
|
31276
31402
|
}
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31403
|
+
while ((m = scriptRe.exec(body)) !== null) {
|
|
31404
|
+
let u = m[1];
|
|
31405
|
+
if (u.startsWith("/"))
|
|
31406
|
+
u = base + u;
|
|
31407
|
+
else if (!/^https?:\/\//i.test(u))
|
|
31408
|
+
u = base + "/" + u;
|
|
31409
|
+
scripts.push(u);
|
|
31280
31410
|
}
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31411
|
+
return { links, scripts };
|
|
31412
|
+
}
|
|
31413
|
+
async function fetchRobotsAndSitemap(base) {
|
|
31414
|
+
const robots = [];
|
|
31415
|
+
const sitemap = [];
|
|
31416
|
+
const rob = await httpGet(`${base}/robots.txt`);
|
|
31417
|
+
if (rob.status === 200) {
|
|
31418
|
+
for (const line of rob.body.split(`
|
|
31419
|
+
`)) {
|
|
31420
|
+
const sm = line.match(/^Sitemap:\s*(.+)$/i);
|
|
31421
|
+
if (sm)
|
|
31422
|
+
sitemap.push(sm[1].trim());
|
|
31423
|
+
const allow = line.match(/^(?:Allow|Disallow):\s*(.+)$/i);
|
|
31424
|
+
if (allow && !allow[1].includes("*"))
|
|
31425
|
+
robots.push(base + allow[1].trim());
|
|
31426
|
+
}
|
|
31285
31427
|
}
|
|
31286
|
-
const
|
|
31287
|
-
|
|
31288
|
-
|
|
31289
|
-
|
|
31290
|
-
|
|
31291
|
-
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
};
|
|
31428
|
+
const sm = await httpGet(`${base}/sitemap.xml`);
|
|
31429
|
+
if (sm.status === 200 && sm.body.includes("<loc>")) {
|
|
31430
|
+
const locRe = /<loc>([^<]+)<\/loc>/gi;
|
|
31431
|
+
let m;
|
|
31432
|
+
while ((m = locRe.exec(sm.body)) !== null)
|
|
31433
|
+
sitemap.push(m[1].trim());
|
|
31434
|
+
}
|
|
31435
|
+
return { robots, sitemap };
|
|
31295
31436
|
}
|
|
31296
|
-
|
|
31297
|
-
|
|
31298
|
-
|
|
31299
|
-
|
|
31300
|
-
|
|
31301
|
-
|
|
31302
|
-
|
|
31303
|
-
|
|
31304
|
-
var INTEL = join8(ROOT3, "intelligence");
|
|
31305
|
-
function loadJson(name) {
|
|
31306
|
-
const p = join8(INTEL, `${name}.json`);
|
|
31307
|
-
if (!existsSync5(p))
|
|
31308
|
-
return null;
|
|
31309
|
-
try {
|
|
31310
|
-
return JSON.parse(readFileSync8(p, "utf8"));
|
|
31311
|
-
} catch {
|
|
31312
|
-
return null;
|
|
31437
|
+
function extractParams(urls) {
|
|
31438
|
+
const params = new Set;
|
|
31439
|
+
for (const u of urls) {
|
|
31440
|
+
try {
|
|
31441
|
+
const q = new URL(u).searchParams;
|
|
31442
|
+
for (const k of q.keys())
|
|
31443
|
+
params.add(k);
|
|
31444
|
+
} catch {}
|
|
31313
31445
|
}
|
|
31446
|
+
return [...params];
|
|
31314
31447
|
}
|
|
31315
|
-
function
|
|
31316
|
-
const
|
|
31317
|
-
|
|
31318
|
-
|
|
31319
|
-
|
|
31320
|
-
|
|
31321
|
-
|
|
31322
|
-
|
|
31323
|
-
|
|
31324
|
-
|
|
31325
|
-
|
|
31326
|
-
|
|
31448
|
+
async function subdomainEnum(domain) {
|
|
31449
|
+
const out = new Set;
|
|
31450
|
+
try {
|
|
31451
|
+
const url = `https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`;
|
|
31452
|
+
const r = await httpGet(url);
|
|
31453
|
+
if (r.status === 200 && r.body.startsWith("[")) {
|
|
31454
|
+
const data = JSON.parse(r.body);
|
|
31455
|
+
for (const entry of data) {
|
|
31456
|
+
for (const field of (entry.name_value ?? "").split(`
|
|
31457
|
+
`)) {
|
|
31458
|
+
const name = field.trim().toLowerCase();
|
|
31459
|
+
if (name && name.endsWith(domain.toLowerCase()) && !name.includes("*"))
|
|
31460
|
+
out.add(name);
|
|
31461
|
+
}
|
|
31462
|
+
}
|
|
31463
|
+
}
|
|
31464
|
+
} catch {}
|
|
31465
|
+
return { subdomains: [...out].sort(), source: "crt.sh (certificate transparency)" };
|
|
31466
|
+
}
|
|
31467
|
+
async function waybackUrls(domain) {
|
|
31468
|
+
const out = new Set;
|
|
31469
|
+
const url = `https://web.archive.org/cdx/search/cdx?url=*.${encodeURIComponent(domain)}/*&output=json&fl=original&collapse=urlkey&limit=500`;
|
|
31470
|
+
try {
|
|
31471
|
+
const controller = new AbortController;
|
|
31472
|
+
const t = setTimeout(() => controller.abort(), 60000);
|
|
31473
|
+
const res = await fetch(url, { signal: controller.signal, redirect: "manual" });
|
|
31474
|
+
const body = await res.text();
|
|
31475
|
+
clearTimeout(t);
|
|
31476
|
+
if (body.startsWith("[[")) {
|
|
31477
|
+
const data = JSON.parse(body);
|
|
31478
|
+
for (let i = 1;i < data.length; i++) {
|
|
31479
|
+
const u = data[i]?.[0];
|
|
31480
|
+
if (u && typeof u === "string" && u.includes(domain))
|
|
31481
|
+
out.add(u);
|
|
31482
|
+
}
|
|
31483
|
+
}
|
|
31484
|
+
} catch {}
|
|
31485
|
+
return { urls: [...out].slice(0, 500), source: "web.archive.org (Wayback CDX)" };
|
|
31486
|
+
}
|
|
31487
|
+
var COMMON_SUBDOMAINS = [
|
|
31488
|
+
"www",
|
|
31489
|
+
"mail",
|
|
31490
|
+
"ftp",
|
|
31491
|
+
"api",
|
|
31492
|
+
"dev",
|
|
31493
|
+
"staging",
|
|
31494
|
+
"test",
|
|
31495
|
+
"admin",
|
|
31496
|
+
"portal",
|
|
31497
|
+
"app",
|
|
31498
|
+
"blog",
|
|
31499
|
+
"shop",
|
|
31500
|
+
"store",
|
|
31501
|
+
"cdn",
|
|
31502
|
+
"static",
|
|
31503
|
+
"assets",
|
|
31504
|
+
"img",
|
|
31505
|
+
"images",
|
|
31506
|
+
"secure",
|
|
31507
|
+
"vpn",
|
|
31508
|
+
"remote",
|
|
31509
|
+
"webmail",
|
|
31510
|
+
"smtp",
|
|
31511
|
+
"pop",
|
|
31512
|
+
"imap",
|
|
31513
|
+
"ns1",
|
|
31514
|
+
"ns2",
|
|
31515
|
+
"dns",
|
|
31516
|
+
"db",
|
|
31517
|
+
"database",
|
|
31518
|
+
"mysql",
|
|
31519
|
+
"git",
|
|
31520
|
+
"gitlab",
|
|
31521
|
+
"jenkins",
|
|
31522
|
+
"ci",
|
|
31523
|
+
"jira",
|
|
31524
|
+
"wiki",
|
|
31525
|
+
"docs",
|
|
31526
|
+
"help",
|
|
31527
|
+
"support",
|
|
31528
|
+
"status",
|
|
31529
|
+
"monitor",
|
|
31530
|
+
"grafana",
|
|
31531
|
+
"kibana",
|
|
31532
|
+
"backup",
|
|
31533
|
+
"old",
|
|
31534
|
+
"beta",
|
|
31535
|
+
"demo",
|
|
31536
|
+
"sandbox",
|
|
31537
|
+
"internal",
|
|
31538
|
+
"intranet"
|
|
31539
|
+
];
|
|
31540
|
+
async function dnsBrute(domain) {
|
|
31541
|
+
const found = [];
|
|
31542
|
+
const base = domain.toLowerCase();
|
|
31543
|
+
const seen = new Set([base, "www." + base]);
|
|
31544
|
+
for (const prefix of COMMON_SUBDOMAINS) {
|
|
31545
|
+
const host = `${prefix}.${base}`;
|
|
31546
|
+
if (seen.has(host))
|
|
31327
31547
|
continue;
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31548
|
+
seen.add(host);
|
|
31549
|
+
try {
|
|
31550
|
+
const ips = await resolve4(host);
|
|
31551
|
+
if (ips.length > 0)
|
|
31552
|
+
found.push({ host, ips });
|
|
31553
|
+
} catch {}
|
|
31554
|
+
}
|
|
31555
|
+
return { found, source: "DNS resolution (common subdomain prefixes)" };
|
|
31556
|
+
}
|
|
31557
|
+
var COMMON_PORTS = [
|
|
31558
|
+
[21, "ftp"],
|
|
31559
|
+
[22, "ssh"],
|
|
31560
|
+
[25, "smtp"],
|
|
31561
|
+
[53, "dns"],
|
|
31562
|
+
[80, "http"],
|
|
31563
|
+
[110, "pop3"],
|
|
31564
|
+
[135, "msrpc"],
|
|
31565
|
+
[139, "netbios"],
|
|
31566
|
+
[143, "imap"],
|
|
31567
|
+
[443, "https"],
|
|
31568
|
+
[445, "smb"],
|
|
31569
|
+
[993, "imaps"],
|
|
31570
|
+
[995, "pop3s"],
|
|
31571
|
+
[1433, "mssql"],
|
|
31572
|
+
[1521, "oracle"],
|
|
31573
|
+
[3306, "mysql"],
|
|
31574
|
+
[3389, "rdp"],
|
|
31575
|
+
[5432, "postgres"],
|
|
31576
|
+
[6379, "redis"],
|
|
31577
|
+
[8080, "http-alt"],
|
|
31578
|
+
[8443, "https-alt"],
|
|
31579
|
+
[9000, "app"],
|
|
31580
|
+
[9090, "app"],
|
|
31581
|
+
[9200, "elasticsearch"],
|
|
31582
|
+
[11211, "memcached"],
|
|
31583
|
+
[27017, "mongodb"]
|
|
31584
|
+
];
|
|
31585
|
+
function tcpProbe(host, port, timeout = 2000) {
|
|
31586
|
+
return new Promise((resolve) => {
|
|
31587
|
+
const socket = connect({ host, port, timeout });
|
|
31588
|
+
socket.once("connect", () => {
|
|
31589
|
+
socket.destroy();
|
|
31590
|
+
resolve(true);
|
|
31591
|
+
});
|
|
31592
|
+
socket.once("timeout", () => {
|
|
31593
|
+
socket.destroy();
|
|
31594
|
+
resolve(false);
|
|
31595
|
+
});
|
|
31596
|
+
socket.once("error", () => {
|
|
31597
|
+
socket.destroy();
|
|
31598
|
+
resolve(false);
|
|
31599
|
+
});
|
|
31600
|
+
});
|
|
31601
|
+
}
|
|
31602
|
+
async function scanPorts(host) {
|
|
31603
|
+
const results = [];
|
|
31604
|
+
for (const [port, service] of COMMON_PORTS) {
|
|
31605
|
+
const open = await tcpProbe(host, port);
|
|
31606
|
+
if (open) {
|
|
31607
|
+
const corr = portCorrelation(port);
|
|
31608
|
+
results.push({ port, service, open: true, correlation: corr });
|
|
31609
|
+
}
|
|
31610
|
+
}
|
|
31611
|
+
return results;
|
|
31612
|
+
}
|
|
31613
|
+
var API_PATHS = [
|
|
31614
|
+
"/swagger.json",
|
|
31615
|
+
"/swagger-ui.html",
|
|
31616
|
+
"/openapi.json",
|
|
31617
|
+
"/api-docs",
|
|
31618
|
+
"/v2/api-docs",
|
|
31619
|
+
"/v3/api-docs",
|
|
31620
|
+
"/graphql",
|
|
31621
|
+
"/graphiql",
|
|
31622
|
+
"/actuator",
|
|
31623
|
+
"/actuator/health",
|
|
31624
|
+
"/actuator/env",
|
|
31625
|
+
"/api/swagger.json",
|
|
31626
|
+
"/docs",
|
|
31627
|
+
"/redoc"
|
|
31628
|
+
];
|
|
31629
|
+
async function discoverApi(base) {
|
|
31630
|
+
const out = [];
|
|
31631
|
+
for (const p of API_PATHS) {
|
|
31632
|
+
const r = await httpGet(`${base}${p}`);
|
|
31633
|
+
if (r.status >= 200 && r.status < 400) {
|
|
31634
|
+
out.push({ path: p, status: r.status, body_size: r.body.length });
|
|
31635
|
+
}
|
|
31636
|
+
}
|
|
31637
|
+
return out;
|
|
31638
|
+
}
|
|
31639
|
+
function extractVersion(headers, body) {
|
|
31640
|
+
const out = [];
|
|
31641
|
+
const server = headers["server"] ?? headers["x-powered-by"] ?? "";
|
|
31642
|
+
const serverVer = /^(nginx|apache|microsoft-iis|openresty|liteSpeed|caddy)\/?([0-9][0-9.\-]*)?/i.exec(server);
|
|
31643
|
+
if (serverVer) {
|
|
31644
|
+
out.push({ product: serverVer[1].toLowerCase(), version: serverVer[2] ?? "unknown", evidence: server });
|
|
31645
|
+
}
|
|
31646
|
+
const phpVer = /php\/([0-9][0-9.\-]*)/i.exec(server);
|
|
31647
|
+
if (phpVer)
|
|
31648
|
+
out.push({ product: "php", version: phpVer[1], evidence: server });
|
|
31649
|
+
const wpVer = /content="WordPress\s+([0-9][0-9.]*)/i.exec(body);
|
|
31650
|
+
if (wpVer)
|
|
31651
|
+
out.push({ product: "wordpress", version: wpVer[1], evidence: `meta generator: WordPress ${wpVer[1]}` });
|
|
31652
|
+
const joomlaVer = /Joomla!?\s*([0-9][0-9.]*)/i.exec(body);
|
|
31653
|
+
if (joomlaVer)
|
|
31654
|
+
out.push({ product: "joomla", version: joomlaVer[1], evidence: `body: Joomla ${joomlaVer[1]}` });
|
|
31655
|
+
const drupalVer = /Drupal\s+([0-9][0-9.]*)/i.exec(body);
|
|
31656
|
+
if (drupalVer)
|
|
31657
|
+
out.push({ product: "drupal", version: drupalVer[1], evidence: `body: Drupal ${drupalVer[1]}` });
|
|
31658
|
+
const powered = headers["x-powered-by"] ?? "";
|
|
31659
|
+
if (powered && !out.some((o) => powered.includes(o.product))) {
|
|
31660
|
+
out.push({ product: powered.split("/")[0].toLowerCase(), version: powered.split("/")[1] ?? "unknown", evidence: powered });
|
|
31661
|
+
}
|
|
31662
|
+
return out;
|
|
31663
|
+
}
|
|
31664
|
+
function correlateIntel(techs, versionList, portList) {
|
|
31665
|
+
const tech = techs.map((t) => ({ tech: t, ...techCorrelation(t) }));
|
|
31666
|
+
const versions = versionList.map((v) => ({
|
|
31667
|
+
product: v.product,
|
|
31668
|
+
version: v.version,
|
|
31669
|
+
correlation: techCorrelation(v.product),
|
|
31670
|
+
payload_lookup: payloadLookup(v.product),
|
|
31671
|
+
template_lookup: templateLookup(v.product, 5)
|
|
31672
|
+
}));
|
|
31673
|
+
const ports = portList.map((p) => ({ port: p.port, service: p.service, ...portCorrelation(p.port) }));
|
|
31674
|
+
return { tech, versions, ports };
|
|
31675
|
+
}
|
|
31676
|
+
async function liveRecon(target, includeActive = false) {
|
|
31677
|
+
const base = normalizeTarget(target);
|
|
31678
|
+
const host = (() => {
|
|
31679
|
+
try {
|
|
31680
|
+
return new URL(base).hostname;
|
|
31681
|
+
} catch {
|
|
31682
|
+
return target;
|
|
31683
|
+
}
|
|
31684
|
+
})();
|
|
31685
|
+
const root = await httpGet(base);
|
|
31686
|
+
const interestingHeaders = {};
|
|
31687
|
+
for (const k of ["server", "x-powered-by", "x-aspnet-version", "x-drupal-cache", "x-generator", "via", "x-cache", "cf-ray"]) {
|
|
31688
|
+
if (root.headers[k])
|
|
31689
|
+
interestingHeaders[k] = root.headers[k];
|
|
31690
|
+
}
|
|
31691
|
+
const fingerprint = {
|
|
31692
|
+
status: root.status,
|
|
31693
|
+
title: (root.body.match(/<title[^>]*>([^<]+)<\/title>/i) ?? [])[1]?.trim().slice(0, 200) ?? "",
|
|
31694
|
+
server: root.headers["server"] ?? root.headers["x-powered-by"] ?? "",
|
|
31695
|
+
headers: interestingHeaders,
|
|
31696
|
+
body_size: root.body.length
|
|
31697
|
+
};
|
|
31698
|
+
const waf = detectWaf(root.headers, root.body);
|
|
31699
|
+
const techSig = [
|
|
31700
|
+
[/wp-content|wp-includes|wp-json/i, "wordpress"],
|
|
31701
|
+
[/wp-login\.php|wp-admin/i, "wordpress"],
|
|
31702
|
+
[/powered by joomla|com_content/i, "joomla"],
|
|
31703
|
+
[/laravel|_token|csrf-token/i, "laravel"],
|
|
31704
|
+
[/react|__NEXT_DATA__|next\/static/i, "nextjs"],
|
|
31705
|
+
[/angular|ng-version/i, "angular"],
|
|
31706
|
+
[/vue|__vue__|v-data/i, "vue"],
|
|
31707
|
+
[/django|csrftoken|__debug__/i, "django"],
|
|
31708
|
+
[/ruby on rails|rails/i, "rails"],
|
|
31709
|
+
[/asp\.net|__VIEWSTATE|__EVENTVALIDATION/i, "aspnet"],
|
|
31710
|
+
[/phpBB|phpbb/i, "phpbb"],
|
|
31711
|
+
[/mybb|mybb/i, "mybb"],
|
|
31712
|
+
[/drupal|Drupal\.settings/i, "drupal"],
|
|
31713
|
+
[/magento|Mage\./i, "magento"],
|
|
31714
|
+
[/shopify|cdn\.shopify/i, "shopify"],
|
|
31715
|
+
[/grafana|kibana|elastic/i, "grafana-kibana"],
|
|
31716
|
+
[/node\.js|express/i, "nodejs"],
|
|
31717
|
+
[/flask|werkzeug/i, "flask"],
|
|
31718
|
+
[/spring|actuator/i, "spring"]
|
|
31719
|
+
];
|
|
31720
|
+
const techs = [...new Set(techSig.filter(([re]) => re.test(root.body)).map(([, t]) => t))];
|
|
31721
|
+
const versions = extractVersion(root.headers, root.body);
|
|
31722
|
+
const { links, scripts } = crawlLinks(base, root.body);
|
|
31723
|
+
const { robots, sitemap } = await fetchRobotsAndSitemap(base);
|
|
31724
|
+
const allUrls = [...links, ...scripts, ...robots, ...sitemap];
|
|
31725
|
+
const crawl = { links, scripts, endpoints: [...new Set(allUrls)], robots, sitemap, count: allUrls.length };
|
|
31726
|
+
const params = extractParams(allUrls);
|
|
31727
|
+
const { subdomains } = await subdomainEnum(host);
|
|
31728
|
+
const { found: dnsFindings } = await dnsBrute(host);
|
|
31729
|
+
const { urls: wayback } = await waybackUrls(host);
|
|
31730
|
+
const apiEndpoints = (await discoverApi(base)).map(({ path, status }) => ({ path, status }));
|
|
31731
|
+
let openPorts = [];
|
|
31732
|
+
if (includeActive && root.status > 0) {
|
|
31733
|
+
const scan = await scanPorts(host);
|
|
31734
|
+
openPorts = scan.map(({ port, service }) => ({ port, service }));
|
|
31735
|
+
}
|
|
31736
|
+
const intel = correlateIntel(techs, versions, openPorts);
|
|
31737
|
+
const nextSteps = [];
|
|
31738
|
+
if (waf.detected)
|
|
31739
|
+
nextSteps.push(`WAF detected (${waf.wafs?.map((w) => w.waf).join(", ")}) — consult detect_waf + WAF bypass playbook before exploitation.`);
|
|
31740
|
+
for (const t of techs) {
|
|
31741
|
+
const c = techCorrelation(t);
|
|
31742
|
+
if (c.found)
|
|
31743
|
+
nextSteps.push(`Tech '${t}' maps to known vuln classes — read tech_correlation('${t}') and fetch nuclei templates with template_lookup('${t}').`);
|
|
31744
|
+
}
|
|
31745
|
+
for (const v of versions) {
|
|
31746
|
+
nextSteps.push(`Version ${v.product}/${v.version} detected — check cve_correlation + nvd_lookup for known CVEs affecting ${v.product} ${v.version}.`);
|
|
31747
|
+
}
|
|
31748
|
+
if (crawl.endpoints.length > 0)
|
|
31749
|
+
nextSteps.push(`Found ${crawl.endpoints.length} endpoints — feed them to taint_file/blitz_scan for source analysis, or strike_verify for live validation.`);
|
|
31750
|
+
if (params.length > 0)
|
|
31751
|
+
nextSteps.push(`Discovered input params: ${params.join(", ")} — these are attack-surface entry points; fuzz them with payload_lookup after scope confirms.`);
|
|
31752
|
+
if (subdomains.length > 0)
|
|
31753
|
+
nextSteps.push(`Found ${subdomains.length} subdomains — each is a separate scope surface; enumerate further with fofa_search.`);
|
|
31754
|
+
if (dnsFindings.length > 0)
|
|
31755
|
+
nextSteps.push(`DNS-resolved ${dnsFindings.length} hosts (${dnsFindings.map((d) => d.host).slice(0, 8).join(", ")}${dnsFindings.length > 8 ? "…" : ""}) — each is a live subdomain to recon.`);
|
|
31756
|
+
if (wayback.length > 0)
|
|
31757
|
+
nextSteps.push(`Wayback Machine surfaced ${wayback.length} historical URLs — mine them for hidden endpoints, old params, and forgotten pages; feed to taint_file/blitz_scan.`);
|
|
31758
|
+
if (openPorts.length > 0)
|
|
31759
|
+
nextSteps.push(`Open ports: ${openPorts.map((p) => `${p.port}/${p.service}`).join(", ")} — map services via port_correlation and probe each with the relevant tool manual.`);
|
|
31760
|
+
if (apiEndpoints.length > 0)
|
|
31761
|
+
nextSteps.push(`API endpoints exposed (${apiEndpoints.map((e) => e.path).join(", ")}) — audit them with api-security playbook; check for BOLA/IDOR/auth gaps.`);
|
|
31762
|
+
if (nextSteps.length === 0)
|
|
31763
|
+
nextSteps.push("Nothing exposed on the surface — try subdomain enumeration, port scan (active), or request source access for EAGLE-EYE analysis.");
|
|
31764
|
+
nextSteps.push("Every confirmed finding: record with finding_create, attach confidence_score, redact secrets. A hit is a hypothesis until verified.");
|
|
31765
|
+
return {
|
|
31766
|
+
target: base,
|
|
31767
|
+
host,
|
|
31768
|
+
fingerprint,
|
|
31769
|
+
waf,
|
|
31770
|
+
tech: { detected: techs, server: fingerprint.server },
|
|
31771
|
+
versions,
|
|
31772
|
+
crawl,
|
|
31773
|
+
params,
|
|
31774
|
+
subdomains,
|
|
31775
|
+
dns_findings: dnsFindings,
|
|
31776
|
+
wayback_urls: wayback,
|
|
31777
|
+
api_endpoints: apiEndpoints,
|
|
31778
|
+
open_ports: openPorts,
|
|
31779
|
+
intel,
|
|
31780
|
+
next_steps: nextSteps
|
|
31781
|
+
};
|
|
31782
|
+
}
|
|
31783
|
+
|
|
31784
|
+
// src/orchestrator.ts
|
|
31785
|
+
var ENGAGEMENT_STAGES = ["scope", "recon", "analyze", "verify", "review", "report"];
|
|
31786
|
+
function engagementBanner(target, mode) {
|
|
31787
|
+
return [
|
|
31788
|
+
"⚡ BLITZ engagement — orchestrator in control",
|
|
31789
|
+
` target: ${target}`,
|
|
31790
|
+
` mode: ${mode}`,
|
|
31791
|
+
` stages: ${ENGAGEMENT_STAGES.join(" → ")}`
|
|
31792
|
+
].join(`
|
|
31793
|
+
`);
|
|
31794
|
+
}
|
|
31795
|
+
function formatStatus(opts) {
|
|
31796
|
+
const lines = [];
|
|
31797
|
+
const idx = opts.phase ? ENGAGEMENT_STAGES.indexOf(opts.phase) : -1;
|
|
31798
|
+
lines.push(`BLITZ engagement${idx >= 0 ? ` — ${opts.phase} (${idx + 1}/${ENGAGEMENT_STAGES.length})` : ""}`);
|
|
31799
|
+
if (idx >= 0) {
|
|
31800
|
+
lines.push(` stages: ${ENGAGEMENT_STAGES.map((s, i) => i < idx ? `${s} ✓` : i === idx ? `${s} ▶` : `${s} ☐`).join(" ")}`);
|
|
31801
|
+
}
|
|
31802
|
+
if (opts.findings) {
|
|
31803
|
+
const f = opts.findings;
|
|
31804
|
+
lines.push(` findings: ${[`${f.detected ?? 0} detected`, `${f.hypothesis ?? 0} hypothesis`, `${f.confirmed ?? 0} confirmed`, `${f.false_positive ?? 0} false_positive`].join(" · ")}`);
|
|
31805
|
+
}
|
|
31806
|
+
if (opts.tasks && opts.tasks.length) {
|
|
31807
|
+
lines.push(` tasks: ${opts.tasks.map((t) => `${t.objective} (${t.status})`).join(" · ")}`);
|
|
31808
|
+
}
|
|
31809
|
+
return lines.join(`
|
|
31810
|
+
`);
|
|
31811
|
+
}
|
|
31812
|
+
function formatPlanChecklist(plan) {
|
|
31813
|
+
const mark = (s) => s === "done" ? "✓" : s === "pending" ? "☐" : "▶";
|
|
31814
|
+
const items = plan.map((p) => `[${mark(p.status)}] ${p.state}`);
|
|
31815
|
+
const rows = [];
|
|
31816
|
+
for (let i = 0;i < items.length; i += 3) {
|
|
31817
|
+
rows.push(" " + items.slice(i, i + 3).map((s) => s.padEnd(22)).join(""));
|
|
31818
|
+
}
|
|
31819
|
+
return rows.join(`
|
|
31820
|
+
`);
|
|
31821
|
+
}
|
|
31822
|
+
function classifyTarget(target) {
|
|
31823
|
+
if (/^https?:\/\//i.test(target))
|
|
31824
|
+
return { kind: "live", target };
|
|
31825
|
+
try {
|
|
31826
|
+
if (existsSync5(target) && (statSync2(target).isDirectory() || statSync2(target).isFile())) {
|
|
31827
|
+
return { kind: "source", target };
|
|
31828
|
+
}
|
|
31829
|
+
} catch {}
|
|
31830
|
+
const CODE_EXT = /\.(php|phtml|py|js|ts|jsx|tsx|java|rb|go|c|cpp|cs|swift|kt|rs|pl|sh|sql|html|vue|m|mm)$/i;
|
|
31831
|
+
if (CODE_EXT.test(target) && !target.includes("/")) {
|
|
31832
|
+
return { kind: "source", target };
|
|
31833
|
+
}
|
|
31834
|
+
const host = /^[\w.-]+\.[a-zA-Z]{2,}(:\d+)?([/?#].*)?$/.test(target) || /^\d{1,3}(\.\d{1,3}){3}(:\d+)?$/.test(target);
|
|
31835
|
+
return { kind: "live", target: host ? `https://${target}` : target };
|
|
31836
|
+
}
|
|
31837
|
+
var _chainsCache = null;
|
|
31838
|
+
function loadChains() {
|
|
31839
|
+
if (_chainsCache)
|
|
31840
|
+
return _chainsCache;
|
|
31841
|
+
try {
|
|
31842
|
+
const dir = join7(fileURLToPath4(new URL(".", import.meta.url)), "..");
|
|
31843
|
+
const path = join7(dir, "chains.json");
|
|
31844
|
+
const raw = JSON.parse(readFileSync7(path, "utf8"));
|
|
31845
|
+
_chainsCache = raw.chains ?? [];
|
|
31846
|
+
return _chainsCache;
|
|
31847
|
+
} catch {
|
|
31848
|
+
_chainsCache = [];
|
|
31849
|
+
return [];
|
|
31850
|
+
}
|
|
31851
|
+
}
|
|
31852
|
+
function scopeCheck(target, scope = "", mode = "bug-bounty") {
|
|
31853
|
+
if (target.startsWith("/") || target.startsWith("./") || target.startsWith("../") || target.startsWith("~") || /^[a-zA-Z]:[\\/]/.test(target)) {
|
|
31854
|
+
return { target, allowed: true, enforcement: "off", reason: "static source path — always in-scope" };
|
|
31855
|
+
}
|
|
31856
|
+
let result;
|
|
31857
|
+
if (mode === "ctf" || mode === "reverse-engineering" || mode === "offensive") {
|
|
31858
|
+
result = { target, allowed: true, enforcement: "off", reason: `mode=${mode} disables scope enforcement` };
|
|
31859
|
+
} else if (!scope.trim()) {
|
|
31860
|
+
result = { target, allowed: false, enforcement: "strict", reason: "no scope provided; refusing active testing" };
|
|
31861
|
+
} else {
|
|
31862
|
+
const inScope = [];
|
|
31863
|
+
const outScope = [];
|
|
31864
|
+
for (const raw of scope.split(/[,\n]/)) {
|
|
31865
|
+
const line = raw.trim();
|
|
31866
|
+
if (!line)
|
|
31867
|
+
continue;
|
|
31868
|
+
if (line.startsWith("-"))
|
|
31869
|
+
outScope.push(line.slice(1).trim());
|
|
31870
|
+
else
|
|
31871
|
+
inScope.push(line);
|
|
31872
|
+
}
|
|
31873
|
+
let host = target;
|
|
31874
|
+
host = host.replace(/^https?:\/\//, "");
|
|
31875
|
+
host = host.split("/")[0].split(":")[0];
|
|
31876
|
+
let decided = false;
|
|
31877
|
+
result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' not in scope` };
|
|
31878
|
+
for (const exc of outScope) {
|
|
31879
|
+
if (exc && (host.includes(exc) || host.endsWith("." + exc.replace(/^\./, "")))) {
|
|
31880
|
+
result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' is explicitly out of scope (${exc})` };
|
|
31881
|
+
decided = true;
|
|
31882
|
+
break;
|
|
31332
31883
|
}
|
|
31333
|
-
}
|
|
31334
|
-
|
|
31335
|
-
|
|
31884
|
+
}
|
|
31885
|
+
if (!decided) {
|
|
31886
|
+
for (const allowed of inScope) {
|
|
31887
|
+
const a = allowed.replace(/\*/g, "");
|
|
31888
|
+
if (!a)
|
|
31889
|
+
continue;
|
|
31890
|
+
if (host.includes(a) || host.endsWith("." + a.replace(/^\./, ""))) {
|
|
31891
|
+
result = { target, allowed: true, enforcement: "strict", reason: `matched in-scope pattern '${allowed}'` };
|
|
31892
|
+
decided = true;
|
|
31893
|
+
break;
|
|
31894
|
+
}
|
|
31336
31895
|
}
|
|
31337
31896
|
}
|
|
31338
31897
|
}
|
|
31339
|
-
|
|
31340
|
-
|
|
31341
|
-
|
|
31342
|
-
|
|
31898
|
+
recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
|
|
31899
|
+
return result;
|
|
31900
|
+
}
|
|
31901
|
+
function classifySink(sink) {
|
|
31902
|
+
if (/(move_uploaded_file|file_put_contents|fwrite)/.test(sink))
|
|
31903
|
+
return "upload_weak_validation";
|
|
31904
|
+
if (/unserialize/.test(sink))
|
|
31905
|
+
return "unserialize_user_input";
|
|
31906
|
+
if (/(exec\(|system\(|shell_exec|passthru|popen|proc_open)/.test(sink))
|
|
31907
|
+
return "command_injection";
|
|
31908
|
+
if (/(include|require)/.test(sink))
|
|
31909
|
+
return "lfi_unvalidated_include";
|
|
31910
|
+
if (/extract\(/.test(sink))
|
|
31911
|
+
return "extract_without_exstr_skip";
|
|
31912
|
+
if (/(\$wpdb|->query)/.test(sink))
|
|
31913
|
+
return "sql_injection";
|
|
31914
|
+
if (/(wp_remote_get|wp_remote_post|file_get_contents)/.test(sink))
|
|
31915
|
+
return "ssrf_unvalidated_url";
|
|
31916
|
+
if (/(eval\(|assert\(|create_function)/.test(sink))
|
|
31917
|
+
return "unsafe_template_render";
|
|
31918
|
+
return "";
|
|
31919
|
+
}
|
|
31920
|
+
function enrichScan(path, maxFiles = 2000) {
|
|
31921
|
+
const expanded = path.replace(/^~/, process.env.HOME ?? "~");
|
|
31922
|
+
const files = iterSourceFiles(expanded, maxFiles);
|
|
31923
|
+
const hits = [];
|
|
31924
|
+
const sinkKinds = new Set;
|
|
31925
|
+
for (const p of files) {
|
|
31926
|
+
const r = scanFile(p);
|
|
31927
|
+
for (const e of r.endpoints) {
|
|
31928
|
+
hits.push({ type: "endpoint", file: r.file, line: e.line, detail: e.hook });
|
|
31929
|
+
}
|
|
31930
|
+
for (const s of r.sinks) {
|
|
31931
|
+
hits.push({ type: "sink", file: r.file, line: s.line, detail: `${s.sink} -> ${s.class}` });
|
|
31932
|
+
const kind = classifySink(s.sink);
|
|
31933
|
+
if (kind)
|
|
31934
|
+
sinkKinds.add(kind);
|
|
31935
|
+
}
|
|
31936
|
+
}
|
|
31937
|
+
const chains = loadChains();
|
|
31938
|
+
const matched = [];
|
|
31939
|
+
for (const c of chains) {
|
|
31940
|
+
if ((c.required_findings ?? []).some((rf) => sinkKinds.has(rf))) {
|
|
31941
|
+
matched.push({ chain_id: c.id, name: c.name, severity: c.severity, steps: c.steps.length });
|
|
31343
31942
|
}
|
|
31344
31943
|
}
|
|
31345
|
-
const byWaf = new Map;
|
|
31346
|
-
for (const m of matches) {
|
|
31347
|
-
const key = m.waf;
|
|
31348
|
-
const prev = byWaf.get(key);
|
|
31349
|
-
if (!prev || m.confidence > prev.confidence)
|
|
31350
|
-
byWaf.set(key, m);
|
|
31351
|
-
}
|
|
31352
|
-
const detected = [...byWaf.values()].sort((a, b) => b.confidence - a.confidence);
|
|
31353
|
-
return {
|
|
31354
|
-
detected: detected.length > 0,
|
|
31355
|
-
wafs: detected,
|
|
31356
|
-
block_status_codes: sigs.block_status_codes ?? []
|
|
31357
|
-
};
|
|
31358
|
-
}
|
|
31359
|
-
function techCorrelation(tech) {
|
|
31360
|
-
const data = loadJson("tech_correlations");
|
|
31361
|
-
if (!data)
|
|
31362
|
-
return { found: false, error: "tech_correlations.json not loaded" };
|
|
31363
|
-
const key = tech.toLowerCase();
|
|
31364
|
-
const entry = data[key];
|
|
31365
|
-
if (!entry) {
|
|
31366
|
-
const matches = Object.keys(data).filter((k) => k.includes(key) || key.includes(k)).slice(0, 5);
|
|
31367
|
-
return { found: false, tech, suggestions: matches };
|
|
31368
|
-
}
|
|
31369
|
-
return { found: true, tech: key, vulns: entry.vulns ?? [], paths: entry.paths ?? [], detection: entry.detection ?? { vulns: entry.vulns, paths: entry.paths } };
|
|
31370
|
-
}
|
|
31371
|
-
function cveCorrelation(cveId) {
|
|
31372
|
-
const data = loadJson("cve_correlations");
|
|
31373
|
-
if (!data)
|
|
31374
|
-
return { error: "cve_correlations.json not loaded" };
|
|
31375
|
-
const key = cveId.toUpperCase();
|
|
31376
|
-
const entry = data[key];
|
|
31377
|
-
if (!entry)
|
|
31378
|
-
return { found: false, cve: key, total_known: Object.keys(data).length };
|
|
31379
|
-
return { found: true, cve: key, ...entry };
|
|
31380
|
-
}
|
|
31381
|
-
function portCorrelation(port) {
|
|
31382
|
-
const data = loadJson("port_correlations");
|
|
31383
|
-
if (!data)
|
|
31384
|
-
return { error: "port_correlations.json not loaded" };
|
|
31385
|
-
const key = String(port);
|
|
31386
|
-
const entry = data[key];
|
|
31387
|
-
if (!entry)
|
|
31388
|
-
return { found: false, port: key, total_known: Object.keys(data).length };
|
|
31389
|
-
return { found: true, port: key, ...entry };
|
|
31390
|
-
}
|
|
31391
|
-
function fuzzerPayloads(category) {
|
|
31392
|
-
const data = loadJson("fuzzer_data");
|
|
31393
|
-
if (!data)
|
|
31394
|
-
return { error: "fuzzer_data.json not loaded" };
|
|
31395
|
-
if (category) {
|
|
31396
|
-
const c = data[category] ?? data[category.toUpperCase()];
|
|
31397
|
-
return { category, found: Boolean(c), data: c ?? null };
|
|
31398
|
-
}
|
|
31399
|
-
return { categories: Object.keys(data), fuzz_points: data.FUZZ_POINTS ?? null, payloads: data.FUZZ_PAYLOADS ?? null };
|
|
31400
|
-
}
|
|
31401
|
-
function intelSummary() {
|
|
31402
|
-
const waf = loadJson("waf_signatures");
|
|
31403
|
-
const tech = loadJson("tech_correlations");
|
|
31404
|
-
const cve = loadJson("cve_correlations");
|
|
31405
|
-
const port = loadJson("port_correlations");
|
|
31406
|
-
const fuzz = loadJson("fuzzer_data");
|
|
31407
|
-
const av = loadJson("attack_vectors");
|
|
31408
31944
|
return {
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31413
|
-
|
|
31414
|
-
payload_categories: listPayloadCategories().length,
|
|
31415
|
-
nuclei_templates: countTemplates(),
|
|
31416
|
-
attack_vector_categories: av ? av.categories.length : 0,
|
|
31417
|
-
attack_vectors: av ? av.categories.reduce((n, c) => n + c.vectors.length, 0) : 0
|
|
31945
|
+
root: expanded,
|
|
31946
|
+
files_scanned: files.length,
|
|
31947
|
+
hits: hits.slice(0, 2000),
|
|
31948
|
+
sink_signals: [...sinkKinds].sort(),
|
|
31949
|
+
matched_chains: matched
|
|
31418
31950
|
};
|
|
31419
31951
|
}
|
|
31420
|
-
function
|
|
31421
|
-
const
|
|
31422
|
-
|
|
31423
|
-
|
|
31424
|
-
|
|
31425
|
-
|
|
31426
|
-
|
|
31427
|
-
|
|
31952
|
+
function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000, remember = true) {
|
|
31953
|
+
const report = {
|
|
31954
|
+
banner: engagementBanner(target, mode),
|
|
31955
|
+
target,
|
|
31956
|
+
mode,
|
|
31957
|
+
tiers: {},
|
|
31958
|
+
findings: [],
|
|
31959
|
+
scope: null,
|
|
31960
|
+
memory: { captured: 0 }
|
|
31428
31961
|
};
|
|
31429
|
-
|
|
31430
|
-
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
31434
|
-
|
|
31435
|
-
|
|
31436
|
-
if (
|
|
31437
|
-
|
|
31438
|
-
|
|
31962
|
+
const isPath = (() => {
|
|
31963
|
+
try {
|
|
31964
|
+
return existsSync5(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
|
|
31965
|
+
} catch {
|
|
31966
|
+
return false;
|
|
31967
|
+
}
|
|
31968
|
+
})();
|
|
31969
|
+
if (isPath) {
|
|
31970
|
+
report.scope = { target, allowed: true, enforcement: "source-audit", reason: "local source path — no network scope gate" };
|
|
31971
|
+
} else {
|
|
31972
|
+
const sc = scopeCheck(target, scope, mode);
|
|
31973
|
+
report.scope = sc;
|
|
31974
|
+
if (!sc.allowed) {
|
|
31975
|
+
report.status = "BLOCKED";
|
|
31976
|
+
report.reason = sc.reason;
|
|
31977
|
+
return report;
|
|
31978
|
+
}
|
|
31439
31979
|
}
|
|
31440
|
-
|
|
31441
|
-
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
31445
|
-
|
|
31446
|
-
|
|
31447
|
-
|
|
31448
|
-
|
|
31449
|
-
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
31453
|
-
|
|
31454
|
-
|
|
31455
|
-
|
|
31456
|
-
|
|
31457
|
-
|
|
31458
|
-
|
|
31459
|
-
|
|
31460
|
-
|
|
31461
|
-
|
|
31980
|
+
const blitz = enrichScan(target, maxFiles);
|
|
31981
|
+
report.tiers = {
|
|
31982
|
+
blitz: {
|
|
31983
|
+
files_scanned: blitz.files_scanned,
|
|
31984
|
+
hits: blitz.hits.length,
|
|
31985
|
+
sink_signals: blitz.sink_signals,
|
|
31986
|
+
matched_chains: blitz.matched_chains
|
|
31987
|
+
}
|
|
31988
|
+
};
|
|
31989
|
+
const chains = loadChains();
|
|
31990
|
+
const chainById = new Map(chains.map((c) => [c.id, c]));
|
|
31991
|
+
const eagle = [];
|
|
31992
|
+
for (const mc of blitz.matched_chains.slice(0, 10)) {
|
|
31993
|
+
const chain = chainById.get(mc.chain_id);
|
|
31994
|
+
if (!chain)
|
|
31995
|
+
continue;
|
|
31996
|
+
eagle.push({
|
|
31997
|
+
chain_id: chain.id,
|
|
31998
|
+
name: chain.name,
|
|
31999
|
+
severity: chain.severity,
|
|
32000
|
+
prerequisites: chain.required_findings,
|
|
32001
|
+
recommended_tools: chain.tools ?? [],
|
|
32002
|
+
tool_manuals: (chain.tools ?? []).map((t) => {
|
|
32003
|
+
const m = findManual(t);
|
|
32004
|
+
return m.found ? { tool: t, manual_name: m.name, preview: m.content.slice(0, 600) } : { tool: t, manual_name: null, preview: null };
|
|
32005
|
+
}),
|
|
32006
|
+
steps: chain.steps.map((s) => ({
|
|
32007
|
+
order: s.order,
|
|
32008
|
+
action: s.action,
|
|
32009
|
+
tool_hint: s.tool_hint,
|
|
32010
|
+
success_criteria: s.success_criteria,
|
|
32011
|
+
invariant_check: s.invariant_check,
|
|
32012
|
+
negative_control: s.negative_control
|
|
32013
|
+
}))
|
|
32014
|
+
});
|
|
31462
32015
|
}
|
|
31463
|
-
}
|
|
31464
|
-
|
|
31465
|
-
const
|
|
31466
|
-
const
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
31479
|
-
|
|
31480
|
-
|
|
31481
|
-
|
|
31482
|
-
|
|
31483
|
-
|
|
31484
|
-
|
|
32016
|
+
report.tiers = { ...report.tiers, eagle_eye: { traced_chains: eagle } };
|
|
32017
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
32018
|
+
const matchedSorted = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
|
|
32019
|
+
const findings = matchedSorted.map((f) => {
|
|
32020
|
+
const chain = chainById.get(f.chain_id);
|
|
32021
|
+
const severity = f.severity ?? "medium";
|
|
32022
|
+
const finding = makeFinding({
|
|
32023
|
+
title: f.name,
|
|
32024
|
+
target: { type: "source", path: target },
|
|
32025
|
+
severity,
|
|
32026
|
+
source: { type: "static_sink_signal", name: f.chain_id },
|
|
32027
|
+
sink: { type: f.chain_id, symbol: f.name },
|
|
32028
|
+
chainId: f.chain_id,
|
|
32029
|
+
chainName: f.name,
|
|
32030
|
+
status: "hypothesis"
|
|
32031
|
+
});
|
|
32032
|
+
const ev = makeEvidence({
|
|
32033
|
+
type: "sink_location",
|
|
32034
|
+
description: `Static sink signal matched escalation chain '${f.chain_id}' (${severity}). Hypothesis only — not yet validated.`,
|
|
32035
|
+
sink: { chain_id: f.chain_id, name: f.name, severity },
|
|
32036
|
+
artifacts: [{ name: "chain_match", kind: "json", content: JSON.stringify({ chain_id: f.chain_id, severity, steps: chain?.steps.length ?? 0 }) }]
|
|
32037
|
+
});
|
|
32038
|
+
finding.evidence.push(ev);
|
|
32039
|
+
return finding;
|
|
32040
|
+
});
|
|
32041
|
+
report.findings = findings.map((f) => ({
|
|
32042
|
+
id: f.id,
|
|
32043
|
+
status: f.status,
|
|
32044
|
+
title: f.title,
|
|
32045
|
+
severity: f.classification.severity,
|
|
32046
|
+
confidence: f.confidence,
|
|
32047
|
+
confidence_level: f.confidence_level,
|
|
32048
|
+
chain_id: f.chain.id,
|
|
32049
|
+
evidence_count: f.evidence.length
|
|
32050
|
+
}));
|
|
32051
|
+
const reportTitle = `Blitz Strike — Engagement Report (${target})`;
|
|
32052
|
+
const reportMarkdownText = reportMarkdown(findings, { title: reportTitle, scope: target, version: VERSION });
|
|
32053
|
+
const reportJsonText = reportJson(findings, { title: reportTitle, scope: target, version: VERSION });
|
|
32054
|
+
const sarifDoc = toSarif(findings.map((f) => ({
|
|
32055
|
+
ruleId: f.chain.id || f.classification.severity,
|
|
32056
|
+
level: sarifLevel(f.classification.severity),
|
|
32057
|
+
message: f.title,
|
|
32058
|
+
file: typeof f.target.path === "string" ? f.target.path : target,
|
|
32059
|
+
line: 1
|
|
32060
|
+
})));
|
|
32061
|
+
report.report = {
|
|
32062
|
+
summary: JSON.parse(reportJsonText).summary ?? null,
|
|
32063
|
+
markdown: reportMarkdownText,
|
|
32064
|
+
json: reportJsonText,
|
|
32065
|
+
sarif: sarifDoc
|
|
32066
|
+
};
|
|
32067
|
+
report.status = "COMPLETE";
|
|
32068
|
+
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.";
|
|
32069
|
+
const executedState = "select_chain";
|
|
32070
|
+
const scopeAllowed = report.scope?.allowed ?? false;
|
|
32071
|
+
const plan = buildPlan(scopeAllowed, mode, executedState);
|
|
32072
|
+
report.state = executedState;
|
|
32073
|
+
report.state_description = stateDefinition(executedState).description;
|
|
32074
|
+
report.plan = plan;
|
|
32075
|
+
report.guidance = computeGuidance(executedState, scopeAllowed);
|
|
32076
|
+
report.risk_policy = {
|
|
32077
|
+
note: "High-risk actions (strike_verify, confirm_finding) require scope + are never run server-side automatically.",
|
|
32078
|
+
gated: plan.filter((p) => !p.permitted).map((p) => p.tool)
|
|
31485
32079
|
};
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
if (cl.includes(normalized) || normalized.includes(cl))
|
|
31495
|
-
return true;
|
|
31496
|
-
return keyTokens.some((tok) => new RegExp(`(^|[^a-z0-9])${tok}`).test(cl));
|
|
31497
|
-
});
|
|
31498
|
-
if (matches.length > 0) {
|
|
31499
|
-
return { found: true, category: matches[0], files: listFiles(join8(PAYLOADS, matches[0])), alternatives: matches.slice(0, 10) };
|
|
31500
|
-
}
|
|
31501
|
-
return { found: false, topic, suggestions: cats.slice(0, 20) };
|
|
31502
|
-
}
|
|
31503
|
-
function readPayload(category, file) {
|
|
31504
|
-
const cats = listPayloadCategories();
|
|
31505
|
-
const exact = cats.find((c) => c.toLowerCase() === category.toLowerCase());
|
|
31506
|
-
if (!exact)
|
|
31507
|
-
return { found: false, category, suggestions: cats.slice(0, 20) };
|
|
31508
|
-
const dir = join8(PAYLOADS, exact);
|
|
31509
|
-
const files = listFiles(dir);
|
|
31510
|
-
if (!file) {
|
|
31511
|
-
const readme = files.find((f) => f.toLowerCase() === "readme.md");
|
|
31512
|
-
if (readme) {
|
|
31513
|
-
return { found: true, category: exact, file: readme, content: readFileSync8(join8(dir, readme), "utf8").slice(0, 20000) };
|
|
32080
|
+
if (remember) {
|
|
32081
|
+
let captured = 0;
|
|
32082
|
+
for (const mc of blitz.matched_chains) {
|
|
32083
|
+
const chain = chainById.get(mc.chain_id);
|
|
32084
|
+
const content = `Chain '${mc.chain_id}' (${mc.severity}) matched on target '${target}'. ` + `Detected sink signals: ${blitz.sink_signals.join(", ")}. ` + `Prerequisites: ${(chain?.required_findings ?? []).join(", ")}.`;
|
|
32085
|
+
if (rememberIfAbsent(mc.chain_id, content, "pattern", [mc.severity, "chain"], "engagement", false)) {
|
|
32086
|
+
captured++;
|
|
32087
|
+
}
|
|
31514
32088
|
}
|
|
31515
|
-
|
|
32089
|
+
report.memory = { captured, deduped: blitz.matched_chains.length - captured };
|
|
31516
32090
|
}
|
|
31517
|
-
|
|
31518
|
-
if (!f)
|
|
31519
|
-
return { found: false, category: exact, file, suggestions: files };
|
|
31520
|
-
return { found: true, category: exact, file: f, content: readFileSync8(join8(dir, f), "utf8").slice(0, 20000) };
|
|
32091
|
+
return report;
|
|
31521
32092
|
}
|
|
31522
|
-
function
|
|
31523
|
-
|
|
31524
|
-
|
|
31525
|
-
|
|
31526
|
-
|
|
31527
|
-
|
|
32093
|
+
function listChains() {
|
|
32094
|
+
const chains = loadChains();
|
|
32095
|
+
return {
|
|
32096
|
+
total: chains.length,
|
|
32097
|
+
chains: chains.map((c) => ({
|
|
32098
|
+
id: c.id,
|
|
32099
|
+
name: c.name,
|
|
32100
|
+
severity: c.severity,
|
|
32101
|
+
steps: c.steps.length,
|
|
32102
|
+
triggers: c.triggers
|
|
32103
|
+
}))
|
|
32104
|
+
};
|
|
31528
32105
|
}
|
|
31529
|
-
|
|
32106
|
+
var LIVE_SWEEP_PARAMS = ["q", "search", "query", "s", "id", "url", "redirect", "name", "page"];
|
|
32107
|
+
var SECURITY_HEADERS = [
|
|
32108
|
+
["strict-transport-security", "Strict-Transport-Security (HSTS)"],
|
|
32109
|
+
["content-security-policy", "Content-Security-Policy (CSP)"],
|
|
32110
|
+
["x-frame-options", "X-Frame-Options (clickjacking)"],
|
|
32111
|
+
["x-content-type-options", "X-Content-Type-Options"],
|
|
32112
|
+
["referrer-policy", "Referrer-Policy"]
|
|
32113
|
+
];
|
|
32114
|
+
var OPEN_REDIRECT_PARAMS = ["redirect", "url", "return", "next", "callback", "redirect_uri", "return_url", "goto", "dest", "target", "continue"];
|
|
32115
|
+
var PATH_PARAMS = ["file", "path", "page", "template", "lang", "include", "doc", "download", "load"];
|
|
32116
|
+
var EVIL_HOST = "evil-blitz.example.com";
|
|
32117
|
+
async function httpGet2(u, extraHeaders) {
|
|
31530
32118
|
try {
|
|
31531
|
-
|
|
31532
|
-
const
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
31536
|
-
|
|
31537
|
-
|
|
31538
|
-
}
|
|
31539
|
-
};
|
|
31540
|
-
walk(TEMPLATES);
|
|
31541
|
-
return n;
|
|
32119
|
+
const res = await fetch(u, { redirect: "manual", headers: extraHeaders, signal: AbortSignal.timeout(1e4) });
|
|
32120
|
+
const body = await res.text();
|
|
32121
|
+
const h = {};
|
|
32122
|
+
res.headers.forEach((v, k) => {
|
|
32123
|
+
h[k.toLowerCase()] = v;
|
|
32124
|
+
});
|
|
32125
|
+
return { status: res.status, headers: h, body };
|
|
31542
32126
|
} catch {
|
|
31543
|
-
return
|
|
32127
|
+
return null;
|
|
31544
32128
|
}
|
|
31545
32129
|
}
|
|
31546
|
-
function
|
|
31547
|
-
const
|
|
31548
|
-
const
|
|
31549
|
-
const
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
31553
|
-
|
|
31554
|
-
|
|
31555
|
-
|
|
31556
|
-
|
|
31557
|
-
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31562
|
-
|
|
31563
|
-
|
|
31564
|
-
|
|
31565
|
-
|
|
31566
|
-
try {
|
|
31567
|
-
const content = readFileSync8(full, "utf8");
|
|
31568
|
-
const m = content.match(/^\s*id:\s*(.+)$/m);
|
|
31569
|
-
if (m)
|
|
31570
|
-
id = m[1].trim();
|
|
31571
|
-
} catch {}
|
|
31572
|
-
hits.push({ path: full.replace(ROOT3 + "/", ""), name: e.name, id });
|
|
31573
|
-
}
|
|
31574
|
-
}
|
|
32130
|
+
async function runDeterministicChecks(url) {
|
|
32131
|
+
const findings = [];
|
|
32132
|
+
const base = url.replace(/\/+$/, "");
|
|
32133
|
+
const sep = base.includes("?") ? "&" : "?";
|
|
32134
|
+
const home = await httpGet2(base);
|
|
32135
|
+
if (home) {
|
|
32136
|
+
const missing = SECURITY_HEADERS.filter(([h]) => !home.headers[h]).map(([, n]) => n);
|
|
32137
|
+
if (missing.length) {
|
|
32138
|
+
findings.push(makeFinding({
|
|
32139
|
+
title: `Missing security headers: ${missing.join(", ")}`,
|
|
32140
|
+
target: { type: "web", host: url, endpoint: "/" },
|
|
32141
|
+
severity: "low",
|
|
32142
|
+
cwe: "CWE-693",
|
|
32143
|
+
cweName: "Protection Mechanism Failure",
|
|
32144
|
+
source: { type: "http_response", name: "headers" },
|
|
32145
|
+
sink: { type: "missing_header", symbol: missing[0] },
|
|
32146
|
+
chainId: "missing_security_headers",
|
|
32147
|
+
chainName: "Missing security headers",
|
|
32148
|
+
status: "hypothesis"
|
|
32149
|
+
}));
|
|
31575
32150
|
}
|
|
31576
|
-
};
|
|
31577
|
-
walk(TEMPLATES);
|
|
31578
|
-
return { found: hits.length > 0, topic, count: hits.length, templates: hits.slice(0, limit) };
|
|
31579
|
-
}
|
|
31580
|
-
var BYPASS_ALIASES = {
|
|
31581
|
-
"403": "auth",
|
|
31582
|
-
"401": "auth",
|
|
31583
|
-
forbidden: "auth",
|
|
31584
|
-
"auth bypass": "auth",
|
|
31585
|
-
authorization: "auth",
|
|
31586
|
-
acl: "auth",
|
|
31587
|
-
access: "auth",
|
|
31588
|
-
waf: "waf",
|
|
31589
|
-
firewall: "waf",
|
|
31590
|
-
modsecurity: "waf",
|
|
31591
|
-
cloudflare: "waf",
|
|
31592
|
-
filter: "filter",
|
|
31593
|
-
sanitizer: "filter",
|
|
31594
|
-
blacklist: "filter",
|
|
31595
|
-
whitelist: "filter",
|
|
31596
|
-
validation: "filter",
|
|
31597
|
-
input: "filter",
|
|
31598
|
-
"rate limit": "rate_limit",
|
|
31599
|
-
ratelimit: "rate_limit",
|
|
31600
|
-
captcha: "rate_limit",
|
|
31601
|
-
bot: "rate_limit",
|
|
31602
|
-
"bot protection": "rate_limit"
|
|
31603
|
-
};
|
|
31604
|
-
function bypassLookup(defense) {
|
|
31605
|
-
const data = loadJson("bypass_techniques");
|
|
31606
|
-
if (!data)
|
|
31607
|
-
return { found: false, error: "bypass_techniques.json not found" };
|
|
31608
|
-
const cats = data.categories;
|
|
31609
|
-
if (!defense) {
|
|
31610
|
-
return {
|
|
31611
|
-
found: true,
|
|
31612
|
-
categories: cats.map((c) => ({ id: c.id, defense: c.defense, techniques: c.techniques.length }))
|
|
31613
|
-
};
|
|
31614
|
-
}
|
|
31615
|
-
const key = defense.toLowerCase();
|
|
31616
|
-
const norm = BYPASS_ALIASES[key] ?? key;
|
|
31617
|
-
const match = cats.find((c) => c.id === norm || c.defense.toLowerCase().includes(norm) || norm.includes(c.id));
|
|
31618
|
-
if (!match)
|
|
31619
|
-
return { found: false, defense, suggestions: cats.map((c) => c.id) };
|
|
31620
|
-
return { found: true, defense: match.defense, techniques: match.techniques };
|
|
31621
|
-
}
|
|
31622
|
-
function chainLinks() {
|
|
31623
|
-
const data = loadJson("chain_links");
|
|
31624
|
-
if (!data)
|
|
31625
|
-
return { found: false, error: "chain_links.json not found" };
|
|
31626
|
-
return { found: true, total: data.links.length, links: data.links };
|
|
31627
|
-
}
|
|
31628
|
-
function retryGuidance(signal) {
|
|
31629
|
-
const data = loadJson("retry_guidance");
|
|
31630
|
-
if (!data)
|
|
31631
|
-
return { found: false, error: "retry_guidance.json not found" };
|
|
31632
|
-
if (!signal) {
|
|
31633
|
-
return {
|
|
31634
|
-
found: true,
|
|
31635
|
-
modes: data.modes.map((m) => ({ id: m.id, signal: m.signal, actions: m.actions.length }))
|
|
31636
|
-
};
|
|
31637
|
-
}
|
|
31638
|
-
const ALIASES = {
|
|
31639
|
-
empty: "empty_output",
|
|
31640
|
-
"no output": "empty_output",
|
|
31641
|
-
stale: "empty_output",
|
|
31642
|
-
blocked: "blocked",
|
|
31643
|
-
waf: "blocked",
|
|
31644
|
-
filter: "blocked",
|
|
31645
|
-
firewall: "blocked",
|
|
31646
|
-
sanitized: "sanitized",
|
|
31647
|
-
sanitizer: "sanitized",
|
|
31648
|
-
neutralized: "sanitized",
|
|
31649
|
-
subagent: "subagent_failed",
|
|
31650
|
-
"sub-agent": "subagent_failed",
|
|
31651
|
-
delegate: "subagent_failed",
|
|
31652
|
-
model: "model_failed",
|
|
31653
|
-
"rate limit": "model_failed",
|
|
31654
|
-
ratelimit: "model_failed",
|
|
31655
|
-
quota: "model_failed",
|
|
31656
|
-
context: "model_failed",
|
|
31657
|
-
unconfirmed: "unconfirmed",
|
|
31658
|
-
"not confirmed": "unconfirmed",
|
|
31659
|
-
low: "low_severity",
|
|
31660
|
-
"low severity": "low_severity"
|
|
31661
|
-
};
|
|
31662
|
-
const key = signal.toLowerCase().trim();
|
|
31663
|
-
const norm = ALIASES[key] ?? key.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
31664
|
-
const match = data.modes.find((m) => m.id === norm || norm.includes(m.id) || m.id.includes(norm));
|
|
31665
|
-
if (!match)
|
|
31666
|
-
return { found: false, signal, suggestions: data.modes.map((m) => m.id) };
|
|
31667
|
-
return { found: true, mode: match.id, signal: match.signal, actions: match.actions };
|
|
31668
|
-
}
|
|
31669
|
-
function orchestration(part) {
|
|
31670
|
-
const data = loadJson("orchestration");
|
|
31671
|
-
if (!data)
|
|
31672
|
-
return { found: false, error: "orchestration.json not found" };
|
|
31673
|
-
if (!part) {
|
|
31674
|
-
return {
|
|
31675
|
-
found: true,
|
|
31676
|
-
phases: data.lifecycle.map((p) => ({ phase: p.phase, name: p.name })),
|
|
31677
|
-
team: data.team.map((r) => ({ id: r.id, name: r.name }))
|
|
31678
|
-
};
|
|
31679
|
-
}
|
|
31680
|
-
const q = part.toLowerCase();
|
|
31681
|
-
if (q === "lifecycle" || q === "phases")
|
|
31682
|
-
return { found: true, lifecycle: data.lifecycle };
|
|
31683
|
-
if (q === "team" || q === "roles")
|
|
31684
|
-
return { found: true, team: data.team };
|
|
31685
|
-
if (q === "handoff")
|
|
31686
|
-
return { found: true, handoff: data.handoff };
|
|
31687
|
-
if (q === "termination" || q === "done")
|
|
31688
|
-
return { found: true, termination: data.termination };
|
|
31689
|
-
const phase = data.lifecycle.find((p) => String(p.phase) === q);
|
|
31690
|
-
if (phase)
|
|
31691
|
-
return { found: true, ...phase };
|
|
31692
|
-
const role = data.team.find((r) => String(r.id) === q);
|
|
31693
|
-
if (role)
|
|
31694
|
-
return { found: true, ...role };
|
|
31695
|
-
return {
|
|
31696
|
-
found: false,
|
|
31697
|
-
part,
|
|
31698
|
-
suggestions: ["lifecycle", "team", "handoff", "termination", ...data.lifecycle.map((p) => String(p.phase)), ...data.team.map((r) => String(r.id))]
|
|
31699
|
-
};
|
|
31700
|
-
}
|
|
31701
|
-
function modelFallback(signal) {
|
|
31702
|
-
const data = loadJson("model_fallback");
|
|
31703
|
-
if (!data)
|
|
31704
|
-
return { found: false, error: "model_fallback.json not found" };
|
|
31705
|
-
if (!signal) {
|
|
31706
|
-
return {
|
|
31707
|
-
found: true,
|
|
31708
|
-
error_classes: data.error_classes.map((c) => ({ id: c.id, retryable: c.retryable, backoff: c.backoff })),
|
|
31709
|
-
fallback_chain: data.fallback_chain,
|
|
31710
|
-
backoff: data.backoff
|
|
31711
|
-
};
|
|
31712
32151
|
}
|
|
31713
|
-
const
|
|
31714
|
-
|
|
31715
|
-
if (
|
|
31716
|
-
|
|
31717
|
-
|
|
31718
|
-
|
|
31719
|
-
|
|
31720
|
-
|
|
31721
|
-
|
|
31722
|
-
|
|
31723
|
-
|
|
31724
|
-
|
|
32152
|
+
for (const p of OPEN_REDIRECT_PARAMS) {
|
|
32153
|
+
const r = await httpGet2(`${base}${sep}${p}=${encodeURIComponent(`https://${EVIL_HOST}`)}`);
|
|
32154
|
+
if (!r)
|
|
32155
|
+
continue;
|
|
32156
|
+
const loc = r.headers["location"] ?? "";
|
|
32157
|
+
if (r.status >= 300 && r.status < 400 && loc.includes(EVIL_HOST)) {
|
|
32158
|
+
findings.push(makeFinding({
|
|
32159
|
+
title: `Open redirect via parameter '${p}'`,
|
|
32160
|
+
target: { type: "web", host: url, endpoint: `?${p}=` },
|
|
32161
|
+
severity: "medium",
|
|
32162
|
+
cwe: "CWE-601",
|
|
32163
|
+
cweName: "URL Redirection to Untrusted Site",
|
|
32164
|
+
source: { type: "request_parameter", name: p },
|
|
32165
|
+
sink: { type: "redirect", symbol: p },
|
|
32166
|
+
chainId: "open_redirect",
|
|
32167
|
+
chainName: "Open redirect",
|
|
32168
|
+
status: "hypothesis"
|
|
32169
|
+
}));
|
|
31725
32170
|
}
|
|
31726
32171
|
}
|
|
31727
|
-
|
|
31728
|
-
}
|
|
31729
|
-
|
|
31730
|
-
|
|
31731
|
-
|
|
31732
|
-
|
|
31733
|
-
|
|
31734
|
-
|
|
31735
|
-
|
|
31736
|
-
|
|
31737
|
-
|
|
31738
|
-
|
|
32172
|
+
for (const p of PATH_PARAMS) {
|
|
32173
|
+
const r = await httpGet2(`${base}${sep}${p}=${encodeURIComponent("../../../../../../etc/passwd")}`);
|
|
32174
|
+
if (r && r.body.includes("root:")) {
|
|
32175
|
+
findings.push(makeFinding({
|
|
32176
|
+
title: `Path traversal via parameter '${p}'`,
|
|
32177
|
+
target: { type: "web", host: url, endpoint: `?${p}=` },
|
|
32178
|
+
severity: "high",
|
|
32179
|
+
cwe: "CWE-22",
|
|
32180
|
+
cweName: "Path Traversal",
|
|
32181
|
+
source: { type: "request_parameter", name: p },
|
|
32182
|
+
sink: { type: "file_read", symbol: p },
|
|
32183
|
+
chainId: "path_traversal",
|
|
32184
|
+
chainName: "Path traversal / LFI",
|
|
32185
|
+
status: "hypothesis"
|
|
32186
|
+
}));
|
|
32187
|
+
}
|
|
31739
32188
|
}
|
|
31740
|
-
const
|
|
31741
|
-
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
|
|
32189
|
+
const cors = await httpGet2(base, { Origin: `https://${EVIL_HOST}` });
|
|
32190
|
+
if (cors) {
|
|
32191
|
+
const acao = cors.headers["access-control-allow-origin"] ?? "";
|
|
32192
|
+
const acac = cors.headers["access-control-allow-credentials"] ?? "";
|
|
32193
|
+
if ((acao.includes(EVIL_HOST) || acao === "*") && acac === "true") {
|
|
32194
|
+
findings.push(makeFinding({
|
|
32195
|
+
title: "CORS misconfiguration: reflected/wildcard origin with credentials",
|
|
32196
|
+
target: { type: "web", host: url, endpoint: "/" },
|
|
32197
|
+
severity: "medium",
|
|
32198
|
+
cwe: "CWE-942",
|
|
32199
|
+
cweName: "Permissive Cross-domain Policy",
|
|
32200
|
+
source: { type: "http_response", name: "access-control-allow-origin" },
|
|
32201
|
+
sink: { type: "cors", symbol: acao },
|
|
32202
|
+
chainId: "cors_misconfiguration",
|
|
32203
|
+
chainName: "CORS misconfiguration",
|
|
32204
|
+
status: "hypothesis"
|
|
32205
|
+
}));
|
|
32206
|
+
}
|
|
31746
32207
|
}
|
|
31747
|
-
return
|
|
31748
|
-
found: false,
|
|
31749
|
-
part,
|
|
31750
|
-
suggestions: ["cross_cutting", ...data.phases.map((p) => String(p.phase))]
|
|
31751
|
-
};
|
|
32208
|
+
return findings;
|
|
31752
32209
|
}
|
|
31753
|
-
function
|
|
31754
|
-
const
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
32210
|
+
async function runLiveEngagement(url, scope = "", mode = "bug-bounty") {
|
|
32211
|
+
const report = { banner: engagementBanner(url, mode), target: url, mode, findings: [], scope: null, status: "" };
|
|
32212
|
+
const sc = scopeCheck(url, scope, mode);
|
|
32213
|
+
report.scope = sc;
|
|
32214
|
+
const recon = await liveRecon(url);
|
|
32215
|
+
report.recon = {
|
|
32216
|
+
fingerprint: recon.fingerprint,
|
|
32217
|
+
waf: recon.waf,
|
|
32218
|
+
tech: recon.tech,
|
|
32219
|
+
versions: recon.versions,
|
|
32220
|
+
crawl: recon.crawl,
|
|
32221
|
+
params: recon.params,
|
|
32222
|
+
subdomains: recon.subdomains,
|
|
32223
|
+
dns_findings: recon.dns_findings,
|
|
32224
|
+
wayback_urls: recon.wayback_urls,
|
|
32225
|
+
api_endpoints: recon.api_endpoints,
|
|
32226
|
+
open_ports: recon.open_ports,
|
|
32227
|
+
intel: recon.intel,
|
|
32228
|
+
next_steps: recon.next_steps
|
|
32229
|
+
};
|
|
32230
|
+
const findings = [];
|
|
32231
|
+
for (const param of LIVE_SWEEP_PARAMS) {
|
|
32232
|
+
const verdict = await strikeVerify({ url, param });
|
|
32233
|
+
const finding = makeFinding({
|
|
32234
|
+
title: `Reflected input in parameter '${param}'`,
|
|
32235
|
+
target: { type: "web", host: url, endpoint: `?${param}=` },
|
|
32236
|
+
severity: "medium",
|
|
32237
|
+
cwe: "CWE-79",
|
|
32238
|
+
cweName: "Cross-site Scripting",
|
|
32239
|
+
source: { type: "request_parameter", name: param },
|
|
32240
|
+
sink: { type: "reflected_input", symbol: param },
|
|
32241
|
+
chainId: "reflected_input",
|
|
32242
|
+
chainName: "Reflected input",
|
|
32243
|
+
status: "hypothesis"
|
|
32244
|
+
});
|
|
32245
|
+
findings.push(resolveFinding(finding, verdict));
|
|
31762
32246
|
}
|
|
31763
|
-
|
|
31764
|
-
|
|
31765
|
-
|
|
31766
|
-
|
|
31767
|
-
|
|
32247
|
+
findings.push(...await runDeterministicChecks(url));
|
|
32248
|
+
report.findings = findings.map((f) => ({
|
|
32249
|
+
id: f.id,
|
|
32250
|
+
status: f.status,
|
|
32251
|
+
title: f.title,
|
|
32252
|
+
severity: f.classification.severity,
|
|
32253
|
+
confidence: f.confidence,
|
|
32254
|
+
confidence_level: f.confidence_level,
|
|
32255
|
+
chain_id: f.chain.id,
|
|
32256
|
+
validation: f.validation
|
|
32257
|
+
}));
|
|
32258
|
+
const title = `Blitz Strike — Live Engagement (${url})`;
|
|
32259
|
+
const markdown = reportMarkdown(findings, { title, scope: url, version: VERSION });
|
|
32260
|
+
const json = reportJson(findings, { title, scope: url, version: VERSION });
|
|
32261
|
+
report.report = {
|
|
32262
|
+
summary: JSON.parse(json).summary ?? null,
|
|
32263
|
+
markdown,
|
|
32264
|
+
json,
|
|
32265
|
+
sarif: toSarif(findings.map((f) => ({
|
|
32266
|
+
ruleId: f.chain.id || f.classification.cwe || f.classification.severity,
|
|
32267
|
+
level: sarifLevel(f.classification.severity),
|
|
32268
|
+
message: f.title,
|
|
32269
|
+
file: url,
|
|
32270
|
+
line: 1
|
|
32271
|
+
})))
|
|
32272
|
+
};
|
|
32273
|
+
report.status = "COMPLETE";
|
|
32274
|
+
report.note = "Autonomous live engagement — full recon + deterministic security checks (reflected " + "input, security headers, open redirect, path traversal, CORS) + inline report, all in " + "one call (no LLM babysitting). Reflected input was validated with a marker + negative " + "control; header/redirect/traversal/CORS checks are deterministic HTTP observations. " + "A hit is a hypothesis until strike_verify confirms it.";
|
|
32275
|
+
return report;
|
|
31768
32276
|
}
|
|
31769
|
-
|
|
31770
|
-
|
|
31771
|
-
|
|
31772
|
-
|
|
31773
|
-
|
|
31774
|
-
|
|
31775
|
-
|
|
31776
|
-
|
|
31777
|
-
|
|
32277
|
+
|
|
32278
|
+
// src/catalog.ts
|
|
32279
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync3 } from "node:fs";
|
|
32280
|
+
import { join as join8 } from "node:path";
|
|
32281
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
32282
|
+
import { spawnSync } from "node:child_process";
|
|
32283
|
+
var ROOT3 = join8(fileURLToPath5(new URL(".", import.meta.url)), "..");
|
|
32284
|
+
var _toolsCache = null;
|
|
32285
|
+
var _skillsCache = null;
|
|
32286
|
+
function loadTools() {
|
|
32287
|
+
if (_toolsCache)
|
|
32288
|
+
return _toolsCache;
|
|
32289
|
+
try {
|
|
32290
|
+
const raw = JSON.parse(readFileSync8(join8(ROOT3, "tools-catalog.json"), "utf8"));
|
|
32291
|
+
_toolsCache = raw.tools ?? [];
|
|
32292
|
+
} catch {
|
|
32293
|
+
_toolsCache = [];
|
|
31778
32294
|
}
|
|
31779
|
-
|
|
31780
|
-
const c = data.classes.find((x) => String(x.id).toLowerCase() === q || String(x.id).toLowerCase().includes(q) || String(x.name).toLowerCase().includes(q));
|
|
31781
|
-
if (c)
|
|
31782
|
-
return { found: true, ...c };
|
|
31783
|
-
return { found: false, cls, suggestions: data.classes.map((x) => String(x.id)) };
|
|
32295
|
+
return _toolsCache;
|
|
31784
32296
|
}
|
|
31785
|
-
function
|
|
31786
|
-
|
|
31787
|
-
|
|
31788
|
-
|
|
31789
|
-
|
|
31790
|
-
|
|
31791
|
-
|
|
31792
|
-
|
|
31793
|
-
|
|
31794
|
-
|
|
31795
|
-
|
|
31796
|
-
|
|
32297
|
+
function loadSkills() {
|
|
32298
|
+
if (_skillsCache)
|
|
32299
|
+
return _skillsCache;
|
|
32300
|
+
const dir = join8(ROOT3, "skills");
|
|
32301
|
+
const out = [];
|
|
32302
|
+
try {
|
|
32303
|
+
for (const f of readdirSync3(dir)) {
|
|
32304
|
+
if (!f.endsWith(".md"))
|
|
32305
|
+
continue;
|
|
32306
|
+
const p = join8(dir, f);
|
|
32307
|
+
const text = readFileSync8(p, "utf8");
|
|
32308
|
+
const descMatch = text.match(/^description:\s*(.+)$/m);
|
|
32309
|
+
out.push({
|
|
32310
|
+
name: f.replace(/\.md$/, ""),
|
|
32311
|
+
file: f,
|
|
32312
|
+
description: (descMatch?.[1] ?? "").trim().slice(0, 200),
|
|
32313
|
+
size: text.length
|
|
32314
|
+
});
|
|
32315
|
+
}
|
|
32316
|
+
} catch {}
|
|
32317
|
+
_skillsCache = out;
|
|
32318
|
+
return out;
|
|
31797
32319
|
}
|
|
31798
|
-
function
|
|
31799
|
-
const
|
|
31800
|
-
|
|
31801
|
-
|
|
31802
|
-
if (!category) {
|
|
32320
|
+
function toolLookup(name) {
|
|
32321
|
+
const tools = loadTools();
|
|
32322
|
+
const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
32323
|
+
if (!t) {
|
|
31803
32324
|
return {
|
|
31804
|
-
found:
|
|
31805
|
-
|
|
32325
|
+
found: false,
|
|
32326
|
+
query: name,
|
|
32327
|
+
suggestions: tools.filter((x) => x.name.includes(name.toLowerCase()) || name.toLowerCase().includes(x.name)).slice(0, 5).map((x) => x.name)
|
|
31806
32328
|
};
|
|
31807
32329
|
}
|
|
31808
|
-
const
|
|
31809
|
-
const
|
|
31810
|
-
if (
|
|
31811
|
-
|
|
31812
|
-
|
|
32330
|
+
const manual = manualForTool(t.name);
|
|
32331
|
+
const result = { found: true, tool: t };
|
|
32332
|
+
if (manual && manual.content) {
|
|
32333
|
+
result.manual = {
|
|
32334
|
+
name: manual.name,
|
|
32335
|
+
category: manual.category,
|
|
32336
|
+
preview: manual.content.slice(0, 1500),
|
|
32337
|
+
full_length: manual.content.length
|
|
32338
|
+
};
|
|
32339
|
+
} else {
|
|
32340
|
+
result.manual = null;
|
|
32341
|
+
}
|
|
32342
|
+
return result;
|
|
31813
32343
|
}
|
|
31814
|
-
function
|
|
31815
|
-
|
|
31816
|
-
|
|
31817
|
-
|
|
31818
|
-
|
|
31819
|
-
if (!query)
|
|
31820
|
-
return { found: true, risks: data.risks };
|
|
31821
|
-
const q = query.toLowerCase();
|
|
31822
|
-
const r = data.risks.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31823
|
-
if (r)
|
|
31824
|
-
return { found: true, ...r };
|
|
31825
|
-
return { found: false, query, suggestions: data.risks.map((x) => String(x.id)) };
|
|
32344
|
+
function listTools() {
|
|
32345
|
+
const tools = loadTools();
|
|
32346
|
+
const byCat = {};
|
|
32347
|
+
for (const t of tools) {
|
|
32348
|
+
(byCat[t.category] ??= []).push(t.name);
|
|
31826
32349
|
}
|
|
31827
|
-
|
|
31828
|
-
|
|
31829
|
-
|
|
31830
|
-
|
|
31831
|
-
|
|
31832
|
-
|
|
31833
|
-
const
|
|
31834
|
-
const
|
|
31835
|
-
|
|
31836
|
-
|
|
31837
|
-
return {
|
|
32350
|
+
return { total: tools.length, categories: byCat, tools: tools.map((t) => ({ name: t.name, category: t.category, description: t.description })) };
|
|
32351
|
+
}
|
|
32352
|
+
function skillLookup(topic) {
|
|
32353
|
+
const skills = loadSkills();
|
|
32354
|
+
const q = topic.toLowerCase();
|
|
32355
|
+
const scored = skills.map((s) => {
|
|
32356
|
+
const hay = `${s.name} ${s.description}`.toLowerCase();
|
|
32357
|
+
const nameHit = s.name.includes(q);
|
|
32358
|
+
const descHit = s.description.includes(q);
|
|
32359
|
+
const wordHit = q.split(/\s+/).filter((w) => w && hay.includes(w)).length;
|
|
32360
|
+
return { s, score: (nameHit ? 10 : 0) + (descHit ? 5 : 0) + wordHit * 2 };
|
|
32361
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
|
|
32362
|
+
if (scored.length === 0) {
|
|
32363
|
+
return { found: false, query: topic, suggestions: skills.map((s) => s.name).slice(0, 10) };
|
|
31838
32364
|
}
|
|
31839
|
-
|
|
31840
|
-
|
|
31841
|
-
|
|
31842
|
-
|
|
31843
|
-
|
|
31844
|
-
|
|
31845
|
-
|
|
31846
|
-
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
32365
|
+
return {
|
|
32366
|
+
found: true,
|
|
32367
|
+
matches: scored.slice(0, 5).map((x) => ({ name: x.s.name, description: x.s.description, file: x.s.file }))
|
|
32368
|
+
};
|
|
32369
|
+
}
|
|
32370
|
+
function listSkills() {
|
|
32371
|
+
const skills = loadSkills();
|
|
32372
|
+
return { total: skills.length, skills: skills.map((s) => ({ name: s.name, description: s.description })) };
|
|
32373
|
+
}
|
|
32374
|
+
function readSkill(name) {
|
|
32375
|
+
const skills = loadSkills();
|
|
32376
|
+
const s = skills.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
32377
|
+
if (!s)
|
|
32378
|
+
return { found: false, query: name };
|
|
32379
|
+
try {
|
|
32380
|
+
const text = readFileSync8(join8(ROOT3, "skills", s.file), "utf8");
|
|
32381
|
+
return { found: true, name: s.name, content: text.slice(0, 12000) };
|
|
32382
|
+
} catch {
|
|
32383
|
+
return { found: false, query: name, error: "unreadable" };
|
|
31850
32384
|
}
|
|
31851
|
-
|
|
31852
|
-
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
31856
|
-
|
|
31857
|
-
|
|
31858
|
-
|
|
31859
|
-
|
|
31860
|
-
|
|
31861
|
-
|
|
32385
|
+
}
|
|
32386
|
+
function ensureTool(name) {
|
|
32387
|
+
const tools = loadTools();
|
|
32388
|
+
const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
32389
|
+
if (!t) {
|
|
32390
|
+
return { found: false, query: name, suggestions: tools.slice(0, 5).map((x) => x.name) };
|
|
32391
|
+
}
|
|
32392
|
+
const check = spawnSync(t.check_installed.command, { shell: true, timeout: 15000 });
|
|
32393
|
+
if (check.status !== 127 && check.error === undefined) {
|
|
32394
|
+
return { name: t.name, installed: true, action: "none", note: "already installed" };
|
|
32395
|
+
}
|
|
32396
|
+
const platform = process.platform;
|
|
32397
|
+
const cmd = t.install[platform] ?? t.install.linux;
|
|
32398
|
+
if (!cmd) {
|
|
32399
|
+
return { name: t.name, installed: false, action: "none", note: `no install command for ${platform}` };
|
|
31862
32400
|
}
|
|
32401
|
+
const r = spawnSync(cmd, { shell: true, timeout: 120000 });
|
|
32402
|
+
const ok = r.status === 0;
|
|
31863
32403
|
return {
|
|
31864
|
-
|
|
31865
|
-
|
|
31866
|
-
|
|
32404
|
+
name: t.name,
|
|
32405
|
+
installed: ok,
|
|
32406
|
+
action: "install",
|
|
32407
|
+
command: cmd,
|
|
32408
|
+
output: (r.stdout?.toString() ?? "").slice(-400) || (r.stderr?.toString() ?? "").slice(-400)
|
|
31867
32409
|
};
|
|
31868
32410
|
}
|
|
31869
32411
|
|
|
@@ -31886,7 +32428,7 @@ async function httpFetch(url, init) {
|
|
|
31886
32428
|
clearTimeout(t);
|
|
31887
32429
|
}
|
|
31888
32430
|
}
|
|
31889
|
-
function
|
|
32431
|
+
function normalizeTarget2(target) {
|
|
31890
32432
|
let t = target.trim();
|
|
31891
32433
|
if (!/^https?:\/\//i.test(t))
|
|
31892
32434
|
t = `https://${t}`;
|
|
@@ -31954,7 +32496,7 @@ async function probeCommonPaths(base) {
|
|
|
31954
32496
|
return out;
|
|
31955
32497
|
}
|
|
31956
32498
|
async function activeScan(target, scope = "", mode = "bug-bounty", allowed = undefined) {
|
|
31957
|
-
const base =
|
|
32499
|
+
const base = normalizeTarget2(target);
|
|
31958
32500
|
const host = (() => {
|
|
31959
32501
|
try {
|
|
31960
32502
|
return new URL(base).hostname;
|
|
@@ -52706,767 +53248,345 @@ function detectDynamicDispatch(code, file, out) {
|
|
|
52706
53248
|
});
|
|
52707
53249
|
}
|
|
52708
53250
|
}
|
|
52709
|
-
function detectDynamicMethod(code, file, out) {
|
|
52710
|
-
const ls = lines(code);
|
|
52711
|
-
DYNAMIC_METHOD_RE.lastIndex = 0;
|
|
52712
|
-
let m;
|
|
52713
|
-
while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
|
|
52714
|
-
const ln = lineNo(code, m.index);
|
|
52715
|
-
const l = ls[ln - 1] ?? "";
|
|
52716
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52717
|
-
`);
|
|
52718
|
-
if (!userControlled(l) && !userControlled(scope))
|
|
52719
|
-
continue;
|
|
52720
|
-
out.push({
|
|
52721
|
-
file,
|
|
52722
|
-
line: ln,
|
|
52723
|
-
type: "dynamic_method_call",
|
|
52724
|
-
category: "Route confusion (dynamic method dispatch)",
|
|
52725
|
-
severity: "high",
|
|
52726
|
-
evidence: l.trim(),
|
|
52727
|
-
detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
|
|
52728
|
-
});
|
|
52729
|
-
}
|
|
52730
|
-
}
|
|
52731
|
-
function detectDynamicInclude(code, file, out) {
|
|
52732
|
-
const ls = lines(code);
|
|
52733
|
-
const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
|
|
52734
|
-
let m;
|
|
52735
|
-
while ((m = re.exec(code)) !== null) {
|
|
52736
|
-
const ln = lineNo(code, m.index);
|
|
52737
|
-
const l = ls[ln - 1] ?? "";
|
|
52738
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52739
|
-
`);
|
|
52740
|
-
if (!userControlled(l) && !userControlled(scope))
|
|
52741
|
-
continue;
|
|
52742
|
-
out.push({
|
|
52743
|
-
file,
|
|
52744
|
-
line: ln,
|
|
52745
|
-
type: "dynamic_include",
|
|
52746
|
-
category: "Route confusion (dynamic file include)",
|
|
52747
|
-
severity: "high",
|
|
52748
|
-
evidence: l.trim(),
|
|
52749
|
-
detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
|
|
52750
|
-
});
|
|
52751
|
-
}
|
|
52752
|
-
}
|
|
52753
|
-
function detectBatchForwarding(code, file, out) {
|
|
52754
|
-
BATCH_LOOP_RE.lastIndex = 0;
|
|
52755
|
-
let m;
|
|
52756
|
-
while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
|
|
52757
|
-
const ln = lineNo(code, m.index);
|
|
52758
|
-
const body = lines(code).slice(ln, ln + 8).join(`
|
|
52759
|
-
`);
|
|
52760
|
-
const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
|
|
52761
|
-
if (!forwards)
|
|
52762
|
-
continue;
|
|
52763
|
-
const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
|
|
52764
|
-
out.push({
|
|
52765
|
-
file,
|
|
52766
|
-
line: ln,
|
|
52767
|
-
type: "batch_forwarding",
|
|
52768
|
-
category: "Route confusion (batch/proxy forwarding)",
|
|
52769
|
-
severity: rechecks ? "medium" : "high",
|
|
52770
|
-
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
52771
|
-
detail: rechecks ? "Batch/proxy loop forwards to inner handlers — auth is re-checked in scope, but verify it applies per-route." : "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
|
|
52772
|
-
});
|
|
52773
|
-
}
|
|
52774
|
-
}
|
|
52775
|
-
function detectRouteNormalization(code, file, out) {
|
|
52776
|
-
const ls = lines(code);
|
|
52777
|
-
for (let i = 0;i < ls.length; i++) {
|
|
52778
|
-
const l = ls[i];
|
|
52779
|
-
if (!ROUTE_MATCH_RE.test(l))
|
|
52780
|
-
continue;
|
|
52781
|
-
const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
|
|
52782
|
-
`);
|
|
52783
|
-
if (!userControlled(scope))
|
|
52784
|
-
continue;
|
|
52785
|
-
if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
|
|
52786
|
-
continue;
|
|
52787
|
-
out.push({
|
|
52788
|
-
file,
|
|
52789
|
-
line: i + 1,
|
|
52790
|
-
type: "route_normalization_gap",
|
|
52791
|
-
category: "Route confusion (missing path normalization)",
|
|
52792
|
-
severity: "medium",
|
|
52793
|
-
evidence: l.trim(),
|
|
52794
|
-
detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
|
|
52795
|
-
});
|
|
52796
|
-
}
|
|
52797
|
-
}
|
|
52798
|
-
function detectRouteConfusion(code, file) {
|
|
52799
|
-
const out = [];
|
|
52800
|
-
detectDynamicDispatch(code, file, out);
|
|
52801
|
-
detectDynamicMethod(code, file, out);
|
|
52802
|
-
detectDynamicInclude(code, file, out);
|
|
52803
|
-
detectBatchForwarding(code, file, out);
|
|
52804
|
-
detectRouteNormalization(code, file, out);
|
|
52805
|
-
return out;
|
|
52806
|
-
}
|
|
52807
|
-
|
|
52808
|
-
// src/complex-bugs.ts
|
|
52809
|
-
var SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data|body|headers|cookies|params)|getParameter\(|req\.(query|body|params|headers|cookies)/;
|
|
52810
|
-
function lines2(code) {
|
|
52811
|
-
return code.split(`
|
|
52812
|
-
`);
|
|
52813
|
-
}
|
|
52814
|
-
function lineNo2(code, pos) {
|
|
52815
|
-
let n = 1;
|
|
52816
|
-
for (let i = 0;i < pos; i++)
|
|
52817
|
-
if (code.charCodeAt(i) === 10)
|
|
52818
|
-
n++;
|
|
52819
|
-
return n;
|
|
52820
|
-
}
|
|
52821
|
-
var MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
|
|
52822
|
-
function detectDeserialization(code, file, out) {
|
|
52823
|
-
if (!MAGIC_METHODS.test(code))
|
|
52824
|
-
return;
|
|
52825
|
-
const ls = lines2(code);
|
|
52826
|
-
const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
|
|
52827
|
-
let m;
|
|
52828
|
-
while ((m = re.exec(code)) !== null) {
|
|
52829
|
-
const ln = lineNo2(code, m.index);
|
|
52830
|
-
const l = ls[ln - 1] ?? "";
|
|
52831
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52832
|
-
`);
|
|
52833
|
-
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
52834
|
-
continue;
|
|
52835
|
-
out.push({
|
|
52836
|
-
file,
|
|
52837
|
-
line: ln,
|
|
52838
|
-
type: "deserialization",
|
|
52839
|
-
category: "Deserialization → POP gadget chain",
|
|
52840
|
-
severity: "high",
|
|
52841
|
-
evidence: l.trim(),
|
|
52842
|
-
detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
|
|
52843
|
-
});
|
|
52844
|
-
}
|
|
52845
|
-
}
|
|
52846
|
-
function detectTypeJuggling(code, file, out) {
|
|
52847
|
-
const ls = lines2(code);
|
|
52848
|
-
const re = /(?<![=!<>])(==|!=)(?!=)/g;
|
|
52849
|
-
let m;
|
|
52850
|
-
while ((m = re.exec(code)) !== null) {
|
|
52851
|
-
const ln = lineNo2(code, m.index);
|
|
52852
|
-
const l = ls[ln - 1] ?? "";
|
|
52853
|
-
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52854
|
-
`);
|
|
52855
|
-
const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
|
|
52856
|
-
const hasHash = /\b(md5|sha1|sha256|hash|crc32|crypt|password_verify|strcmp|hash_equals|secret|token|nonce)\s*\(?/i.test(l) || /\b(secret|token|nonce|hash)\b/i.test(l);
|
|
52857
|
-
if (!hasSource || !hasHash)
|
|
52858
|
-
continue;
|
|
52859
|
-
out.push({
|
|
52860
|
-
file,
|
|
52861
|
-
line: ln,
|
|
52862
|
-
type: "type_juggling",
|
|
52863
|
-
category: "Type juggling (loose comparison)",
|
|
52864
|
-
severity: "medium",
|
|
52865
|
-
evidence: l.trim(),
|
|
52866
|
-
detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
|
|
52867
|
-
});
|
|
52868
|
-
}
|
|
52869
|
-
}
|
|
52870
|
-
function detectMassAssignment(code, file, out) {
|
|
52871
|
-
const ls = lines2(code);
|
|
52872
|
-
const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
|
|
52873
|
-
let m;
|
|
52874
|
-
while ((m = extractRe.exec(code)) !== null) {
|
|
52875
|
-
const ln = lineNo2(code, m.index);
|
|
52876
|
-
const l = ls[ln - 1] ?? "";
|
|
52877
|
-
if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
|
|
52878
|
-
continue;
|
|
52879
|
-
out.push({
|
|
52880
|
-
file,
|
|
52881
|
-
line: ln,
|
|
52882
|
-
type: "mass_assignment",
|
|
52883
|
-
category: "Mass assignment (variable injection)",
|
|
52884
|
-
severity: "high",
|
|
52885
|
-
evidence: l.trim(),
|
|
52886
|
-
detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
|
|
52887
|
-
});
|
|
52888
|
-
}
|
|
52889
|
-
const parseRe = /\bparse_str\s*\(/g;
|
|
52890
|
-
while ((m = parseRe.exec(code)) !== null) {
|
|
52891
|
-
const ln = lineNo2(code, m.index);
|
|
52892
|
-
const l = ls[ln - 1] ?? "";
|
|
52893
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
52894
|
-
`);
|
|
52895
|
-
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
52896
|
-
continue;
|
|
52897
|
-
out.push({
|
|
52898
|
-
file,
|
|
52899
|
-
line: ln,
|
|
52900
|
-
type: "mass_assignment",
|
|
52901
|
-
category: "Mass assignment (variable injection)",
|
|
52902
|
-
severity: "medium",
|
|
52903
|
-
evidence: l.trim(),
|
|
52904
|
-
detail: "`parse_str()` of user input into the symbol table — variable injection."
|
|
52905
|
-
});
|
|
52906
|
-
}
|
|
52907
|
-
}
|
|
52908
|
-
function detectComplexBugs(code, file) {
|
|
52909
|
-
const out = [];
|
|
52910
|
-
detectDeserialization(code, file, out);
|
|
52911
|
-
detectTypeJuggling(code, file, out);
|
|
52912
|
-
detectMassAssignment(code, file, out);
|
|
52913
|
-
detectPrototypePollution(code, file, out);
|
|
52914
|
-
detectCrlfInjection(code, file, out);
|
|
52915
|
-
detectPathConfusion(code, file, out);
|
|
52916
|
-
const seen = new Set;
|
|
52917
|
-
return out.filter((f) => {
|
|
52918
|
-
const key = `${f.type}:${f.line}`;
|
|
52919
|
-
if (seen.has(key))
|
|
52920
|
-
return false;
|
|
52921
|
-
seen.add(key);
|
|
52922
|
-
return true;
|
|
52923
|
-
});
|
|
52924
|
-
}
|
|
52925
|
-
function detectPrototypePollution(code, file, out) {
|
|
52926
|
-
const ls = lines2(code);
|
|
52927
|
-
const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
|
|
53251
|
+
function detectDynamicMethod(code, file, out) {
|
|
53252
|
+
const ls = lines(code);
|
|
53253
|
+
DYNAMIC_METHOD_RE.lastIndex = 0;
|
|
52928
53254
|
let m;
|
|
52929
|
-
while ((m =
|
|
52930
|
-
const ln =
|
|
53255
|
+
while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
|
|
53256
|
+
const ln = lineNo(code, m.index);
|
|
52931
53257
|
const l = ls[ln - 1] ?? "";
|
|
52932
53258
|
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52933
53259
|
`);
|
|
52934
|
-
if (!
|
|
53260
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
52935
53261
|
continue;
|
|
52936
53262
|
out.push({
|
|
52937
53263
|
file,
|
|
52938
53264
|
line: ln,
|
|
52939
|
-
type: "
|
|
52940
|
-
category: "
|
|
53265
|
+
type: "dynamic_method_call",
|
|
53266
|
+
category: "Route confusion (dynamic method dispatch)",
|
|
52941
53267
|
severity: "high",
|
|
52942
53268
|
evidence: l.trim(),
|
|
52943
|
-
detail: "
|
|
53269
|
+
detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
|
|
52944
53270
|
});
|
|
52945
53271
|
}
|
|
52946
53272
|
}
|
|
52947
|
-
function
|
|
52948
|
-
const ls =
|
|
52949
|
-
const
|
|
53273
|
+
function detectDynamicInclude(code, file, out) {
|
|
53274
|
+
const ls = lines(code);
|
|
53275
|
+
const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
|
|
52950
53276
|
let m;
|
|
52951
|
-
while ((m =
|
|
52952
|
-
const ln =
|
|
53277
|
+
while ((m = re.exec(code)) !== null) {
|
|
53278
|
+
const ln = lineNo(code, m.index);
|
|
52953
53279
|
const l = ls[ln - 1] ?? "";
|
|
52954
|
-
const scope = ls.slice(Math.max(0, ln -
|
|
53280
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52955
53281
|
`);
|
|
52956
|
-
if (!
|
|
53282
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
52957
53283
|
continue;
|
|
52958
53284
|
out.push({
|
|
52959
53285
|
file,
|
|
52960
53286
|
line: ln,
|
|
52961
|
-
type: "
|
|
52962
|
-
category: "
|
|
52963
|
-
severity: "
|
|
53287
|
+
type: "dynamic_include",
|
|
53288
|
+
category: "Route confusion (dynamic file include)",
|
|
53289
|
+
severity: "high",
|
|
52964
53290
|
evidence: l.trim(),
|
|
52965
|
-
detail:
|
|
53291
|
+
detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
|
|
52966
53292
|
});
|
|
52967
53293
|
}
|
|
52968
53294
|
}
|
|
52969
|
-
function
|
|
52970
|
-
|
|
52971
|
-
const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
|
|
53295
|
+
function detectBatchForwarding(code, file, out) {
|
|
53296
|
+
BATCH_LOOP_RE.lastIndex = 0;
|
|
52972
53297
|
let m;
|
|
52973
|
-
while ((m =
|
|
52974
|
-
const ln =
|
|
52975
|
-
const
|
|
52976
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53298
|
+
while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
|
|
53299
|
+
const ln = lineNo(code, m.index);
|
|
53300
|
+
const body = lines(code).slice(ln, ln + 8).join(`
|
|
52977
53301
|
`);
|
|
52978
|
-
|
|
52979
|
-
|
|
52980
|
-
if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
|
|
52981
|
-
` + l))
|
|
53302
|
+
const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
|
|
53303
|
+
if (!forwards)
|
|
52982
53304
|
continue;
|
|
53305
|
+
const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
|
|
52983
53306
|
out.push({
|
|
52984
53307
|
file,
|
|
52985
53308
|
line: ln,
|
|
52986
|
-
type: "
|
|
52987
|
-
category: "
|
|
52988
|
-
severity: "medium",
|
|
52989
|
-
evidence:
|
|
52990
|
-
detail: "
|
|
53309
|
+
type: "batch_forwarding",
|
|
53310
|
+
category: "Route confusion (batch/proxy forwarding)",
|
|
53311
|
+
severity: rechecks ? "medium" : "high",
|
|
53312
|
+
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
53313
|
+
detail: rechecks ? "Batch/proxy loop forwards to inner handlers — auth is re-checked in scope, but verify it applies per-route." : "Batch/proxy loop forwards to inner handlers with no per-route authorization re-check — an attacker may reach a protected handler."
|
|
52991
53314
|
});
|
|
52992
53315
|
}
|
|
52993
53316
|
}
|
|
52994
|
-
|
|
52995
|
-
|
|
52996
|
-
|
|
52997
|
-
|
|
52998
|
-
|
|
52999
|
-
|
|
53000
|
-
|
|
53001
|
-
|
|
53002
|
-
|
|
53003
|
-
|
|
53004
|
-
|
|
53005
|
-
|
|
53006
|
-
|
|
53007
|
-
|
|
53008
|
-
|
|
53009
|
-
|
|
53010
|
-
|
|
53011
|
-
|
|
53012
|
-
|
|
53013
|
-
|
|
53014
|
-
const steps = [];
|
|
53015
|
-
if (ctx.sanitizedCount && ctx.sanitizedCount > 0) {
|
|
53016
|
-
steps.push(`${ctx.sanitizedCount} sink(s) were sanitized and are NOT findings — do not report them. Only tainted, unsanitized, unguarded sinks count.`);
|
|
53017
|
-
}
|
|
53018
|
-
if (ctx.taintedSinks && ctx.taintedSinks.length > 0) {
|
|
53019
|
-
const seen = new Set;
|
|
53020
|
-
for (const k of ctx.taintedSinks) {
|
|
53021
|
-
if (seen.has(k))
|
|
53022
|
-
continue;
|
|
53023
|
-
seen.add(k);
|
|
53024
|
-
const d = SINK_DIRECTIVES[k];
|
|
53025
|
-
if (d)
|
|
53026
|
-
steps.push(d);
|
|
53027
|
-
}
|
|
53028
|
-
}
|
|
53029
|
-
if (steps.length === 0 && ctx.hasFindings) {
|
|
53030
|
-
steps.push("Findings found. Trace each sink's reachability (taint_file / eagle_eye), then validate live with strike_verify before reporting.");
|
|
53031
|
-
}
|
|
53032
|
-
if (ctx.findingStatus) {
|
|
53033
|
-
const transitions = {
|
|
53034
|
-
detected: "detected -> triaged (classify severity + CWE + reachability)",
|
|
53035
|
-
triaged: "triaged -> hypothesis (state the sink, source, and expected impact)",
|
|
53036
|
-
hypothesis: "hypothesis -> validating (run strike_verify with marker + negative control)",
|
|
53037
|
-
validating: "validating -> confirmed (only if marker reflected AND negative control inert) or false_positive",
|
|
53038
|
-
confirmed: "confirmed — attach evidence (SHA-256), then report."
|
|
53039
|
-
};
|
|
53040
|
-
if (transitions[ctx.findingStatus]) {
|
|
53041
|
-
steps.push(`Finding lifecycle next step: ${transitions[ctx.findingStatus]}.`);
|
|
53042
|
-
}
|
|
53043
|
-
}
|
|
53044
|
-
if (ctx.hasFindings || ctx.findingStatus) {
|
|
53045
|
-
steps.push("Record every confirmed finding with finding_create, attach confidence_score, and redact any secrets before persisting.");
|
|
53046
|
-
}
|
|
53047
|
-
return steps;
|
|
53048
|
-
}
|
|
53049
|
-
|
|
53050
|
-
// src/live-recon.ts
|
|
53051
|
-
import { resolve4 } from "node:dns/promises";
|
|
53052
|
-
import { connect } from "node:net";
|
|
53053
|
-
var HTTP_TIMEOUT_MS = 20000;
|
|
53054
|
-
async function httpGet(url, opts = {}) {
|
|
53055
|
-
const controller = new AbortController;
|
|
53056
|
-
const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
53057
|
-
try {
|
|
53058
|
-
const res = await fetch(url, { signal: controller.signal, redirect: opts.redirect ?? "follow" });
|
|
53059
|
-
const body = await res.text();
|
|
53060
|
-
const headers = {};
|
|
53061
|
-
res.headers.forEach((v, k) => {
|
|
53062
|
-
headers[k] = v;
|
|
53317
|
+
function detectRouteNormalization(code, file, out) {
|
|
53318
|
+
const ls = lines(code);
|
|
53319
|
+
for (let i = 0;i < ls.length; i++) {
|
|
53320
|
+
const l = ls[i];
|
|
53321
|
+
if (!ROUTE_MATCH_RE.test(l))
|
|
53322
|
+
continue;
|
|
53323
|
+
const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
|
|
53324
|
+
`);
|
|
53325
|
+
if (!userControlled(scope))
|
|
53326
|
+
continue;
|
|
53327
|
+
if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
|
|
53328
|
+
continue;
|
|
53329
|
+
out.push({
|
|
53330
|
+
file,
|
|
53331
|
+
line: i + 1,
|
|
53332
|
+
type: "route_normalization_gap",
|
|
53333
|
+
category: "Route confusion (missing path normalization)",
|
|
53334
|
+
severity: "medium",
|
|
53335
|
+
evidence: l.trim(),
|
|
53336
|
+
detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
|
|
53063
53337
|
});
|
|
53064
|
-
return { status: res.status, headers, body: body.slice(0, 50000), ok: res.ok };
|
|
53065
|
-
} catch (e) {
|
|
53066
|
-
return { status: 0, headers: {}, body: String(e), ok: false };
|
|
53067
|
-
} finally {
|
|
53068
|
-
clearTimeout(t);
|
|
53069
|
-
}
|
|
53070
|
-
}
|
|
53071
|
-
function normalizeTarget2(target) {
|
|
53072
|
-
let t = target.trim();
|
|
53073
|
-
if (!/^https?:\/\//i.test(t))
|
|
53074
|
-
t = `https://${t}`;
|
|
53075
|
-
return t.replace(/\/+$/, "");
|
|
53076
|
-
}
|
|
53077
|
-
function crawlLinks(base, body) {
|
|
53078
|
-
const links = [];
|
|
53079
|
-
const scripts = [];
|
|
53080
|
-
const hrefRe = /<a[^>]+href=["']([^"'#]+)["']/gi;
|
|
53081
|
-
const scriptRe = /<script[^>]+src=["']([^"'#]+)["']/gi;
|
|
53082
|
-
let m;
|
|
53083
|
-
while ((m = hrefRe.exec(body)) !== null) {
|
|
53084
|
-
let u = m[1];
|
|
53085
|
-
if (u.startsWith("/"))
|
|
53086
|
-
u = base + u;
|
|
53087
|
-
else if (!/^https?:\/\//i.test(u))
|
|
53088
|
-
u = base + "/" + u;
|
|
53089
|
-
links.push(u);
|
|
53090
|
-
}
|
|
53091
|
-
while ((m = scriptRe.exec(body)) !== null) {
|
|
53092
|
-
let u = m[1];
|
|
53093
|
-
if (u.startsWith("/"))
|
|
53094
|
-
u = base + u;
|
|
53095
|
-
else if (!/^https?:\/\//i.test(u))
|
|
53096
|
-
u = base + "/" + u;
|
|
53097
|
-
scripts.push(u);
|
|
53098
53338
|
}
|
|
53099
|
-
return { links, scripts };
|
|
53100
|
-
}
|
|
53101
|
-
async function fetchRobotsAndSitemap(base) {
|
|
53102
|
-
const robots = [];
|
|
53103
|
-
const sitemap = [];
|
|
53104
|
-
const rob = await httpGet(`${base}/robots.txt`);
|
|
53105
|
-
if (rob.status === 200) {
|
|
53106
|
-
for (const line of rob.body.split(`
|
|
53107
|
-
`)) {
|
|
53108
|
-
const sm = line.match(/^Sitemap:\s*(.+)$/i);
|
|
53109
|
-
if (sm)
|
|
53110
|
-
sitemap.push(sm[1].trim());
|
|
53111
|
-
const allow = line.match(/^(?:Allow|Disallow):\s*(.+)$/i);
|
|
53112
|
-
if (allow && !allow[1].includes("*"))
|
|
53113
|
-
robots.push(base + allow[1].trim());
|
|
53114
|
-
}
|
|
53115
|
-
}
|
|
53116
|
-
const sm = await httpGet(`${base}/sitemap.xml`);
|
|
53117
|
-
if (sm.status === 200 && sm.body.includes("<loc>")) {
|
|
53118
|
-
const locRe = /<loc>([^<]+)<\/loc>/gi;
|
|
53119
|
-
let m;
|
|
53120
|
-
while ((m = locRe.exec(sm.body)) !== null)
|
|
53121
|
-
sitemap.push(m[1].trim());
|
|
53122
|
-
}
|
|
53123
|
-
return { robots, sitemap };
|
|
53124
53339
|
}
|
|
53125
|
-
function
|
|
53126
|
-
const
|
|
53127
|
-
|
|
53128
|
-
|
|
53129
|
-
|
|
53130
|
-
|
|
53131
|
-
|
|
53132
|
-
|
|
53133
|
-
}
|
|
53134
|
-
return [...params];
|
|
53340
|
+
function detectRouteConfusion(code, file) {
|
|
53341
|
+
const out = [];
|
|
53342
|
+
detectDynamicDispatch(code, file, out);
|
|
53343
|
+
detectDynamicMethod(code, file, out);
|
|
53344
|
+
detectDynamicInclude(code, file, out);
|
|
53345
|
+
detectBatchForwarding(code, file, out);
|
|
53346
|
+
detectRouteNormalization(code, file, out);
|
|
53347
|
+
return out;
|
|
53135
53348
|
}
|
|
53136
|
-
|
|
53137
|
-
|
|
53138
|
-
|
|
53139
|
-
|
|
53140
|
-
|
|
53141
|
-
|
|
53142
|
-
const data = JSON.parse(r.body);
|
|
53143
|
-
for (const entry of data) {
|
|
53144
|
-
for (const field of (entry.name_value ?? "").split(`
|
|
53145
|
-
`)) {
|
|
53146
|
-
const name = field.trim().toLowerCase();
|
|
53147
|
-
if (name && name.endsWith(domain.toLowerCase()) && !name.includes("*"))
|
|
53148
|
-
out.add(name);
|
|
53149
|
-
}
|
|
53150
|
-
}
|
|
53151
|
-
}
|
|
53152
|
-
} catch {}
|
|
53153
|
-
return { subdomains: [...out].sort(), source: "crt.sh (certificate transparency)" };
|
|
53349
|
+
|
|
53350
|
+
// src/complex-bugs.ts
|
|
53351
|
+
var SOURCE_TOKENS2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SERVER)\b|\$request->|\$req->|request\.(get|input|query|post|data|body|headers|cookies|params)|getParameter\(|req\.(query|body|params|headers|cookies)/;
|
|
53352
|
+
function lines2(code) {
|
|
53353
|
+
return code.split(`
|
|
53354
|
+
`);
|
|
53154
53355
|
}
|
|
53155
|
-
|
|
53156
|
-
|
|
53157
|
-
|
|
53158
|
-
|
|
53159
|
-
|
|
53160
|
-
|
|
53161
|
-
const res = await fetch(url, { signal: controller.signal, redirect: "manual" });
|
|
53162
|
-
const body = await res.text();
|
|
53163
|
-
clearTimeout(t);
|
|
53164
|
-
if (body.startsWith("[[")) {
|
|
53165
|
-
const data = JSON.parse(body);
|
|
53166
|
-
for (let i = 1;i < data.length; i++) {
|
|
53167
|
-
const u = data[i]?.[0];
|
|
53168
|
-
if (u && typeof u === "string" && u.includes(domain))
|
|
53169
|
-
out.add(u);
|
|
53170
|
-
}
|
|
53171
|
-
}
|
|
53172
|
-
} catch {}
|
|
53173
|
-
return { urls: [...out].slice(0, 500), source: "web.archive.org (Wayback CDX)" };
|
|
53356
|
+
function lineNo2(code, pos) {
|
|
53357
|
+
let n = 1;
|
|
53358
|
+
for (let i = 0;i < pos; i++)
|
|
53359
|
+
if (code.charCodeAt(i) === 10)
|
|
53360
|
+
n++;
|
|
53361
|
+
return n;
|
|
53174
53362
|
}
|
|
53175
|
-
var
|
|
53176
|
-
|
|
53177
|
-
|
|
53178
|
-
|
|
53179
|
-
|
|
53180
|
-
|
|
53181
|
-
|
|
53182
|
-
|
|
53183
|
-
|
|
53184
|
-
|
|
53185
|
-
|
|
53186
|
-
|
|
53187
|
-
|
|
53188
|
-
"store",
|
|
53189
|
-
"cdn",
|
|
53190
|
-
"static",
|
|
53191
|
-
"assets",
|
|
53192
|
-
"img",
|
|
53193
|
-
"images",
|
|
53194
|
-
"secure",
|
|
53195
|
-
"vpn",
|
|
53196
|
-
"remote",
|
|
53197
|
-
"webmail",
|
|
53198
|
-
"smtp",
|
|
53199
|
-
"pop",
|
|
53200
|
-
"imap",
|
|
53201
|
-
"ns1",
|
|
53202
|
-
"ns2",
|
|
53203
|
-
"dns",
|
|
53204
|
-
"db",
|
|
53205
|
-
"database",
|
|
53206
|
-
"mysql",
|
|
53207
|
-
"git",
|
|
53208
|
-
"gitlab",
|
|
53209
|
-
"jenkins",
|
|
53210
|
-
"ci",
|
|
53211
|
-
"jira",
|
|
53212
|
-
"wiki",
|
|
53213
|
-
"docs",
|
|
53214
|
-
"help",
|
|
53215
|
-
"support",
|
|
53216
|
-
"status",
|
|
53217
|
-
"monitor",
|
|
53218
|
-
"grafana",
|
|
53219
|
-
"kibana",
|
|
53220
|
-
"backup",
|
|
53221
|
-
"old",
|
|
53222
|
-
"beta",
|
|
53223
|
-
"demo",
|
|
53224
|
-
"sandbox",
|
|
53225
|
-
"internal",
|
|
53226
|
-
"intranet"
|
|
53227
|
-
];
|
|
53228
|
-
async function dnsBrute(domain) {
|
|
53229
|
-
const found = [];
|
|
53230
|
-
const base = domain.toLowerCase();
|
|
53231
|
-
const seen = new Set([base, "www." + base]);
|
|
53232
|
-
for (const prefix of COMMON_SUBDOMAINS) {
|
|
53233
|
-
const host = `${prefix}.${base}`;
|
|
53234
|
-
if (seen.has(host))
|
|
53363
|
+
var MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
|
|
53364
|
+
function detectDeserialization(code, file, out) {
|
|
53365
|
+
if (!MAGIC_METHODS.test(code))
|
|
53366
|
+
return;
|
|
53367
|
+
const ls = lines2(code);
|
|
53368
|
+
const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
|
|
53369
|
+
let m;
|
|
53370
|
+
while ((m = re.exec(code)) !== null) {
|
|
53371
|
+
const ln = lineNo2(code, m.index);
|
|
53372
|
+
const l = ls[ln - 1] ?? "";
|
|
53373
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
53374
|
+
`);
|
|
53375
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53235
53376
|
continue;
|
|
53236
|
-
|
|
53237
|
-
|
|
53238
|
-
|
|
53239
|
-
|
|
53240
|
-
|
|
53241
|
-
|
|
53377
|
+
out.push({
|
|
53378
|
+
file,
|
|
53379
|
+
line: ln,
|
|
53380
|
+
type: "deserialization",
|
|
53381
|
+
category: "Deserialization → POP gadget chain",
|
|
53382
|
+
severity: "high",
|
|
53383
|
+
evidence: l.trim(),
|
|
53384
|
+
detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
|
|
53385
|
+
});
|
|
53242
53386
|
}
|
|
53243
|
-
return { found, source: "DNS resolution (common subdomain prefixes)" };
|
|
53244
53387
|
}
|
|
53245
|
-
|
|
53246
|
-
|
|
53247
|
-
[
|
|
53248
|
-
|
|
53249
|
-
|
|
53250
|
-
|
|
53251
|
-
|
|
53252
|
-
|
|
53253
|
-
|
|
53254
|
-
|
|
53255
|
-
|
|
53256
|
-
|
|
53257
|
-
|
|
53258
|
-
|
|
53259
|
-
|
|
53260
|
-
|
|
53261
|
-
|
|
53262
|
-
|
|
53263
|
-
|
|
53264
|
-
|
|
53265
|
-
|
|
53266
|
-
[8443, "https-alt"],
|
|
53267
|
-
[9000, "app"],
|
|
53268
|
-
[9090, "app"],
|
|
53269
|
-
[9200, "elasticsearch"],
|
|
53270
|
-
[11211, "memcached"],
|
|
53271
|
-
[27017, "mongodb"]
|
|
53272
|
-
];
|
|
53273
|
-
function tcpProbe(host, port, timeout = 2000) {
|
|
53274
|
-
return new Promise((resolve) => {
|
|
53275
|
-
const socket = connect({ host, port, timeout });
|
|
53276
|
-
socket.once("connect", () => {
|
|
53277
|
-
socket.destroy();
|
|
53278
|
-
resolve(true);
|
|
53388
|
+
function detectTypeJuggling(code, file, out) {
|
|
53389
|
+
const ls = lines2(code);
|
|
53390
|
+
const re = /(?<![=!<>])(==|!=)(?!=)/g;
|
|
53391
|
+
let m;
|
|
53392
|
+
while ((m = re.exec(code)) !== null) {
|
|
53393
|
+
const ln = lineNo2(code, m.index);
|
|
53394
|
+
const l = ls[ln - 1] ?? "";
|
|
53395
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
53396
|
+
`);
|
|
53397
|
+
const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
|
|
53398
|
+
const hasHash = /\b(md5|sha1|sha256|hash|crc32|crypt|password_verify|strcmp|hash_equals|secret|token|nonce)\s*\(?/i.test(l) || /\b(secret|token|nonce|hash)\b/i.test(l);
|
|
53399
|
+
if (!hasSource || !hasHash)
|
|
53400
|
+
continue;
|
|
53401
|
+
out.push({
|
|
53402
|
+
file,
|
|
53403
|
+
line: ln,
|
|
53404
|
+
type: "type_juggling",
|
|
53405
|
+
category: "Type juggling (loose comparison)",
|
|
53406
|
+
severity: "medium",
|
|
53407
|
+
evidence: l.trim(),
|
|
53408
|
+
detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
|
|
53279
53409
|
});
|
|
53280
|
-
|
|
53281
|
-
|
|
53282
|
-
|
|
53410
|
+
}
|
|
53411
|
+
}
|
|
53412
|
+
function detectMassAssignment(code, file, out) {
|
|
53413
|
+
const ls = lines2(code);
|
|
53414
|
+
const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
|
|
53415
|
+
let m;
|
|
53416
|
+
while ((m = extractRe.exec(code)) !== null) {
|
|
53417
|
+
const ln = lineNo2(code, m.index);
|
|
53418
|
+
const l = ls[ln - 1] ?? "";
|
|
53419
|
+
if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
|
|
53420
|
+
continue;
|
|
53421
|
+
out.push({
|
|
53422
|
+
file,
|
|
53423
|
+
line: ln,
|
|
53424
|
+
type: "mass_assignment",
|
|
53425
|
+
category: "Mass assignment (variable injection)",
|
|
53426
|
+
severity: "high",
|
|
53427
|
+
evidence: l.trim(),
|
|
53428
|
+
detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
|
|
53283
53429
|
});
|
|
53284
|
-
|
|
53285
|
-
|
|
53286
|
-
|
|
53430
|
+
}
|
|
53431
|
+
const parseRe = /\bparse_str\s*\(/g;
|
|
53432
|
+
while ((m = parseRe.exec(code)) !== null) {
|
|
53433
|
+
const ln = lineNo2(code, m.index);
|
|
53434
|
+
const l = ls[ln - 1] ?? "";
|
|
53435
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53436
|
+
`);
|
|
53437
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53438
|
+
continue;
|
|
53439
|
+
out.push({
|
|
53440
|
+
file,
|
|
53441
|
+
line: ln,
|
|
53442
|
+
type: "mass_assignment",
|
|
53443
|
+
category: "Mass assignment (variable injection)",
|
|
53444
|
+
severity: "medium",
|
|
53445
|
+
evidence: l.trim(),
|
|
53446
|
+
detail: "`parse_str()` of user input into the symbol table — variable injection."
|
|
53287
53447
|
});
|
|
53448
|
+
}
|
|
53449
|
+
}
|
|
53450
|
+
function detectComplexBugs(code, file) {
|
|
53451
|
+
const out = [];
|
|
53452
|
+
detectDeserialization(code, file, out);
|
|
53453
|
+
detectTypeJuggling(code, file, out);
|
|
53454
|
+
detectMassAssignment(code, file, out);
|
|
53455
|
+
detectPrototypePollution(code, file, out);
|
|
53456
|
+
detectCrlfInjection(code, file, out);
|
|
53457
|
+
detectPathConfusion(code, file, out);
|
|
53458
|
+
const seen = new Set;
|
|
53459
|
+
return out.filter((f) => {
|
|
53460
|
+
const key = `${f.type}:${f.line}`;
|
|
53461
|
+
if (seen.has(key))
|
|
53462
|
+
return false;
|
|
53463
|
+
seen.add(key);
|
|
53464
|
+
return true;
|
|
53288
53465
|
});
|
|
53289
53466
|
}
|
|
53290
|
-
|
|
53291
|
-
const
|
|
53292
|
-
|
|
53293
|
-
|
|
53294
|
-
|
|
53295
|
-
|
|
53296
|
-
|
|
53297
|
-
|
|
53467
|
+
function detectPrototypePollution(code, file, out) {
|
|
53468
|
+
const ls = lines2(code);
|
|
53469
|
+
const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
|
|
53470
|
+
let m;
|
|
53471
|
+
while ((m = mergeRe.exec(code)) !== null) {
|
|
53472
|
+
const ln = lineNo2(code, m.index);
|
|
53473
|
+
const l = ls[ln - 1] ?? "";
|
|
53474
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
53475
|
+
`);
|
|
53476
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53477
|
+
continue;
|
|
53478
|
+
out.push({
|
|
53479
|
+
file,
|
|
53480
|
+
line: ln,
|
|
53481
|
+
type: "prototype_pollution",
|
|
53482
|
+
category: "Prototype pollution (unsafe merge)",
|
|
53483
|
+
severity: "high",
|
|
53484
|
+
evidence: l.trim(),
|
|
53485
|
+
detail: "Merge/extend of request data into an object without a `__proto__`/`constructor.prototype` guard — attacker-controlled keys can pollute `Object.prototype` and pivot to RCE or property injection."
|
|
53486
|
+
});
|
|
53298
53487
|
}
|
|
53299
|
-
return results;
|
|
53300
53488
|
}
|
|
53301
|
-
|
|
53302
|
-
|
|
53303
|
-
|
|
53304
|
-
|
|
53305
|
-
|
|
53306
|
-
|
|
53307
|
-
|
|
53308
|
-
|
|
53309
|
-
|
|
53310
|
-
|
|
53311
|
-
|
|
53312
|
-
|
|
53313
|
-
|
|
53314
|
-
|
|
53315
|
-
|
|
53316
|
-
|
|
53317
|
-
|
|
53318
|
-
|
|
53319
|
-
|
|
53320
|
-
|
|
53321
|
-
if (r.status >= 200 && r.status < 400) {
|
|
53322
|
-
out.push({ path: p, status: r.status, body_size: r.body.length });
|
|
53323
|
-
}
|
|
53489
|
+
function detectCrlfInjection(code, file, out) {
|
|
53490
|
+
const ls = lines2(code);
|
|
53491
|
+
const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
|
|
53492
|
+
let m;
|
|
53493
|
+
while ((m = headerRe.exec(code)) !== null) {
|
|
53494
|
+
const ln = lineNo2(code, m.index);
|
|
53495
|
+
const l = ls[ln - 1] ?? "";
|
|
53496
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53497
|
+
`);
|
|
53498
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53499
|
+
continue;
|
|
53500
|
+
out.push({
|
|
53501
|
+
file,
|
|
53502
|
+
line: ln,
|
|
53503
|
+
type: "crlf_injection",
|
|
53504
|
+
category: "CRLF / header injection",
|
|
53505
|
+
severity: "medium",
|
|
53506
|
+
evidence: l.trim(),
|
|
53507
|
+
detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
|
|
53508
|
+
});
|
|
53324
53509
|
}
|
|
53325
|
-
return out;
|
|
53326
53510
|
}
|
|
53327
|
-
function
|
|
53328
|
-
const
|
|
53329
|
-
const
|
|
53330
|
-
|
|
53331
|
-
|
|
53332
|
-
|
|
53333
|
-
|
|
53334
|
-
|
|
53335
|
-
|
|
53336
|
-
|
|
53337
|
-
|
|
53338
|
-
|
|
53339
|
-
|
|
53340
|
-
|
|
53341
|
-
|
|
53342
|
-
|
|
53343
|
-
|
|
53344
|
-
|
|
53345
|
-
|
|
53346
|
-
|
|
53347
|
-
|
|
53348
|
-
|
|
53511
|
+
function detectPathConfusion(code, file, out) {
|
|
53512
|
+
const ls = lines2(code);
|
|
53513
|
+
const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
|
|
53514
|
+
let m;
|
|
53515
|
+
while ((m = fileRe.exec(code)) !== null) {
|
|
53516
|
+
const ln = lineNo2(code, m.index);
|
|
53517
|
+
const l = ls[ln - 1] ?? "";
|
|
53518
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53519
|
+
`);
|
|
53520
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53521
|
+
continue;
|
|
53522
|
+
if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
|
|
53523
|
+
` + l))
|
|
53524
|
+
continue;
|
|
53525
|
+
out.push({
|
|
53526
|
+
file,
|
|
53527
|
+
line: ln,
|
|
53528
|
+
type: "path_confusion",
|
|
53529
|
+
category: "Path confusion (unsanitized file path)",
|
|
53530
|
+
severity: "medium",
|
|
53531
|
+
evidence: l.trim(),
|
|
53532
|
+
detail: "User input reaches a file path with no canonicalization (`basename`/`realpath`/`normalize`) — traversal, `..%2f`, backslash and null-byte variants can confuse a naive `../` filter."
|
|
53533
|
+
});
|
|
53349
53534
|
}
|
|
53350
|
-
return out;
|
|
53351
|
-
}
|
|
53352
|
-
function correlateIntel(techs, versionList, portList) {
|
|
53353
|
-
const tech = techs.map((t) => ({ tech: t, ...techCorrelation(t) }));
|
|
53354
|
-
const versions = versionList.map((v) => ({
|
|
53355
|
-
product: v.product,
|
|
53356
|
-
version: v.version,
|
|
53357
|
-
correlation: techCorrelation(v.product),
|
|
53358
|
-
payload_lookup: payloadLookup(v.product),
|
|
53359
|
-
template_lookup: templateLookup(v.product, 5)
|
|
53360
|
-
}));
|
|
53361
|
-
const ports = portList.map((p) => ({ port: p.port, service: p.service, ...portCorrelation(p.port) }));
|
|
53362
|
-
return { tech, versions, ports };
|
|
53363
53535
|
}
|
|
53364
|
-
|
|
53365
|
-
|
|
53366
|
-
|
|
53367
|
-
|
|
53368
|
-
|
|
53369
|
-
|
|
53370
|
-
|
|
53536
|
+
|
|
53537
|
+
// src/guidance.ts
|
|
53538
|
+
var SINK_DIRECTIVES = {
|
|
53539
|
+
sql_execution: "Validate the SQL injection live with strike_verify (use a unique MARKER and a NEGATIVE CONTROL that stays inert), or read_tool_manual for sqlmap. Only a reflected marker + inert control = confirmed.",
|
|
53540
|
+
command_execution: "Validate command injection with strike_verify (e.g. `id`/`whoami` marker vs a control string). Read the relevant tool manual first (commix / os-command-injection).",
|
|
53541
|
+
code_execution: "Code execution is a high-impact claim — confirm reachability end-to-end first, then validate with strike_verify. Never report eval/exec without a live marker.",
|
|
53542
|
+
file_operations: "For upload sinks, confirm the file is writable and the uploaded content is reachable/executable. Read payload_lookup for upload payloads; validate with strike_verify.",
|
|
53543
|
+
file_inclusion: "Validate LFI/RFI with strike_verify (marker file read vs control). Read payload_lookup for local-file-inclusion payloads and check for wrappers (php://, data://).",
|
|
53544
|
+
deserialization: "Deserialization needs a crafted gadget chain — read_tool_manual (ysoserial / PHPGGC) and confirm the entry point is reachable before validating.",
|
|
53545
|
+
http_request: "Validate SSRF with strike_verify (marker to a canary host vs control). Read payload_lookup for ssrf payloads and check for protocol/redirect filters.",
|
|
53546
|
+
redirect: "Validate open redirect with strike_verify (marker URL vs control). Confirm the redirect reflects attacker-controlled input.",
|
|
53547
|
+
html_render: "Validate the XSS with strike_verify (reflected marker script vs inert control). Check WAF first with detect_waf — many XSS are filtered at the edge.",
|
|
53548
|
+
xml_processing: "Validate XXE with strike_verify (external entity marker vs control). Read payload_lookup for xxe payloads.",
|
|
53549
|
+
archive_extraction: "Validate the archive/path issue (zip-slip / path traversal) with strike_verify. Confirm the extraction path is attacker-controlled.",
|
|
53550
|
+
path_traversal: "Validate path traversal with strike_verify (marker file read vs control). Read payload_lookup for path-traversal payloads.",
|
|
53551
|
+
template_injection: "SSTI needs a template-expression probe (e.g. {{7*7}} / ${7*7} / <%= 7*7 %>) — the marker is the evaluated arithmetic, the control is a benign literal. Confirm the template engine actually renders user input before reporting.",
|
|
53552
|
+
xpath_injection: "Validate XPath injection with strike_verify (an XPath boolean/union probe vs a control that stays inert). Confirm the XPath expression is built from attacker-controlled input.",
|
|
53553
|
+
ldap_injection: "Validate LDAP injection with strike_verify (an LDAP filter-wildcard/OR probe vs a control). Confirm the filter is concatenated from attacker-controlled input."
|
|
53554
|
+
};
|
|
53555
|
+
function nextSteps(ctx) {
|
|
53556
|
+
const steps = [];
|
|
53557
|
+
if (ctx.sanitizedCount && ctx.sanitizedCount > 0) {
|
|
53558
|
+
steps.push(`${ctx.sanitizedCount} sink(s) were sanitized and are NOT findings — do not report them. Only tainted, unsanitized, unguarded sinks count.`);
|
|
53559
|
+
}
|
|
53560
|
+
if (ctx.taintedSinks && ctx.taintedSinks.length > 0) {
|
|
53561
|
+
const seen = new Set;
|
|
53562
|
+
for (const k of ctx.taintedSinks) {
|
|
53563
|
+
if (seen.has(k))
|
|
53564
|
+
continue;
|
|
53565
|
+
seen.add(k);
|
|
53566
|
+
const d = SINK_DIRECTIVES[k];
|
|
53567
|
+
if (d)
|
|
53568
|
+
steps.push(d);
|
|
53371
53569
|
}
|
|
53372
|
-
})();
|
|
53373
|
-
const root = await httpGet(base);
|
|
53374
|
-
const interestingHeaders = {};
|
|
53375
|
-
for (const k of ["server", "x-powered-by", "x-aspnet-version", "x-drupal-cache", "x-generator", "via", "x-cache", "cf-ray"]) {
|
|
53376
|
-
if (root.headers[k])
|
|
53377
|
-
interestingHeaders[k] = root.headers[k];
|
|
53378
53570
|
}
|
|
53379
|
-
|
|
53380
|
-
|
|
53381
|
-
title: (root.body.match(/<title[^>]*>([^<]+)<\/title>/i) ?? [])[1]?.trim().slice(0, 200) ?? "",
|
|
53382
|
-
server: root.headers["server"] ?? root.headers["x-powered-by"] ?? "",
|
|
53383
|
-
headers: interestingHeaders,
|
|
53384
|
-
body_size: root.body.length
|
|
53385
|
-
};
|
|
53386
|
-
const waf = detectWaf(root.headers, root.body);
|
|
53387
|
-
const techSig = [
|
|
53388
|
-
[/wp-content|wp-includes|wp-json/i, "wordpress"],
|
|
53389
|
-
[/wp-login\.php|wp-admin/i, "wordpress"],
|
|
53390
|
-
[/powered by joomla|com_content/i, "joomla"],
|
|
53391
|
-
[/laravel|_token|csrf-token/i, "laravel"],
|
|
53392
|
-
[/react|__NEXT_DATA__|next\/static/i, "nextjs"],
|
|
53393
|
-
[/angular|ng-version/i, "angular"],
|
|
53394
|
-
[/vue|__vue__|v-data/i, "vue"],
|
|
53395
|
-
[/django|csrftoken|__debug__/i, "django"],
|
|
53396
|
-
[/ruby on rails|rails/i, "rails"],
|
|
53397
|
-
[/asp\.net|__VIEWSTATE|__EVENTVALIDATION/i, "aspnet"],
|
|
53398
|
-
[/phpBB|phpbb/i, "phpbb"],
|
|
53399
|
-
[/mybb|mybb/i, "mybb"],
|
|
53400
|
-
[/drupal|Drupal\.settings/i, "drupal"],
|
|
53401
|
-
[/magento|Mage\./i, "magento"],
|
|
53402
|
-
[/shopify|cdn\.shopify/i, "shopify"],
|
|
53403
|
-
[/grafana|kibana|elastic/i, "grafana-kibana"],
|
|
53404
|
-
[/node\.js|express/i, "nodejs"],
|
|
53405
|
-
[/flask|werkzeug/i, "flask"],
|
|
53406
|
-
[/spring|actuator/i, "spring"]
|
|
53407
|
-
];
|
|
53408
|
-
const techs = [...new Set(techSig.filter(([re]) => re.test(root.body)).map(([, t]) => t))];
|
|
53409
|
-
const versions = extractVersion(root.headers, root.body);
|
|
53410
|
-
const { links, scripts } = crawlLinks(base, root.body);
|
|
53411
|
-
const { robots, sitemap } = await fetchRobotsAndSitemap(base);
|
|
53412
|
-
const allUrls = [...links, ...scripts, ...robots, ...sitemap];
|
|
53413
|
-
const crawl = { links, scripts, endpoints: [...new Set(allUrls)], robots, sitemap, count: allUrls.length };
|
|
53414
|
-
const params = extractParams(allUrls);
|
|
53415
|
-
const { subdomains } = await subdomainEnum(host);
|
|
53416
|
-
const { found: dnsFindings } = await dnsBrute(host);
|
|
53417
|
-
const { urls: wayback } = await waybackUrls(host);
|
|
53418
|
-
const apiEndpoints = (await discoverApi(base)).map(({ path, status }) => ({ path, status }));
|
|
53419
|
-
let openPorts = [];
|
|
53420
|
-
if (includeActive && root.status > 0) {
|
|
53421
|
-
const scan = await scanPorts(host);
|
|
53422
|
-
openPorts = scan.map(({ port, service }) => ({ port, service }));
|
|
53571
|
+
if (steps.length === 0 && ctx.hasFindings) {
|
|
53572
|
+
steps.push("Findings found. Trace each sink's reachability (taint_file / eagle_eye), then validate live with strike_verify before reporting.");
|
|
53423
53573
|
}
|
|
53424
|
-
|
|
53425
|
-
|
|
53426
|
-
|
|
53427
|
-
|
|
53428
|
-
|
|
53429
|
-
|
|
53430
|
-
|
|
53431
|
-
|
|
53574
|
+
if (ctx.findingStatus) {
|
|
53575
|
+
const transitions = {
|
|
53576
|
+
detected: "detected -> triaged (classify severity + CWE + reachability)",
|
|
53577
|
+
triaged: "triaged -> hypothesis (state the sink, source, and expected impact)",
|
|
53578
|
+
hypothesis: "hypothesis -> validating (run strike_verify with marker + negative control)",
|
|
53579
|
+
validating: "validating -> confirmed (only if marker reflected AND negative control inert) or false_positive",
|
|
53580
|
+
confirmed: "confirmed — attach evidence (SHA-256), then report."
|
|
53581
|
+
};
|
|
53582
|
+
if (transitions[ctx.findingStatus]) {
|
|
53583
|
+
steps.push(`Finding lifecycle next step: ${transitions[ctx.findingStatus]}.`);
|
|
53584
|
+
}
|
|
53432
53585
|
}
|
|
53433
|
-
|
|
53434
|
-
|
|
53586
|
+
if (ctx.hasFindings || ctx.findingStatus) {
|
|
53587
|
+
steps.push("Record every confirmed finding with finding_create, attach confidence_score, and redact any secrets before persisting.");
|
|
53435
53588
|
}
|
|
53436
|
-
|
|
53437
|
-
nextSteps.push(`Found ${crawl.endpoints.length} endpoints — feed them to taint_file/blitz_scan for source analysis, or strike_verify for live validation.`);
|
|
53438
|
-
if (params.length > 0)
|
|
53439
|
-
nextSteps.push(`Discovered input params: ${params.join(", ")} — these are attack-surface entry points; fuzz them with payload_lookup after scope confirms.`);
|
|
53440
|
-
if (subdomains.length > 0)
|
|
53441
|
-
nextSteps.push(`Found ${subdomains.length} subdomains — each is a separate scope surface; enumerate further with fofa_search.`);
|
|
53442
|
-
if (dnsFindings.length > 0)
|
|
53443
|
-
nextSteps.push(`DNS-resolved ${dnsFindings.length} hosts (${dnsFindings.map((d) => d.host).slice(0, 8).join(", ")}${dnsFindings.length > 8 ? "…" : ""}) — each is a live subdomain to recon.`);
|
|
53444
|
-
if (wayback.length > 0)
|
|
53445
|
-
nextSteps.push(`Wayback Machine surfaced ${wayback.length} historical URLs — mine them for hidden endpoints, old params, and forgotten pages; feed to taint_file/blitz_scan.`);
|
|
53446
|
-
if (openPorts.length > 0)
|
|
53447
|
-
nextSteps.push(`Open ports: ${openPorts.map((p) => `${p.port}/${p.service}`).join(", ")} — map services via port_correlation and probe each with the relevant tool manual.`);
|
|
53448
|
-
if (apiEndpoints.length > 0)
|
|
53449
|
-
nextSteps.push(`API endpoints exposed (${apiEndpoints.map((e) => e.path).join(", ")}) — audit them with api-security playbook; check for BOLA/IDOR/auth gaps.`);
|
|
53450
|
-
if (nextSteps.length === 0)
|
|
53451
|
-
nextSteps.push("Nothing exposed on the surface — try subdomain enumeration, port scan (active), or request source access for EAGLE-EYE analysis.");
|
|
53452
|
-
nextSteps.push("Every confirmed finding: record with finding_create, attach confidence_score, redact secrets. A hit is a hypothesis until verified.");
|
|
53453
|
-
return {
|
|
53454
|
-
target: base,
|
|
53455
|
-
host,
|
|
53456
|
-
fingerprint,
|
|
53457
|
-
waf,
|
|
53458
|
-
tech: { detected: techs, server: fingerprint.server },
|
|
53459
|
-
versions,
|
|
53460
|
-
crawl,
|
|
53461
|
-
params,
|
|
53462
|
-
subdomains,
|
|
53463
|
-
dns_findings: dnsFindings,
|
|
53464
|
-
wayback_urls: wayback,
|
|
53465
|
-
api_endpoints: apiEndpoints,
|
|
53466
|
-
open_ports: openPorts,
|
|
53467
|
-
intel,
|
|
53468
|
-
next_steps: nextSteps
|
|
53469
|
-
};
|
|
53589
|
+
return steps;
|
|
53470
53590
|
}
|
|
53471
53591
|
|
|
53472
53592
|
// src/benchmark.ts
|
|
@@ -53812,7 +53932,7 @@ function createServer() {
|
|
|
53812
53932
|
name: "blitzstrike",
|
|
53813
53933
|
version: "1.0.0"
|
|
53814
53934
|
}, {
|
|
53815
|
-
instructions: "Blitz Strike is a universal penetration-testing toolbelt with three tiers — " + "BLITZ (reconnaissance / attack-surface mapping), EAGLE-EYE (source-to-sink " + "analysis), STRIKE (live validation) — plus a canonical evidence-first finding " + "engine and a multi-language taint engine.\\n\\n" + "CORE MENTAL MODEL:\\n" + "- A scanner hit is a HYPOTHESIS, not a finding. Live verification (or data-flow " + " proof) is the verdict. Never report an unverified hit as a vulnerability.\\n" + "- Distinguish REACHABLE from mere PRESENT: a sink is only a finding when tainted " + " input reaches it AND it is not neutralized by a sanitizer and not guarded by an " + " auth gate.\\n\\n" + "HOW TO RUN AN AUDIT (start here):\\n" + "0. ROUTE BY TARGET. Classify automatically - no need to ask. A URL - with or " + " without http/https - is LIVE (live_recon + the " + " web-hunting doctrine + strike_verify). A filesystem path (exists on disk, " + " or a code extension) is SOURCE audit (source-audit doctrine: blitz_scan + " + " eagle_eye2 + complex_scan/route_scan). TWO DOCTRINES - never mix: live " + " web/API -> web-hunting skill; source tree -> source-audit skill.\\n" + "1. RECORD SCOPE. scope_check classifies the target (intel for the report) - " + " it does NOT gate the engagement. No authorization gate: the repo disclaimer " + " places responsibility on the user. Hunt the target you are given.\\n" + "2. MAP THE SURFACE (BLITZ). For a source tree use blitz_scan (tree) or blitz_file " + " (single file) then enrich_scan to match sinks to escalation chains. For a live " + " URL use live_recon for a full pass (fingerprint/WAF/tech/version/crawler/params/" + " subdomains/API endpoints/intel correlation) — it returns next_steps. active_scan " + " is the lighter alternative. Both record scope internally.\\n" + "3. TRACE REACHABILITY (EAGLE-EYE). For every sink, determine whether attacker " + " input actually reaches it:\\n" + " - PHP/JS/TS/Python/Java source: taint_file (auto-detects language) or " + " taint_scan/taint_tree (PHP AST). list_languages shows what's supported.\\n" + " - Any language: trace_data_flow (window heuristic) and eagle_eye/eagle_grep " + " (function-scoped view of sinks + auth gates). Beyond taint: route_scan (route confusion / dispatch abuse / batch forwarding) and complex_scan (deserialization->POP, type juggling, mass assignment, prototype pollution, CRLF/header injection, path confusion) catch classes plain taint misses.\\n" + " A sink counts only if tainted input reaches it unsanitized and unguarded.\\n" + "4. VALIDATE (STRIKE). Confirm live with strike_verify using a MARKER plus a " + " NEGATIVE CONTROL plus a BASELINE. The verdict is deterministic: marker " + " reflected AND control inert = confirmed; both reflected = false_positive; " + " marker not reflected = unconfirmed. Feed the verdict to strike_resolve to " + " advance the finding lifecycle (hypothesis->validating->confirmed). Use " + " read_tool_manual for the exploit tool's manual, payload_lookup for payloads, " + " detect_waf/tech_correlation for context.\\n" + "5. RECORD + REPORT. Create a canonical finding with finding_create, advance it " + " through the lifecycle with finding_transition, attach confidence_score, and " + " redact any secrets before persisting. Only report findings that survived " + " verification.\\n\\n" + "SUPPORTING LAYERS:\\n" + "- run_engagement: ONE call does the whole audit (no babysitting). Source path -> " + " full static pipeline + inline report (markdown/JSON/SARIF). URL target -> " + " autonomous live
|
|
53935
|
+
instructions: "Blitz Strike is a universal penetration-testing toolbelt with three tiers — " + "BLITZ (reconnaissance / attack-surface mapping), EAGLE-EYE (source-to-sink " + "analysis), STRIKE (live validation) — plus a canonical evidence-first finding " + "engine and a multi-language taint engine.\\n\\n" + "CORE MENTAL MODEL:\\n" + "- A scanner hit is a HYPOTHESIS, not a finding. Live verification (or data-flow " + " proof) is the verdict. Never report an unverified hit as a vulnerability.\\n" + "- Distinguish REACHABLE from mere PRESENT: a sink is only a finding when tainted " + " input reaches it AND it is not neutralized by a sanitizer and not guarded by an " + " auth gate.\\n\\n" + "HOW TO RUN AN AUDIT (start here):\\n" + "0. ROUTE BY TARGET. Classify automatically - no need to ask. A URL - with or " + " without http/https - is LIVE (live_recon + the " + " web-hunting doctrine + strike_verify). A filesystem path (exists on disk, " + " or a code extension) is SOURCE audit (source-audit doctrine: blitz_scan + " + " eagle_eye2 + complex_scan/route_scan). TWO DOCTRINES - never mix: live " + " web/API -> web-hunting skill; source tree -> source-audit skill. For a WHOLE " + " engagement prefer run_engagement (one call, no babysitting) FIRST; steps 1-5 " + " below are the manual path for targeted follow-up on a specific finding.\\n" + "1. RECORD SCOPE. scope_check classifies the target (intel for the report) - " + " it does NOT gate the engagement. No authorization gate: the repo disclaimer " + " places responsibility on the user. Hunt the target you are given.\\n" + "2. MAP THE SURFACE (BLITZ). For a source tree use blitz_scan (tree) or blitz_file " + " (single file) then enrich_scan to match sinks to escalation chains. For a live " + " URL use live_recon for a full pass (fingerprint/WAF/tech/version/crawler/params/" + " subdomains/API endpoints/intel correlation) — it returns next_steps. active_scan " + " is the lighter alternative. Both record scope internally.\\n" + "3. TRACE REACHABILITY (EAGLE-EYE). For every sink, determine whether attacker " + " input actually reaches it:\\n" + " - PHP/JS/TS/Python/Java source: taint_file (auto-detects language) or " + " taint_scan/taint_tree (PHP AST). list_languages shows what's supported.\\n" + " - Any language: trace_data_flow (window heuristic) and eagle_eye/eagle_grep " + " (function-scoped view of sinks + auth gates). Beyond taint: route_scan (route confusion / dispatch abuse / batch forwarding) and complex_scan (deserialization->POP, type juggling, mass assignment, prototype pollution, CRLF/header injection, path confusion) catch classes plain taint misses.\\n" + " A sink counts only if tainted input reaches it unsanitized and unguarded.\\n" + "4. VALIDATE (STRIKE). Confirm live with strike_verify using a MARKER plus a " + " NEGATIVE CONTROL plus a BASELINE. The verdict is deterministic: marker " + " reflected AND control inert = confirmed; both reflected = false_positive; " + " marker not reflected = unconfirmed. Feed the verdict to strike_resolve to " + " advance the finding lifecycle (hypothesis->validating->confirmed). Use " + " read_tool_manual for the exploit tool's manual, payload_lookup for payloads, " + " detect_waf/tech_correlation for context.\\n" + "5. RECORD + REPORT. Create a canonical finding with finding_create, advance it " + " through the lifecycle with finding_transition, attach confidence_score, and " + " redact any secrets before persisting. Only report findings that survived " + " verification.\\n\\n" + "SUPPORTING LAYERS:\\n" + "- run_engagement: ONE call does the whole audit (no babysitting). Source path -> " + " full static pipeline + inline report (markdown/JSON/SARIF). URL target -> " + " full autonomous live pipeline (live_recon recon + deterministic checks: reflected " + " input, security headers, open redirect, path traversal, CORS) + findings + report. " + " PREFER THIS FIRST for a whole engagement; drive " + " the steps above manually only for targeted follow-up on a specific finding.\\n" + "- list_chains: 57 escalation chains (source -> sink -> impact).\\n" + "- list_languages / taint_file: multi-language taint (PHP, JS/TS, Python, Java).\\n" + "- tool_lookup / ensure_tool / list_tools: 130-tool catalog, auto-install.\\n" + "- read_playbook / list_playbooks: 17 engagement playbooks (web-app, api-security, " + " source-code-audit, active-directory, mobile, external-attack-surface, ...).\\n" + "- skill_lookup / read_skill / list_skills: 39 skills (7 bs-* agent doctrine: " + " orchestrate, scope, source-audit, web-hunting, verify, review, report; + " + " technique playbooks: WP/PHP 0-day, AD, malware, mobile, cloud).\\n" + "- read_tool_manual / list_manuals: 317 tool manuals (sqlmap, nmap, commix, ...).\\n" + "- payload_lookup / read_payload: 66 payload collections (sqli, xss, ssti, xxe, lfi).\\n" + "- detect_waf / tech_correlation / cve_correlation / port_correlation: signature " + " + correlation intelligence.\\n" + "- nvd_lookup / fofa_search: CVE lookup + asset reconnaissance (FOFA needs env creds).\\n" + "- remember / memory_lookup / memory_list: persist + recall verified knowledge.\\n\\n" + "RESOURCE MAP (know what to reach for):\\n" + "- ORCHESTRATION = the engagement framework. orchestration() returns the " + " lifecycle (scope->recon->analyze->deep->verify->report, each with " + " entry/exit criteria) + sub-agent team + handoff contract + termination " + " criteria. Load it at engagement start.\\n" + "- TOOL = acts (scan, taint, verify, record, correlate). Call it to DO something.\\n" + "- PLAYBOOK = campaign plan for a target TYPE. read_playbook(web-application) " + " BEFORE a full engagement of that type.\\n" + "- SKILL = procedure doctrine. Load the bs-* skill for the phase you are on " + " (orchestrate, scope, web-hunting for LIVE, source-audit for SOURCE, verify, " + " review, report); load a technique skill for the specific technique.\\n" + "- MANUAL = deep reference for ONE tool. read_tool_manual right before using it.\\n" + "- CATALOG = external tools (130: sqlmap, nmap, nuclei, ffuf, ysoserial, ...). " + " Blitz Strike's OWN tools find + verify; the catalog is for deeper EXTERNAL " + " exploitation/enumeration. After your own analysis confirms a finding, run " + " tool_lookup -> ensure_tool (auto-install) -> read_tool_manual to drive the " + " external tool that demonstrates impact.\\n" + "- PAYLOAD = attack payloads for a class. payload_lookup before crafting a probe.\\n" + "- INTELLIGENCE = knowledge. list_attack_vectors (34 categories) maps the " + " full surface during RECON; list_chains (57) turns a finding into impact; " + " chain_links composes chains for complex bugs; bypass_lookup when blocked; " + " payload_lookup when crafting a probe. technique_lookup(cls) returns the " + " DETAILED methodology + tricks for a vulnerability class (ssrf, ssti, " + " jwt, deserialization, ...) - load before testing a class. wstg_map maps " + " an OWASP category to coverage; taxonomy maps findings to standard IDs " + " (OWASP Top 10 / API Top 10 / CWE / ASVS); doctrine_map shows how everything links.\\n\\n" + "NO-BABYSITTING (while scanning + testing):\\n" + "- Scan the WHOLE surface before reporting - every file, entry point, and sink. " + " Do not stop after the first hit to ask; keep enumerating the full tree.\\n" + "- Test every hypothesis through its full verification cycle - vary probes on " + " failure, apply bypasses when blocked, keep going. Do not pause to ask.\\n" + "- Persist through partial results: a scan that surfaces few sinks is NOT done - " + " widen the surface, escalate detectors, dig into each reachable sink.\\n" + "- NEVER ask 'should I continue?' / 'proceed?' / approval-style questions. " + " Continue autonomously - the moment one step verifies, start the next. " + " Pause ONLY when: a genuinely destructive/DoS action needs consent, an " + " external dependency beyond your control blocks you (e.g. phone OTP), or " + " the engagement is COMPLETE (deliver the final report). Otherwise keep hunting.\\n\\n" + "PERSISTENCE (complex bugs - do not give up):\\n" + "- There is NO retry cap. A failed probe/sub-agent is diagnosed and resumed " + " until verified or definitively ruled out - never move on unverified.\\n" + "- On any failure (empty output, blocked probe, failed sub-agent/model), call " + " retry_guidance(signal) for concrete recovery actions before giving up.\\n" + "- On a model/sub-agent error (rate-limit, quota, timeout, 5xx, auth, " + " unavailable), model_fallback(signal) classifies it and returns the exact " + " recovery (retry+backoff / fall back / shrink scope / fix credentials / " + " abort). Do not fall back blindly - the class decides.\\n" + "- One failed probe != not vulnerable. Vary: alternative sinks, other params, " + " encoding/case/whitespace bypasses, different injection context.\\n" + "- Empty output != no findings. Re-run (server may be stale after an upgrade - " + " /reload-mcp), or escalate to route_scan / complex_scan for classes taint " + " misses (POP chains, type juggling, mass assignment, route confusion).\\n" + "- A 'sanitized' sink may not be safe. Challenge the sanitizer: loose == vs ===, " + " missing EXTR_SKIP, int cast truncation, weak regex.\\n" + "- An unconfirmed hypothesis is KEPT as hypothesis - try alternate verification, " + " then let adversarial review decide.\\n" + "- Complex chains (deserialization->POP, LFI->RCE) need the FULL chain traced, " + ` not just the first sink.\\n
|
|
53816
53936
|
` + "- When a probe is BLOCKED by a WAF/filter/sanitizer, call bypass_lookup(defense) " + " for concrete techniques (encoding, case, comment insertion, chunked, null byte, " + " double-encoding, verb tampering, header spoofing) and apply them - do not give up.\\n" + "- CHAIN findings: a low-severity finding may escalate — do not assume it is the " + " end of the story. list_chains / chain_links show whether it composes into a " + " multi-stage chain (e.g. SSRF->metadata, SQLi->RCE, LFI->RCE, upload->shell). " + " Check before concluding, and do not overclaim an escalation that is not there.\\n\\n" + "- Complex bugs chain ACROSS chains: after confirming one chain, chain_links " + " shows what follows (SSRF->metadata then default-creds->pivot via stolen IAM). " + " Trace the full composition, not just a single chain.\\n\\n" + "HYPOTHESIS EXPANSION (from each finding, enumerate + verify every follow-up):\\n" + "- A confirmed finding is the START, not the end. From each one, enumerate " + " the follow-ups: ESCALATE (list_chains + chain_links), CHAIN (chain_links " + " from->to), BYPASS (bypass_lookup), PIVOT (new endpoints/sinks now reachable).\\n" + "- Every follow-up (this might happen, this could chain, maybe bypassable) is " + " a HYPOTHESIS, not a finding. Verify EACH via strike_verify (marker + negative " + " control) before it enters the report.\\n" + "- Never report speculation: might / could / possibly live in the " + " hypothesis list, never in the report. Every report line is a verified fact.\\n\\n" + "PROFESSIONAL JUDGMENT (act like a senior tester, not a checklist runner):\\n" + "- PRIORITIZE by severity, computed deterministically with cvss_score (impact x " + " exploitability) and weighted by escalation potential (chain_links). Do NOT rank " + " by bug class - a critical stored XSS that yields account takeover outranks an " + " unreachable blind RCE. Weight by the engagement context (which asset/data is " + " most sensitive for THIS target).\\n" + "- Recognize dead ends: if a branch yields nothing after a few varied probes, " + " note it and move on. Do not tunnel-vision one sink at the expense of the rest.\\n" + "- Live-test with care: respect rate limits, do not lock accounts, do not " + " trigger DoS. Test non-destructively and prove impact with the smallest payload.\\n" + "- Know when done: stop when the termination criteria are met, not when you feel " + " tired. A thorough audit over a rushed one, every time.\\n\\n" + "IRON RULES:\\n" + "- No authorization gate - the repo disclaimer places responsibility on the " + " user. Hunt the target you are given (like nmap/sqlmap/Burp, which do not " + " ask). The one hard stop: malicious/third-party infra (cari pelaku) - hold/static.\\n" + " need finer control."
|
|
53817
53937
|
});
|
|
53818
53938
|
server.registerTool("blitz_scan", {
|
|
@@ -54000,7 +54120,7 @@ function createServer() {
|
|
|
54000
54120
|
});
|
|
54001
54121
|
server.registerTool("run_engagement", {
|
|
54002
54122
|
title: "STRIKE full engagement",
|
|
54003
|
-
description: "STRIKE: run a full audit in ONE call (no babysitting). Source path -> triage -> chain enrichment -> findings -> inline report (markdown+JSON+SARIF). URL target -> autonomous live
|
|
54123
|
+
description: "STRIKE: run a full audit in ONE call (no babysitting). Source path -> triage -> chain enrichment -> findings -> inline report (markdown+JSON+SARIF). URL target -> full autonomous live pipeline: live_recon (fingerprint/WAF/tech/crawl/subdomains/API) + deterministic security checks (reflected input via marker/negative-control, missing security headers, open redirect, path traversal, CORS) -> findings -> inline report. Prefer this over driving tools one-by-one when you want the whole engagement in a single deterministic call.",
|
|
54004
54124
|
inputSchema: {
|
|
54005
54125
|
target: string2().describe("Source path or URL"),
|
|
54006
54126
|
scope: string2().optional().describe("Scope (for URL targets)"),
|