blitzstrike 1.1.7 → 1.2.1
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 +22 -0
- package/dist/index.js +1687 -1671
- 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,1522 @@ 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
|
-
|
|
31327
|
-
|
|
31328
|
-
|
|
31329
|
-
const val = (headers[h] ?? headers[s.header ?? ""] ?? "").toLowerCase();
|
|
31330
|
-
if (val.includes(pattern)) {
|
|
31331
|
-
matches.push({ waf: s.waf, header: s.header, pattern: s.pattern, confidence: s.confidence, via: "header-value" });
|
|
31332
|
-
}
|
|
31333
|
-
} else {
|
|
31334
|
-
if (headerKeys.includes(h)) {
|
|
31335
|
-
matches.push({ waf: s.waf, header: s.header, confidence: s.confidence, via: "header-name" });
|
|
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
|
+
}
|
|
31336
31462
|
}
|
|
31337
31463
|
}
|
|
31338
|
-
}
|
|
31339
|
-
|
|
31340
|
-
|
|
31341
|
-
|
|
31342
|
-
|
|
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
|
+
}
|
|
31343
31483
|
}
|
|
31344
|
-
}
|
|
31345
|
-
|
|
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
|
-
};
|
|
31484
|
+
} catch {}
|
|
31485
|
+
return { urls: [...out].slice(0, 500), source: "web.archive.org (Wayback CDX)" };
|
|
31358
31486
|
}
|
|
31359
|
-
|
|
31360
|
-
|
|
31361
|
-
|
|
31362
|
-
|
|
31363
|
-
|
|
31364
|
-
|
|
31365
|
-
|
|
31366
|
-
|
|
31367
|
-
|
|
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))
|
|
31547
|
+
continue;
|
|
31548
|
+
seen.add(host);
|
|
31549
|
+
try {
|
|
31550
|
+
const ips = await resolve4(host);
|
|
31551
|
+
if (ips.length > 0)
|
|
31552
|
+
found.push({ host, ips });
|
|
31553
|
+
} catch {}
|
|
31368
31554
|
}
|
|
31369
|
-
return { found
|
|
31555
|
+
return { found, source: "DNS resolution (common subdomain prefixes)" };
|
|
31370
31556
|
}
|
|
31371
|
-
|
|
31372
|
-
|
|
31373
|
-
|
|
31374
|
-
|
|
31375
|
-
|
|
31376
|
-
|
|
31377
|
-
|
|
31378
|
-
|
|
31379
|
-
|
|
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
|
+
});
|
|
31380
31601
|
}
|
|
31381
|
-
function
|
|
31382
|
-
const
|
|
31383
|
-
|
|
31384
|
-
|
|
31385
|
-
|
|
31386
|
-
|
|
31387
|
-
|
|
31388
|
-
|
|
31389
|
-
|
|
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;
|
|
31390
31612
|
}
|
|
31391
|
-
|
|
31392
|
-
|
|
31393
|
-
|
|
31394
|
-
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
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
|
+
}
|
|
31398
31636
|
}
|
|
31399
|
-
return
|
|
31637
|
+
return out;
|
|
31400
31638
|
}
|
|
31401
|
-
function
|
|
31402
|
-
const
|
|
31403
|
-
const
|
|
31404
|
-
const
|
|
31405
|
-
|
|
31406
|
-
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31413
|
-
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
|
|
31417
|
-
|
|
31418
|
-
|
|
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;
|
|
31419
31663
|
}
|
|
31420
|
-
function
|
|
31421
|
-
const
|
|
31422
|
-
|
|
31423
|
-
|
|
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.");
|
|
31424
31765
|
return {
|
|
31425
|
-
|
|
31426
|
-
|
|
31427
|
-
|
|
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
|
|
31428
31781
|
};
|
|
31429
31782
|
}
|
|
31430
|
-
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
31434
|
-
|
|
31435
|
-
|
|
31436
|
-
|
|
31437
|
-
|
|
31438
|
-
|
|
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(" ")}`);
|
|
31439
31801
|
}
|
|
31440
|
-
|
|
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
|
+
`);
|
|
31441
31811
|
}
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
31445
|
-
|
|
31446
|
-
|
|
31447
|
-
|
|
31448
|
-
|
|
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
|
+
`);
|
|
31449
31821
|
}
|
|
31450
|
-
function
|
|
31451
|
-
if (
|
|
31452
|
-
return
|
|
31453
|
-
|
|
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 };
|
|
31454
31836
|
}
|
|
31455
|
-
var
|
|
31456
|
-
|
|
31457
|
-
|
|
31837
|
+
var _chainsCache = null;
|
|
31838
|
+
function loadChains() {
|
|
31839
|
+
if (_chainsCache)
|
|
31840
|
+
return _chainsCache;
|
|
31458
31841
|
try {
|
|
31459
|
-
|
|
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;
|
|
31460
31847
|
} catch {
|
|
31848
|
+
_chainsCache = [];
|
|
31461
31849
|
return [];
|
|
31462
31850
|
}
|
|
31463
31851
|
}
|
|
31464
|
-
function
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
const ALIASES = {
|
|
31468
|
-
sqli: "sql injection",
|
|
31469
|
-
xss: "xss injection",
|
|
31470
|
-
ssrf: "server side request forgery",
|
|
31471
|
-
ssti: "server side template injection",
|
|
31472
|
-
jwt: "json web token",
|
|
31473
|
-
lfi: "file inclusion",
|
|
31474
|
-
rfi: "file inclusion",
|
|
31475
|
-
xxe: "xxe injection",
|
|
31476
|
-
idor: "insecure direct object references",
|
|
31477
|
-
csrf: "cross-site request forgery",
|
|
31478
|
-
cmdi: "command injection",
|
|
31479
|
-
rce: "command injection",
|
|
31480
|
-
cors: "cors misconfiguration",
|
|
31481
|
-
crlf: "crlf injection",
|
|
31482
|
-
prototype: "prototype pollution",
|
|
31483
|
-
nosql: "nosql injection",
|
|
31484
|
-
upload: "upload insecure files"
|
|
31485
|
-
};
|
|
31486
|
-
const normalized = ALIASES[key] ?? key;
|
|
31487
|
-
const exact = cats.find((c) => c.toLowerCase() === normalized);
|
|
31488
|
-
if (exact) {
|
|
31489
|
-
return { found: true, category: exact, files: listFiles(join8(PAYLOADS, exact)) };
|
|
31490
|
-
}
|
|
31491
|
-
const keyTokens = normalized.split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
31492
|
-
const matches = cats.filter((c) => {
|
|
31493
|
-
const cl = c.toLowerCase();
|
|
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) };
|
|
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" };
|
|
31500
31855
|
}
|
|
31501
|
-
|
|
31502
|
-
|
|
31503
|
-
|
|
31504
|
-
|
|
31505
|
-
|
|
31506
|
-
|
|
31507
|
-
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
|
|
31511
|
-
|
|
31512
|
-
|
|
31513
|
-
|
|
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;
|
|
31883
|
+
}
|
|
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
|
+
}
|
|
31895
|
+
}
|
|
31514
31896
|
}
|
|
31515
|
-
return { found: true, category: exact, files };
|
|
31516
31897
|
}
|
|
31517
|
-
|
|
31518
|
-
|
|
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) };
|
|
31898
|
+
recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
|
|
31899
|
+
return result;
|
|
31521
31900
|
}
|
|
31522
|
-
function
|
|
31523
|
-
|
|
31524
|
-
return
|
|
31525
|
-
|
|
31526
|
-
return
|
|
31527
|
-
|
|
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 "";
|
|
31528
31919
|
}
|
|
31529
|
-
function
|
|
31530
|
-
|
|
31531
|
-
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
31536
|
-
|
|
31537
|
-
|
|
31538
|
-
|
|
31539
|
-
|
|
31540
|
-
|
|
31541
|
-
|
|
31542
|
-
|
|
31543
|
-
|
|
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 });
|
|
31942
|
+
}
|
|
31544
31943
|
}
|
|
31944
|
+
return {
|
|
31945
|
+
root: expanded,
|
|
31946
|
+
files_scanned: files.length,
|
|
31947
|
+
hits: hits.slice(0, 2000),
|
|
31948
|
+
sink_signals: [...sinkKinds].sort(),
|
|
31949
|
+
matched_chains: matched
|
|
31950
|
+
};
|
|
31545
31951
|
}
|
|
31546
|
-
function
|
|
31547
|
-
const
|
|
31548
|
-
|
|
31549
|
-
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
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 }
|
|
31961
|
+
};
|
|
31962
|
+
const isPath = (() => {
|
|
31553
31963
|
try {
|
|
31554
|
-
|
|
31964
|
+
return existsSync5(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
|
|
31555
31965
|
} catch {
|
|
31556
|
-
return;
|
|
31966
|
+
return false;
|
|
31557
31967
|
}
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31562
|
-
|
|
31563
|
-
|
|
31564
|
-
|
|
31565
|
-
|
|
31566
|
-
|
|
31567
|
-
|
|
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
|
-
}
|
|
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;
|
|
31575
31978
|
}
|
|
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
31979
|
}
|
|
31615
|
-
const
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
|
|
31619
|
-
|
|
31620
|
-
|
|
31621
|
-
|
|
31622
|
-
|
|
31623
|
-
|
|
31624
|
-
|
|
31625
|
-
|
|
31626
|
-
|
|
31627
|
-
|
|
31628
|
-
|
|
31629
|
-
|
|
31630
|
-
|
|
31631
|
-
|
|
31632
|
-
|
|
31633
|
-
|
|
31634
|
-
|
|
31635
|
-
|
|
31636
|
-
|
|
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
|
+
});
|
|
31637
32015
|
}
|
|
31638
|
-
|
|
31639
|
-
|
|
31640
|
-
|
|
31641
|
-
|
|
31642
|
-
|
|
31643
|
-
|
|
31644
|
-
|
|
31645
|
-
|
|
31646
|
-
|
|
31647
|
-
|
|
31648
|
-
|
|
31649
|
-
|
|
31650
|
-
|
|
31651
|
-
|
|
31652
|
-
|
|
31653
|
-
|
|
31654
|
-
|
|
31655
|
-
|
|
31656
|
-
|
|
31657
|
-
|
|
31658
|
-
|
|
31659
|
-
|
|
31660
|
-
|
|
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
|
|
31661
32066
|
};
|
|
31662
|
-
|
|
31663
|
-
|
|
31664
|
-
const
|
|
31665
|
-
|
|
31666
|
-
|
|
31667
|
-
|
|
31668
|
-
|
|
31669
|
-
|
|
31670
|
-
|
|
31671
|
-
|
|
31672
|
-
|
|
31673
|
-
|
|
31674
|
-
|
|
31675
|
-
|
|
31676
|
-
|
|
31677
|
-
|
|
31678
|
-
|
|
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)
|
|
32079
|
+
};
|
|
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
|
+
}
|
|
32088
|
+
}
|
|
32089
|
+
report.memory = { captured, deduped: blitz.matched_chains.length - captured };
|
|
31679
32090
|
}
|
|
31680
|
-
|
|
31681
|
-
|
|
31682
|
-
|
|
31683
|
-
|
|
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 };
|
|
32091
|
+
return report;
|
|
32092
|
+
}
|
|
32093
|
+
function listChains() {
|
|
32094
|
+
const chains = loadChains();
|
|
31695
32095
|
return {
|
|
31696
|
-
|
|
31697
|
-
|
|
31698
|
-
|
|
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
|
+
};
|
|
32105
|
+
}
|
|
32106
|
+
var LIVE_SWEEP_PARAMS = ["q", "search", "query", "s", "id", "url", "redirect", "name", "page"];
|
|
32107
|
+
async function runLiveEngagement(url, scope = "", mode = "bug-bounty") {
|
|
32108
|
+
const report = { banner: engagementBanner(url, mode), target: url, mode, findings: [], scope: null, status: "" };
|
|
32109
|
+
const sc = scopeCheck(url, scope, mode);
|
|
32110
|
+
report.scope = sc;
|
|
32111
|
+
const recon = await liveRecon(url);
|
|
32112
|
+
report.recon = {
|
|
32113
|
+
fingerprint: recon.fingerprint,
|
|
32114
|
+
waf: recon.waf,
|
|
32115
|
+
tech: recon.tech,
|
|
32116
|
+
versions: recon.versions,
|
|
32117
|
+
crawl: recon.crawl,
|
|
32118
|
+
params: recon.params,
|
|
32119
|
+
subdomains: recon.subdomains,
|
|
32120
|
+
dns_findings: recon.dns_findings,
|
|
32121
|
+
wayback_urls: recon.wayback_urls,
|
|
32122
|
+
api_endpoints: recon.api_endpoints,
|
|
32123
|
+
open_ports: recon.open_ports,
|
|
32124
|
+
intel: recon.intel,
|
|
32125
|
+
next_steps: recon.next_steps
|
|
32126
|
+
};
|
|
32127
|
+
const findings = [];
|
|
32128
|
+
for (const param of LIVE_SWEEP_PARAMS) {
|
|
32129
|
+
const verdict = await strikeVerify({ url, param });
|
|
32130
|
+
const finding = makeFinding({
|
|
32131
|
+
title: `Reflected input in parameter '${param}'`,
|
|
32132
|
+
target: { type: "web", host: url, endpoint: `?${param}=` },
|
|
32133
|
+
severity: "medium",
|
|
32134
|
+
cwe: "CWE-79",
|
|
32135
|
+
cweName: "Cross-site Scripting",
|
|
32136
|
+
source: { type: "request_parameter", name: param },
|
|
32137
|
+
sink: { type: "reflected_input", symbol: param },
|
|
32138
|
+
chainId: "reflected_input",
|
|
32139
|
+
chainName: "Reflected input",
|
|
32140
|
+
status: "hypothesis"
|
|
32141
|
+
});
|
|
32142
|
+
findings.push(resolveFinding(finding, verdict));
|
|
32143
|
+
}
|
|
32144
|
+
report.findings = findings.map((f) => ({
|
|
32145
|
+
id: f.id,
|
|
32146
|
+
status: f.status,
|
|
32147
|
+
title: f.title,
|
|
32148
|
+
severity: f.classification.severity,
|
|
32149
|
+
confidence: f.confidence,
|
|
32150
|
+
confidence_level: f.confidence_level,
|
|
32151
|
+
chain_id: f.chain.id,
|
|
32152
|
+
validation: f.validation
|
|
32153
|
+
}));
|
|
32154
|
+
const title = `Blitz Strike — Live Engagement (${url})`;
|
|
32155
|
+
const markdown = reportMarkdown(findings, { title, scope: url, version: VERSION });
|
|
32156
|
+
const json = reportJson(findings, { title, scope: url, version: VERSION });
|
|
32157
|
+
report.report = {
|
|
32158
|
+
summary: JSON.parse(json).summary ?? null,
|
|
32159
|
+
markdown,
|
|
32160
|
+
json,
|
|
32161
|
+
sarif: toSarif(findings.map((f) => ({
|
|
32162
|
+
ruleId: f.chain.id || f.classification.cwe || f.classification.severity,
|
|
32163
|
+
level: sarifLevel(f.classification.severity),
|
|
32164
|
+
message: f.title,
|
|
32165
|
+
file: url,
|
|
32166
|
+
line: 1
|
|
32167
|
+
})))
|
|
31699
32168
|
};
|
|
32169
|
+
report.status = "COMPLETE";
|
|
32170
|
+
report.note = "Autonomous live engagement — full recon + marker/negative-control sweep, all in one " + "deterministic call (no LLM babysitting). Each reflected parameter was validated with a " + "marker + negative control; confirmed reflections are real findings, everything else is a " + "hypothesis or false positive. Deeper per-endpoint validation: run strike_verify explicitly.";
|
|
32171
|
+
return report;
|
|
31700
32172
|
}
|
|
31701
|
-
|
|
31702
|
-
|
|
31703
|
-
|
|
31704
|
-
|
|
31705
|
-
|
|
31706
|
-
|
|
31707
|
-
|
|
31708
|
-
|
|
31709
|
-
|
|
31710
|
-
|
|
31711
|
-
|
|
32173
|
+
|
|
32174
|
+
// src/catalog.ts
|
|
32175
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync3 } from "node:fs";
|
|
32176
|
+
import { join as join8 } from "node:path";
|
|
32177
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
32178
|
+
import { spawnSync } from "node:child_process";
|
|
32179
|
+
var ROOT3 = join8(fileURLToPath5(new URL(".", import.meta.url)), "..");
|
|
32180
|
+
var _toolsCache = null;
|
|
32181
|
+
var _skillsCache = null;
|
|
32182
|
+
function loadTools() {
|
|
32183
|
+
if (_toolsCache)
|
|
32184
|
+
return _toolsCache;
|
|
32185
|
+
try {
|
|
32186
|
+
const raw = JSON.parse(readFileSync8(join8(ROOT3, "tools-catalog.json"), "utf8"));
|
|
32187
|
+
_toolsCache = raw.tools ?? [];
|
|
32188
|
+
} catch {
|
|
32189
|
+
_toolsCache = [];
|
|
31712
32190
|
}
|
|
31713
|
-
|
|
31714
|
-
|
|
31715
|
-
|
|
31716
|
-
|
|
31717
|
-
|
|
31718
|
-
|
|
31719
|
-
|
|
31720
|
-
|
|
31721
|
-
|
|
31722
|
-
|
|
31723
|
-
|
|
31724
|
-
|
|
32191
|
+
return _toolsCache;
|
|
32192
|
+
}
|
|
32193
|
+
function loadSkills() {
|
|
32194
|
+
if (_skillsCache)
|
|
32195
|
+
return _skillsCache;
|
|
32196
|
+
const dir = join8(ROOT3, "skills");
|
|
32197
|
+
const out = [];
|
|
32198
|
+
try {
|
|
32199
|
+
for (const f of readdirSync3(dir)) {
|
|
32200
|
+
if (!f.endsWith(".md"))
|
|
32201
|
+
continue;
|
|
32202
|
+
const p = join8(dir, f);
|
|
32203
|
+
const text = readFileSync8(p, "utf8");
|
|
32204
|
+
const descMatch = text.match(/^description:\s*(.+)$/m);
|
|
32205
|
+
out.push({
|
|
32206
|
+
name: f.replace(/\.md$/, ""),
|
|
32207
|
+
file: f,
|
|
32208
|
+
description: (descMatch?.[1] ?? "").trim().slice(0, 200),
|
|
32209
|
+
size: text.length
|
|
32210
|
+
});
|
|
31725
32211
|
}
|
|
31726
|
-
}
|
|
31727
|
-
|
|
32212
|
+
} catch {}
|
|
32213
|
+
_skillsCache = out;
|
|
32214
|
+
return out;
|
|
31728
32215
|
}
|
|
31729
|
-
function
|
|
31730
|
-
const
|
|
31731
|
-
|
|
31732
|
-
|
|
31733
|
-
if (!part) {
|
|
32216
|
+
function toolLookup(name) {
|
|
32217
|
+
const tools = loadTools();
|
|
32218
|
+
const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
32219
|
+
if (!t) {
|
|
31734
32220
|
return {
|
|
31735
|
-
found:
|
|
31736
|
-
|
|
31737
|
-
|
|
32221
|
+
found: false,
|
|
32222
|
+
query: name,
|
|
32223
|
+
suggestions: tools.filter((x) => x.name.includes(name.toLowerCase()) || name.toLowerCase().includes(x.name)).slice(0, 5).map((x) => x.name)
|
|
31738
32224
|
};
|
|
31739
32225
|
}
|
|
31740
|
-
const
|
|
31741
|
-
const
|
|
31742
|
-
if (
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
|
|
31746
|
-
|
|
31747
|
-
|
|
31748
|
-
found: false,
|
|
31749
|
-
part,
|
|
31750
|
-
suggestions: ["cross_cutting", ...data.phases.map((p) => String(p.phase))]
|
|
31751
|
-
};
|
|
31752
|
-
}
|
|
31753
|
-
function wstgMap(category) {
|
|
31754
|
-
const data = loadJson("wstg_map");
|
|
31755
|
-
if (!data)
|
|
31756
|
-
return { found: false, error: "wstg_map.json not found" };
|
|
31757
|
-
if (!category) {
|
|
31758
|
-
return {
|
|
31759
|
-
found: true,
|
|
31760
|
-
categories: data.categories.map((c) => ({ wstg: c.wstg, name: c.name, phase: c.phase, skill: c.skill }))
|
|
32226
|
+
const manual = manualForTool(t.name);
|
|
32227
|
+
const result = { found: true, tool: t };
|
|
32228
|
+
if (manual && manual.content) {
|
|
32229
|
+
result.manual = {
|
|
32230
|
+
name: manual.name,
|
|
32231
|
+
category: manual.category,
|
|
32232
|
+
preview: manual.content.slice(0, 1500),
|
|
32233
|
+
full_length: manual.content.length
|
|
31761
32234
|
};
|
|
32235
|
+
} else {
|
|
32236
|
+
result.manual = null;
|
|
31762
32237
|
}
|
|
31763
|
-
|
|
31764
|
-
const c = data.categories.find((x) => String(x.wstg).toUpperCase() === q || String(x.name).toLowerCase().includes(category.toLowerCase()) || String(x.wstg).toUpperCase().includes(q));
|
|
31765
|
-
if (c)
|
|
31766
|
-
return { found: true, ...c };
|
|
31767
|
-
return { found: false, category, suggestions: data.categories.map((x) => String(x.wstg)) };
|
|
32238
|
+
return result;
|
|
31768
32239
|
}
|
|
31769
|
-
function
|
|
31770
|
-
const
|
|
31771
|
-
|
|
31772
|
-
|
|
31773
|
-
|
|
31774
|
-
return {
|
|
31775
|
-
found: true,
|
|
31776
|
-
classes: data.classes.map((c) => ({ id: c.id, name: c.name }))
|
|
31777
|
-
};
|
|
32240
|
+
function listTools() {
|
|
32241
|
+
const tools = loadTools();
|
|
32242
|
+
const byCat = {};
|
|
32243
|
+
for (const t of tools) {
|
|
32244
|
+
(byCat[t.category] ??= []).push(t.name);
|
|
31778
32245
|
}
|
|
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)) };
|
|
32246
|
+
return { total: tools.length, categories: byCat, tools: tools.map((t) => ({ name: t.name, category: t.category, description: t.description })) };
|
|
31784
32247
|
}
|
|
31785
|
-
function
|
|
31786
|
-
const
|
|
31787
|
-
|
|
31788
|
-
|
|
31789
|
-
|
|
31790
|
-
|
|
32248
|
+
function skillLookup(topic) {
|
|
32249
|
+
const skills = loadSkills();
|
|
32250
|
+
const q = topic.toLowerCase();
|
|
32251
|
+
const scored = skills.map((s) => {
|
|
32252
|
+
const hay = `${s.name} ${s.description}`.toLowerCase();
|
|
32253
|
+
const nameHit = s.name.includes(q);
|
|
32254
|
+
const descHit = s.description.includes(q);
|
|
32255
|
+
const wordHit = q.split(/\s+/).filter((w) => w && hay.includes(w)).length;
|
|
32256
|
+
return { s, score: (nameHit ? 10 : 0) + (descHit ? 5 : 0) + wordHit * 2 };
|
|
32257
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
|
|
32258
|
+
if (scored.length === 0) {
|
|
32259
|
+
return { found: false, query: topic, suggestions: skills.map((s) => s.name).slice(0, 10) };
|
|
31791
32260
|
}
|
|
31792
|
-
|
|
31793
|
-
|
|
31794
|
-
|
|
31795
|
-
|
|
31796
|
-
return { found: false, framework, suggestions: data.frameworks.map((x) => String(x.framework)) };
|
|
32261
|
+
return {
|
|
32262
|
+
found: true,
|
|
32263
|
+
matches: scored.slice(0, 5).map((x) => ({ name: x.s.name, description: x.s.description, file: x.s.file }))
|
|
32264
|
+
};
|
|
31797
32265
|
}
|
|
31798
|
-
function
|
|
31799
|
-
const
|
|
31800
|
-
|
|
31801
|
-
return { found: false, error: "resources.json not found" };
|
|
31802
|
-
if (!category) {
|
|
31803
|
-
return {
|
|
31804
|
-
found: true,
|
|
31805
|
-
resources: data.resources.map((r) => ({ id: r.id, name: r.name, category: r.category, use_when: r.use_when }))
|
|
31806
|
-
};
|
|
31807
|
-
}
|
|
31808
|
-
const q = category.toLowerCase();
|
|
31809
|
-
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));
|
|
31810
|
-
if (matches.length > 0)
|
|
31811
|
-
return { found: true, matches };
|
|
31812
|
-
return { found: false, category, suggestions: [...new Set(data.resources.map((r) => String(r.category)))] };
|
|
32266
|
+
function listSkills() {
|
|
32267
|
+
const skills = loadSkills();
|
|
32268
|
+
return { total: skills.length, skills: skills.map((s) => ({ name: s.name, description: s.description })) };
|
|
31813
32269
|
}
|
|
31814
|
-
function
|
|
31815
|
-
|
|
31816
|
-
|
|
31817
|
-
|
|
31818
|
-
|
|
31819
|
-
|
|
31820
|
-
|
|
31821
|
-
|
|
31822
|
-
|
|
31823
|
-
|
|
31824
|
-
return { found: true, ...r };
|
|
31825
|
-
return { found: false, query, suggestions: data.risks.map((x) => String(x.id)) };
|
|
31826
|
-
}
|
|
31827
|
-
if (kind === "cwe" || kind === "weakness") {
|
|
31828
|
-
const data = loadJson("cwe_map");
|
|
31829
|
-
if (!data)
|
|
31830
|
-
return { found: false, error: "cwe_map.json not found" };
|
|
31831
|
-
if (!query)
|
|
31832
|
-
return { found: true, mapping: data.mapping };
|
|
31833
|
-
const q = query.toLowerCase();
|
|
31834
|
-
const m = data.mapping.filter((x) => String(x.id).toLowerCase().includes(q) || String(x.cwe).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31835
|
-
if (m.length > 0)
|
|
31836
|
-
return { found: true, matches: m };
|
|
31837
|
-
return { found: false, query, suggestions: data.mapping.map((x) => String(x.id)) };
|
|
31838
|
-
}
|
|
31839
|
-
if (kind === "web" || kind === "owasp" || kind === "top10" || kind === "web_top10") {
|
|
31840
|
-
const data = loadJson("owasp_top10");
|
|
31841
|
-
if (!data)
|
|
31842
|
-
return { found: false, error: "owasp_top10.json not found" };
|
|
31843
|
-
if (!query)
|
|
31844
|
-
return { found: true, risks: data.risks };
|
|
31845
|
-
const q = query.toLowerCase();
|
|
31846
|
-
const r = data.risks.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
|
|
31847
|
-
if (r)
|
|
31848
|
-
return { found: true, ...r };
|
|
31849
|
-
return { found: false, query, suggestions: data.risks.map((x) => String(x.id)) };
|
|
32270
|
+
function readSkill(name) {
|
|
32271
|
+
const skills = loadSkills();
|
|
32272
|
+
const s = skills.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
32273
|
+
if (!s)
|
|
32274
|
+
return { found: false, query: name };
|
|
32275
|
+
try {
|
|
32276
|
+
const text = readFileSync8(join8(ROOT3, "skills", s.file), "utf8");
|
|
32277
|
+
return { found: true, name: s.name, content: text.slice(0, 12000) };
|
|
32278
|
+
} catch {
|
|
32279
|
+
return { found: false, query: name, error: "unreadable" };
|
|
31850
32280
|
}
|
|
31851
|
-
|
|
31852
|
-
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
31856
|
-
|
|
31857
|
-
|
|
31858
|
-
|
|
31859
|
-
|
|
31860
|
-
|
|
31861
|
-
|
|
32281
|
+
}
|
|
32282
|
+
function ensureTool(name) {
|
|
32283
|
+
const tools = loadTools();
|
|
32284
|
+
const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
32285
|
+
if (!t) {
|
|
32286
|
+
return { found: false, query: name, suggestions: tools.slice(0, 5).map((x) => x.name) };
|
|
32287
|
+
}
|
|
32288
|
+
const check = spawnSync(t.check_installed.command, { shell: true, timeout: 15000 });
|
|
32289
|
+
if (check.status !== 127 && check.error === undefined) {
|
|
32290
|
+
return { name: t.name, installed: true, action: "none", note: "already installed" };
|
|
32291
|
+
}
|
|
32292
|
+
const platform = process.platform;
|
|
32293
|
+
const cmd = t.install[platform] ?? t.install.linux;
|
|
32294
|
+
if (!cmd) {
|
|
32295
|
+
return { name: t.name, installed: false, action: "none", note: `no install command for ${platform}` };
|
|
31862
32296
|
}
|
|
32297
|
+
const r = spawnSync(cmd, { shell: true, timeout: 120000 });
|
|
32298
|
+
const ok = r.status === 0;
|
|
31863
32299
|
return {
|
|
31864
|
-
|
|
31865
|
-
|
|
31866
|
-
|
|
32300
|
+
name: t.name,
|
|
32301
|
+
installed: ok,
|
|
32302
|
+
action: "install",
|
|
32303
|
+
command: cmd,
|
|
32304
|
+
output: (r.stdout?.toString() ?? "").slice(-400) || (r.stderr?.toString() ?? "").slice(-400)
|
|
31867
32305
|
};
|
|
31868
32306
|
}
|
|
31869
32307
|
|
|
@@ -31886,7 +32324,7 @@ async function httpFetch(url, init) {
|
|
|
31886
32324
|
clearTimeout(t);
|
|
31887
32325
|
}
|
|
31888
32326
|
}
|
|
31889
|
-
function
|
|
32327
|
+
function normalizeTarget2(target) {
|
|
31890
32328
|
let t = target.trim();
|
|
31891
32329
|
if (!/^https?:\/\//i.test(t))
|
|
31892
32330
|
t = `https://${t}`;
|
|
@@ -31954,7 +32392,7 @@ async function probeCommonPaths(base) {
|
|
|
31954
32392
|
return out;
|
|
31955
32393
|
}
|
|
31956
32394
|
async function activeScan(target, scope = "", mode = "bug-bounty", allowed = undefined) {
|
|
31957
|
-
const base =
|
|
32395
|
+
const base = normalizeTarget2(target);
|
|
31958
32396
|
const host = (() => {
|
|
31959
32397
|
try {
|
|
31960
32398
|
return new URL(base).hostname;
|
|
@@ -52706,767 +53144,345 @@ function detectDynamicDispatch(code, file, out) {
|
|
|
52706
53144
|
});
|
|
52707
53145
|
}
|
|
52708
53146
|
}
|
|
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;
|
|
53147
|
+
function detectDynamicMethod(code, file, out) {
|
|
53148
|
+
const ls = lines(code);
|
|
53149
|
+
DYNAMIC_METHOD_RE.lastIndex = 0;
|
|
52928
53150
|
let m;
|
|
52929
|
-
while ((m =
|
|
52930
|
-
const ln =
|
|
53151
|
+
while ((m = DYNAMIC_METHOD_RE.exec(code)) !== null) {
|
|
53152
|
+
const ln = lineNo(code, m.index);
|
|
52931
53153
|
const l = ls[ln - 1] ?? "";
|
|
52932
53154
|
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52933
53155
|
`);
|
|
52934
|
-
if (!
|
|
53156
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
52935
53157
|
continue;
|
|
52936
53158
|
out.push({
|
|
52937
53159
|
file,
|
|
52938
53160
|
line: ln,
|
|
52939
|
-
type: "
|
|
52940
|
-
category: "
|
|
53161
|
+
type: "dynamic_method_call",
|
|
53162
|
+
category: "Route confusion (dynamic method dispatch)",
|
|
52941
53163
|
severity: "high",
|
|
52942
53164
|
evidence: l.trim(),
|
|
52943
|
-
detail: "
|
|
53165
|
+
detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
|
|
52944
53166
|
});
|
|
52945
53167
|
}
|
|
52946
53168
|
}
|
|
52947
|
-
function
|
|
52948
|
-
const ls =
|
|
52949
|
-
const
|
|
53169
|
+
function detectDynamicInclude(code, file, out) {
|
|
53170
|
+
const ls = lines(code);
|
|
53171
|
+
const re = /\b(include|require)(_once)?\s*\(\s*(\$[A-Za-z_]\w*)/g;
|
|
52950
53172
|
let m;
|
|
52951
|
-
while ((m =
|
|
52952
|
-
const ln =
|
|
53173
|
+
while ((m = re.exec(code)) !== null) {
|
|
53174
|
+
const ln = lineNo(code, m.index);
|
|
52953
53175
|
const l = ls[ln - 1] ?? "";
|
|
52954
|
-
const scope = ls.slice(Math.max(0, ln -
|
|
53176
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
52955
53177
|
`);
|
|
52956
|
-
if (!
|
|
53178
|
+
if (!userControlled(l) && !userControlled(scope))
|
|
52957
53179
|
continue;
|
|
52958
53180
|
out.push({
|
|
52959
53181
|
file,
|
|
52960
53182
|
line: ln,
|
|
52961
|
-
type: "
|
|
52962
|
-
category: "
|
|
52963
|
-
severity: "
|
|
53183
|
+
type: "dynamic_include",
|
|
53184
|
+
category: "Route confusion (dynamic file include)",
|
|
53185
|
+
severity: "high",
|
|
52964
53186
|
evidence: l.trim(),
|
|
52965
|
-
detail:
|
|
53187
|
+
detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
|
|
52966
53188
|
});
|
|
52967
53189
|
}
|
|
52968
53190
|
}
|
|
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;
|
|
53191
|
+
function detectBatchForwarding(code, file, out) {
|
|
53192
|
+
BATCH_LOOP_RE.lastIndex = 0;
|
|
52972
53193
|
let m;
|
|
52973
|
-
while ((m =
|
|
52974
|
-
const ln =
|
|
52975
|
-
const
|
|
52976
|
-
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53194
|
+
while ((m = BATCH_LOOP_RE.exec(code)) !== null) {
|
|
53195
|
+
const ln = lineNo(code, m.index);
|
|
53196
|
+
const body = lines(code).slice(ln, ln + 8).join(`
|
|
52977
53197
|
`);
|
|
52978
|
-
|
|
52979
|
-
|
|
52980
|
-
if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
|
|
52981
|
-
` + l))
|
|
53198
|
+
const forwards = /\b(call_user_func|forward|dispatch|resolve_route|invoke|handle|route)\s*\(/i.test(body);
|
|
53199
|
+
if (!forwards)
|
|
52982
53200
|
continue;
|
|
53201
|
+
const rechecks = /\b(current_user_can|permission_callback|check_ajax_referer|authorize|authorise|is_user_logged_in)\b/i.test(body);
|
|
52983
53202
|
out.push({
|
|
52984
53203
|
file,
|
|
52985
53204
|
line: ln,
|
|
52986
|
-
type: "
|
|
52987
|
-
category: "
|
|
52988
|
-
severity: "medium",
|
|
52989
|
-
evidence:
|
|
52990
|
-
detail: "
|
|
53205
|
+
type: "batch_forwarding",
|
|
53206
|
+
category: "Route confusion (batch/proxy forwarding)",
|
|
53207
|
+
severity: rechecks ? "medium" : "high",
|
|
53208
|
+
evidence: lines(code)[ln - 1]?.trim() ?? "",
|
|
53209
|
+
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
53210
|
});
|
|
52992
53211
|
}
|
|
52993
53212
|
}
|
|
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;
|
|
53213
|
+
function detectRouteNormalization(code, file, out) {
|
|
53214
|
+
const ls = lines(code);
|
|
53215
|
+
for (let i = 0;i < ls.length; i++) {
|
|
53216
|
+
const l = ls[i];
|
|
53217
|
+
if (!ROUTE_MATCH_RE.test(l))
|
|
53218
|
+
continue;
|
|
53219
|
+
const scope = ls.slice(Math.max(0, i - 3), i + 1).join(`
|
|
53220
|
+
`);
|
|
53221
|
+
if (!userControlled(scope))
|
|
53222
|
+
continue;
|
|
53223
|
+
if (NORMALIZERS.test(l) || NORMALIZERS.test(scope))
|
|
53224
|
+
continue;
|
|
53225
|
+
out.push({
|
|
53226
|
+
file,
|
|
53227
|
+
line: i + 1,
|
|
53228
|
+
type: "route_normalization_gap",
|
|
53229
|
+
category: "Route confusion (missing path normalization)",
|
|
53230
|
+
severity: "medium",
|
|
53231
|
+
evidence: l.trim(),
|
|
53232
|
+
detail: "Route matched against user input without visible normalization (strtolower/rtrim/urldecode) — route desync possible."
|
|
53063
53233
|
});
|
|
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
53234
|
}
|
|
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
53235
|
}
|
|
53125
|
-
function
|
|
53126
|
-
const
|
|
53127
|
-
|
|
53128
|
-
|
|
53129
|
-
|
|
53130
|
-
|
|
53131
|
-
|
|
53132
|
-
|
|
53133
|
-
}
|
|
53134
|
-
return [...params];
|
|
53236
|
+
function detectRouteConfusion(code, file) {
|
|
53237
|
+
const out = [];
|
|
53238
|
+
detectDynamicDispatch(code, file, out);
|
|
53239
|
+
detectDynamicMethod(code, file, out);
|
|
53240
|
+
detectDynamicInclude(code, file, out);
|
|
53241
|
+
detectBatchForwarding(code, file, out);
|
|
53242
|
+
detectRouteNormalization(code, file, out);
|
|
53243
|
+
return out;
|
|
53135
53244
|
}
|
|
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)" };
|
|
53245
|
+
|
|
53246
|
+
// src/complex-bugs.ts
|
|
53247
|
+
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)/;
|
|
53248
|
+
function lines2(code) {
|
|
53249
|
+
return code.split(`
|
|
53250
|
+
`);
|
|
53154
53251
|
}
|
|
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)" };
|
|
53252
|
+
function lineNo2(code, pos) {
|
|
53253
|
+
let n = 1;
|
|
53254
|
+
for (let i = 0;i < pos; i++)
|
|
53255
|
+
if (code.charCodeAt(i) === 10)
|
|
53256
|
+
n++;
|
|
53257
|
+
return n;
|
|
53174
53258
|
}
|
|
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))
|
|
53259
|
+
var MAGIC_METHODS = /\b(__destruct|__wakeup|__toString|__invoke|__call|__get|__set|__isset|__unset|__sleep)\s*\(/;
|
|
53260
|
+
function detectDeserialization(code, file, out) {
|
|
53261
|
+
if (!MAGIC_METHODS.test(code))
|
|
53262
|
+
return;
|
|
53263
|
+
const ls = lines2(code);
|
|
53264
|
+
const re = /\b(unserialize|maybe_unserialize)\s*\(/g;
|
|
53265
|
+
let m;
|
|
53266
|
+
while ((m = re.exec(code)) !== null) {
|
|
53267
|
+
const ln = lineNo2(code, m.index);
|
|
53268
|
+
const l = ls[ln - 1] ?? "";
|
|
53269
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
53270
|
+
`);
|
|
53271
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53235
53272
|
continue;
|
|
53236
|
-
|
|
53237
|
-
|
|
53238
|
-
|
|
53239
|
-
|
|
53240
|
-
|
|
53241
|
-
|
|
53273
|
+
out.push({
|
|
53274
|
+
file,
|
|
53275
|
+
line: ln,
|
|
53276
|
+
type: "deserialization",
|
|
53277
|
+
category: "Deserialization → POP gadget chain",
|
|
53278
|
+
severity: "high",
|
|
53279
|
+
evidence: l.trim(),
|
|
53280
|
+
detail: `\`${m[1]}()\` of attacker-controlled data AND a magic method is present — object injection can pivot to RCE via the gadget chain.`
|
|
53281
|
+
});
|
|
53242
53282
|
}
|
|
53243
|
-
return { found, source: "DNS resolution (common subdomain prefixes)" };
|
|
53244
53283
|
}
|
|
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);
|
|
53284
|
+
function detectTypeJuggling(code, file, out) {
|
|
53285
|
+
const ls = lines2(code);
|
|
53286
|
+
const re = /(?<![=!<>])(==|!=)(?!=)/g;
|
|
53287
|
+
let m;
|
|
53288
|
+
while ((m = re.exec(code)) !== null) {
|
|
53289
|
+
const ln = lineNo2(code, m.index);
|
|
53290
|
+
const l = ls[ln - 1] ?? "";
|
|
53291
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
53292
|
+
`);
|
|
53293
|
+
const hasSource = SOURCE_TOKENS2.test(l) || SOURCE_TOKENS2.test(scope);
|
|
53294
|
+
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);
|
|
53295
|
+
if (!hasSource || !hasHash)
|
|
53296
|
+
continue;
|
|
53297
|
+
out.push({
|
|
53298
|
+
file,
|
|
53299
|
+
line: ln,
|
|
53300
|
+
type: "type_juggling",
|
|
53301
|
+
category: "Type juggling (loose comparison)",
|
|
53302
|
+
severity: "medium",
|
|
53303
|
+
evidence: l.trim(),
|
|
53304
|
+
detail: 'Loose comparison of user input against a hash/secret — PHP type juggling (`0e…`, `"str" == 0`) may bypass the check. Use strict `===`.'
|
|
53279
53305
|
});
|
|
53280
|
-
|
|
53281
|
-
|
|
53282
|
-
|
|
53306
|
+
}
|
|
53307
|
+
}
|
|
53308
|
+
function detectMassAssignment(code, file, out) {
|
|
53309
|
+
const ls = lines2(code);
|
|
53310
|
+
const extractRe = /\bextract\s*\(\s*[^)]*(REQUEST|POST|GET|COOKIE|FILES|\$_(REQUEST|POST|GET|COOKIE|FILES))/g;
|
|
53311
|
+
let m;
|
|
53312
|
+
while ((m = extractRe.exec(code)) !== null) {
|
|
53313
|
+
const ln = lineNo2(code, m.index);
|
|
53314
|
+
const l = ls[ln - 1] ?? "";
|
|
53315
|
+
if (/EXTR_SKIP|EXTR_PREFIX_ALL/.test(l))
|
|
53316
|
+
continue;
|
|
53317
|
+
out.push({
|
|
53318
|
+
file,
|
|
53319
|
+
line: ln,
|
|
53320
|
+
type: "mass_assignment",
|
|
53321
|
+
category: "Mass assignment (variable injection)",
|
|
53322
|
+
severity: "high",
|
|
53323
|
+
evidence: l.trim(),
|
|
53324
|
+
detail: "`extract()` of request data without EXTR_SKIP — attacker can overwrite arbitrary variables."
|
|
53283
53325
|
});
|
|
53284
|
-
|
|
53285
|
-
|
|
53286
|
-
|
|
53326
|
+
}
|
|
53327
|
+
const parseRe = /\bparse_str\s*\(/g;
|
|
53328
|
+
while ((m = parseRe.exec(code)) !== null) {
|
|
53329
|
+
const ln = lineNo2(code, m.index);
|
|
53330
|
+
const l = ls[ln - 1] ?? "";
|
|
53331
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53332
|
+
`);
|
|
53333
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53334
|
+
continue;
|
|
53335
|
+
out.push({
|
|
53336
|
+
file,
|
|
53337
|
+
line: ln,
|
|
53338
|
+
type: "mass_assignment",
|
|
53339
|
+
category: "Mass assignment (variable injection)",
|
|
53340
|
+
severity: "medium",
|
|
53341
|
+
evidence: l.trim(),
|
|
53342
|
+
detail: "`parse_str()` of user input into the symbol table — variable injection."
|
|
53287
53343
|
});
|
|
53344
|
+
}
|
|
53345
|
+
}
|
|
53346
|
+
function detectComplexBugs(code, file) {
|
|
53347
|
+
const out = [];
|
|
53348
|
+
detectDeserialization(code, file, out);
|
|
53349
|
+
detectTypeJuggling(code, file, out);
|
|
53350
|
+
detectMassAssignment(code, file, out);
|
|
53351
|
+
detectPrototypePollution(code, file, out);
|
|
53352
|
+
detectCrlfInjection(code, file, out);
|
|
53353
|
+
detectPathConfusion(code, file, out);
|
|
53354
|
+
const seen = new Set;
|
|
53355
|
+
return out.filter((f) => {
|
|
53356
|
+
const key = `${f.type}:${f.line}`;
|
|
53357
|
+
if (seen.has(key))
|
|
53358
|
+
return false;
|
|
53359
|
+
seen.add(key);
|
|
53360
|
+
return true;
|
|
53288
53361
|
});
|
|
53289
53362
|
}
|
|
53290
|
-
|
|
53291
|
-
const
|
|
53292
|
-
|
|
53293
|
-
|
|
53294
|
-
|
|
53295
|
-
|
|
53296
|
-
|
|
53297
|
-
|
|
53363
|
+
function detectPrototypePollution(code, file, out) {
|
|
53364
|
+
const ls = lines2(code);
|
|
53365
|
+
const mergeRe = /\b(Object\.assign|_\.merge|_\.defaultsDeep|\.extend|\.merge|\.defaultsDeep|mergeDeep|deepMerge|_.set)\s*\(/g;
|
|
53366
|
+
let m;
|
|
53367
|
+
while ((m = mergeRe.exec(code)) !== null) {
|
|
53368
|
+
const ln = lineNo2(code, m.index);
|
|
53369
|
+
const l = ls[ln - 1] ?? "";
|
|
53370
|
+
const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
|
|
53371
|
+
`);
|
|
53372
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53373
|
+
continue;
|
|
53374
|
+
out.push({
|
|
53375
|
+
file,
|
|
53376
|
+
line: ln,
|
|
53377
|
+
type: "prototype_pollution",
|
|
53378
|
+
category: "Prototype pollution (unsafe merge)",
|
|
53379
|
+
severity: "high",
|
|
53380
|
+
evidence: l.trim(),
|
|
53381
|
+
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."
|
|
53382
|
+
});
|
|
53298
53383
|
}
|
|
53299
|
-
return results;
|
|
53300
53384
|
}
|
|
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
|
-
}
|
|
53385
|
+
function detectCrlfInjection(code, file, out) {
|
|
53386
|
+
const ls = lines2(code);
|
|
53387
|
+
const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
|
|
53388
|
+
let m;
|
|
53389
|
+
while ((m = headerRe.exec(code)) !== null) {
|
|
53390
|
+
const ln = lineNo2(code, m.index);
|
|
53391
|
+
const l = ls[ln - 1] ?? "";
|
|
53392
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53393
|
+
`);
|
|
53394
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53395
|
+
continue;
|
|
53396
|
+
out.push({
|
|
53397
|
+
file,
|
|
53398
|
+
line: ln,
|
|
53399
|
+
type: "crlf_injection",
|
|
53400
|
+
category: "CRLF / header injection",
|
|
53401
|
+
severity: "medium",
|
|
53402
|
+
evidence: l.trim(),
|
|
53403
|
+
detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
|
|
53404
|
+
});
|
|
53324
53405
|
}
|
|
53325
|
-
return out;
|
|
53326
53406
|
}
|
|
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
|
-
|
|
53407
|
+
function detectPathConfusion(code, file, out) {
|
|
53408
|
+
const ls = lines2(code);
|
|
53409
|
+
const fileRe = /\b(file_get_contents|file_put_contents|fopen|readFileSync|readFile|open|include|require|new\s+File|Files\.read|FileInputStream|FileOutputStream)\s*\(/g;
|
|
53410
|
+
let m;
|
|
53411
|
+
while ((m = fileRe.exec(code)) !== null) {
|
|
53412
|
+
const ln = lineNo2(code, m.index);
|
|
53413
|
+
const l = ls[ln - 1] ?? "";
|
|
53414
|
+
const scope = ls.slice(Math.max(0, ln - 3), ln).join(`
|
|
53415
|
+
`);
|
|
53416
|
+
if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
|
|
53417
|
+
continue;
|
|
53418
|
+
if (/\b(basename|realpath|canonicalPath|getCanonicalPath|normalize|resolve|toRealPath)\s*\(/i.test(scope + `
|
|
53419
|
+
` + l))
|
|
53420
|
+
continue;
|
|
53421
|
+
out.push({
|
|
53422
|
+
file,
|
|
53423
|
+
line: ln,
|
|
53424
|
+
type: "path_confusion",
|
|
53425
|
+
category: "Path confusion (unsanitized file path)",
|
|
53426
|
+
severity: "medium",
|
|
53427
|
+
evidence: l.trim(),
|
|
53428
|
+
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."
|
|
53429
|
+
});
|
|
53349
53430
|
}
|
|
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
53431
|
}
|
|
53364
|
-
|
|
53365
|
-
|
|
53366
|
-
|
|
53367
|
-
|
|
53368
|
-
|
|
53369
|
-
|
|
53370
|
-
|
|
53432
|
+
|
|
53433
|
+
// src/guidance.ts
|
|
53434
|
+
var SINK_DIRECTIVES = {
|
|
53435
|
+
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.",
|
|
53436
|
+
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).",
|
|
53437
|
+
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.",
|
|
53438
|
+
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.",
|
|
53439
|
+
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://).",
|
|
53440
|
+
deserialization: "Deserialization needs a crafted gadget chain — read_tool_manual (ysoserial / PHPGGC) and confirm the entry point is reachable before validating.",
|
|
53441
|
+
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.",
|
|
53442
|
+
redirect: "Validate open redirect with strike_verify (marker URL vs control). Confirm the redirect reflects attacker-controlled input.",
|
|
53443
|
+
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.",
|
|
53444
|
+
xml_processing: "Validate XXE with strike_verify (external entity marker vs control). Read payload_lookup for xxe payloads.",
|
|
53445
|
+
archive_extraction: "Validate the archive/path issue (zip-slip / path traversal) with strike_verify. Confirm the extraction path is attacker-controlled.",
|
|
53446
|
+
path_traversal: "Validate path traversal with strike_verify (marker file read vs control). Read payload_lookup for path-traversal payloads.",
|
|
53447
|
+
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.",
|
|
53448
|
+
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.",
|
|
53449
|
+
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."
|
|
53450
|
+
};
|
|
53451
|
+
function nextSteps(ctx) {
|
|
53452
|
+
const steps = [];
|
|
53453
|
+
if (ctx.sanitizedCount && ctx.sanitizedCount > 0) {
|
|
53454
|
+
steps.push(`${ctx.sanitizedCount} sink(s) were sanitized and are NOT findings — do not report them. Only tainted, unsanitized, unguarded sinks count.`);
|
|
53455
|
+
}
|
|
53456
|
+
if (ctx.taintedSinks && ctx.taintedSinks.length > 0) {
|
|
53457
|
+
const seen = new Set;
|
|
53458
|
+
for (const k of ctx.taintedSinks) {
|
|
53459
|
+
if (seen.has(k))
|
|
53460
|
+
continue;
|
|
53461
|
+
seen.add(k);
|
|
53462
|
+
const d = SINK_DIRECTIVES[k];
|
|
53463
|
+
if (d)
|
|
53464
|
+
steps.push(d);
|
|
53371
53465
|
}
|
|
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
53466
|
}
|
|
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 }));
|
|
53467
|
+
if (steps.length === 0 && ctx.hasFindings) {
|
|
53468
|
+
steps.push("Findings found. Trace each sink's reachability (taint_file / eagle_eye), then validate live with strike_verify before reporting.");
|
|
53423
53469
|
}
|
|
53424
|
-
|
|
53425
|
-
|
|
53426
|
-
|
|
53427
|
-
|
|
53428
|
-
|
|
53429
|
-
|
|
53430
|
-
|
|
53431
|
-
|
|
53470
|
+
if (ctx.findingStatus) {
|
|
53471
|
+
const transitions = {
|
|
53472
|
+
detected: "detected -> triaged (classify severity + CWE + reachability)",
|
|
53473
|
+
triaged: "triaged -> hypothesis (state the sink, source, and expected impact)",
|
|
53474
|
+
hypothesis: "hypothesis -> validating (run strike_verify with marker + negative control)",
|
|
53475
|
+
validating: "validating -> confirmed (only if marker reflected AND negative control inert) or false_positive",
|
|
53476
|
+
confirmed: "confirmed — attach evidence (SHA-256), then report."
|
|
53477
|
+
};
|
|
53478
|
+
if (transitions[ctx.findingStatus]) {
|
|
53479
|
+
steps.push(`Finding lifecycle next step: ${transitions[ctx.findingStatus]}.`);
|
|
53480
|
+
}
|
|
53432
53481
|
}
|
|
53433
|
-
|
|
53434
|
-
|
|
53482
|
+
if (ctx.hasFindings || ctx.findingStatus) {
|
|
53483
|
+
steps.push("Record every confirmed finding with finding_create, attach confidence_score, and redact any secrets before persisting.");
|
|
53435
53484
|
}
|
|
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
|
-
};
|
|
53485
|
+
return steps;
|
|
53470
53486
|
}
|
|
53471
53487
|
|
|
53472
53488
|
// src/benchmark.ts
|
|
@@ -53812,7 +53828,7 @@ function createServer() {
|
|
|
53812
53828
|
name: "blitzstrike",
|
|
53813
53829
|
version: "1.0.0"
|
|
53814
53830
|
}, {
|
|
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 marker sweep + confirmed findings + report.
|
|
53831
|
+
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 + marker/negative-control sweep) " + " + confirmed 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
53832
|
` + "- 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
53833
|
});
|
|
53818
53834
|
server.registerTool("blitz_scan", {
|
|
@@ -54000,7 +54016,7 @@ function createServer() {
|
|
|
54000
54016
|
});
|
|
54001
54017
|
server.registerTool("run_engagement", {
|
|
54002
54018
|
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 marker sweep
|
|
54019
|
+
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) + marker/negative-control sweep per parameter -> confirmed findings -> inline report. Prefer this over driving tools one-by-one when you want the whole engagement in a single deterministic call.",
|
|
54004
54020
|
inputSchema: {
|
|
54005
54021
|
target: string2().describe("Source path or URL"),
|
|
54006
54022
|
scope: string2().optional().describe("Scope (for URL targets)"),
|