blitzstrike 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +1686 -1670
- 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
|
|
@@ -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)"),
|