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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.js +1686 -1670
  3. 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 readFileSync6, existsSync as existsSync4, statSync as statSync2 } from "node:fs";
29766
- import { join as join6 } from "node:path";
29767
- import { fileURLToPath as fileURLToPath3 } from "node:url";
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/orchestrator.ts
30790
- var ENGAGEMENT_STAGES = ["scope", "recon", "analyze", "verify", "review", "report"];
30791
- function engagementBanner(target, mode) {
30792
- return [
30793
- "⚡ BLITZ engagement — orchestrator in control",
30794
- ` target: ${target}`,
30795
- ` mode: ${mode}`,
30796
- ` stages: ${ENGAGEMENT_STAGES.join(" → ")}`
30797
- ].join(`
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 formatStatus(opts) {
30801
- const lines = [];
30802
- const idx = opts.phase ? ENGAGEMENT_STAGES.indexOf(opts.phase) : -1;
30803
- lines.push(`BLITZ engagement${idx >= 0 ? ` — ${opts.phase} (${idx + 1}/${ENGAGEMENT_STAGES.length})` : ""}`);
30804
- if (idx >= 0) {
30805
- lines.push(` stages: ${ENGAGEMENT_STAGES.map((s, i) => i < idx ? `${s} ✓` : i === idx ? `${s} ▶` : `${s} ☐`).join(" ")}`);
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
- if (opts.findings) {
30808
- const f = opts.findings;
30809
- lines.push(` findings: ${[`${f.detected ?? 0} detected`, `${f.hypothesis ?? 0} hypothesis`, `${f.confirmed ?? 0} confirmed`, `${f.false_positive ?? 0} false_positive`].join(" · ")}`);
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
- if (opts.tasks && opts.tasks.length) {
30812
- lines.push(` tasks: ${opts.tasks.map((t) => `${t.objective} (${t.status})`).join(" · ")}`);
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
- return lines.join(`
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 formatPlanChecklist(plan) {
30818
- const mark = (s) => s === "done" ? "✓" : s === "pending" ? "☐" : "▶";
30819
- const items = plan.map((p) => `[${mark(p.status)}] ${p.state}`);
30820
- const rows = [];
30821
- for (let i = 0;i < items.length; i += 3) {
30822
- rows.push(" " + items.slice(i, i + 3).map((s) => s.padEnd(22)).join(""));
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 rows.join(`
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 classifyTarget(target) {
30828
- if (/^https?:\/\//i.test(target))
30829
- return { kind: "live", target };
30830
- try {
30831
- if (existsSync4(target) && (statSync2(target).isDirectory() || statSync2(target).isFile())) {
30832
- return { kind: "source", target };
30833
- }
30834
- } catch {}
30835
- 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;
30836
- if (CODE_EXT.test(target) && !target.includes("/")) {
30837
- return { kind: "source", target };
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
- const host = /^[\w.-]+\.[a-zA-Z]{2,}(:\d+)?([/?#].*)?$/.test(target) || /^\d{1,3}(\.\d{1,3}){3}(:\d+)?$/.test(target);
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
- var _chainsCache = null;
30843
- function loadChains() {
30844
- if (_chainsCache)
30845
- return _chainsCache;
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
- const dir = join6(fileURLToPath3(new URL(".", import.meta.url)), "..");
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 scopeCheck(target, scope = "", mode = "bug-bounty") {
30858
- if (target.startsWith("/") || target.startsWith("./") || target.startsWith("../") || target.startsWith("~") || /^[a-zA-Z]:[\\/]/.test(target)) {
30859
- return { target, allowed: true, enforcement: "off", reason: "static source path — always in-scope" };
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
- let result;
30862
- if (mode === "ctf" || mode === "reverse-engineering" || mode === "offensive") {
30863
- result = { target, allowed: true, enforcement: "off", reason: `mode=${mode} disables scope enforcement` };
30864
- } else if (!scope.trim()) {
30865
- result = { target, allowed: false, enforcement: "strict", reason: "no scope provided; refusing active testing" };
30866
- } else {
30867
- const inScope = [];
30868
- const outScope = [];
30869
- for (const raw of scope.split(/[,\n]/)) {
30870
- const line = raw.trim();
30871
- if (!line)
30872
- continue;
30873
- if (line.startsWith("-"))
30874
- outScope.push(line.slice(1).trim());
30875
- else
30876
- inScope.push(line);
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
- let host = target;
30879
- host = host.replace(/^https?:\/\//, "");
30880
- host = host.split("/")[0].split(":")[0];
30881
- let decided = false;
30882
- result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' not in scope` };
30883
- for (const exc of outScope) {
30884
- if (exc && (host.includes(exc) || host.endsWith("." + exc.replace(/^\./, "")))) {
30885
- result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' is explicitly out of scope (${exc})` };
30886
- decided = true;
30887
- break;
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
- if (!decided) {
30891
- for (const allowed of inScope) {
30892
- const a = allowed.replace(/\*/g, "");
30893
- if (!a)
30894
- continue;
30895
- if (host.includes(a) || host.endsWith("." + a.replace(/^\./, ""))) {
30896
- result = { target, allowed: true, enforcement: "strict", reason: `matched in-scope pattern '${allowed}'` };
30897
- decided = true;
30898
- break;
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
- recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
30904
- return result;
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 classifySink(sink) {
30907
- if (/(move_uploaded_file|file_put_contents|fwrite)/.test(sink))
30908
- return "upload_weak_validation";
30909
- if (/unserialize/.test(sink))
30910
- return "unserialize_user_input";
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 enrichScan(path, maxFiles = 2000) {
30926
- const expanded = path.replace(/^~/, process.env.HOME ?? "~");
30927
- const files = iterSourceFiles(expanded, maxFiles);
30928
- const hits = [];
30929
- const sinkKinds = new Set;
30930
- for (const p of files) {
30931
- const r = scanFile(p);
30932
- for (const e of r.endpoints) {
30933
- hits.push({ type: "endpoint", file: r.file, line: e.line, detail: e.hook });
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 chains = loadChains();
30943
- const matched = [];
30944
- for (const c of chains) {
30945
- if ((c.required_findings ?? []).some((rf) => sinkKinds.has(rf))) {
30946
- matched.push({ chain_id: c.id, name: c.name, severity: c.severity, steps: c.steps.length });
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
- root: expanded,
30951
- files_scanned: files.length,
30952
- hits: hits.slice(0, 2000),
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 runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000, remember = true) {
30958
- const report = {
30959
- banner: engagementBanner(target, mode),
30960
- target,
30961
- mode,
30962
- tiers: {},
30963
- findings: [],
30964
- scope: null,
30965
- memory: { captured: 0 }
30966
- };
30967
- const isPath = (() => {
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
- report.tiers = { ...report.tiers, eagle_eye: { traced_chains: eagle } };
31022
- const rank = { critical: 0, high: 1, medium: 2, low: 3 };
31023
- const matchedSorted = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
31024
- const findings = matchedSorted.map((f) => {
31025
- const chain = chainById.get(f.chain_id);
31026
- const severity = f.severity ?? "medium";
31027
- const finding = makeFinding({
31028
- title: f.name,
31029
- target: { type: "source", path: target },
31030
- severity,
31031
- source: { type: "static_sink_signal", name: f.chain_id },
31032
- sink: { type: f.chain_id, symbol: f.name },
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 report;
31219
+ return { found: false, signal, suggestions: data.error_classes.map((c) => c.id) };
31097
31220
  }
31098
- function listChains() {
31099
- const chains = loadChains();
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
- total: chains.length,
31102
- chains: chains.map((c) => ({
31103
- id: c.id,
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
- var LIVE_SWEEP_PARAMS = ["q", "search", "query", "s", "id", "url", "redirect", "name", "page"];
31112
- async function runLiveEngagement(url, scope = "", mode = "bug-bounty") {
31113
- const report = { banner: engagementBanner(url, mode), target: url, mode, findings: [], scope: null, status: "" };
31114
- const sc = scopeCheck(url, scope, mode);
31115
- report.scope = sc;
31116
- const findings = [];
31117
- for (const param of LIVE_SWEEP_PARAMS) {
31118
- const verdict = await strikeVerify({ url, param });
31119
- const finding = makeFinding({
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
- report.findings = findings.map((f) => ({
31134
- id: f.id,
31135
- status: f.status,
31136
- title: f.title,
31137
- severity: f.classification.severity,
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
- // src/catalog.ts
31164
- import { readFileSync as readFileSync7, readdirSync as readdirSync2 } from "node:fs";
31165
- import { join as join7 } from "node:path";
31166
- import { fileURLToPath as fileURLToPath4 } from "node:url";
31167
- import { spawnSync } from "node:child_process";
31168
- var ROOT2 = join7(fileURLToPath4(new URL(".", import.meta.url)), "..");
31169
- var _toolsCache = null;
31170
- var _skillsCache = null;
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
- return _toolsCache;
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 loadSkills() {
31183
- if (_skillsCache)
31184
- return _skillsCache;
31185
- const dir = join7(ROOT2, "skills");
31186
- const out = [];
31187
- try {
31188
- for (const f of readdirSync2(dir)) {
31189
- if (!f.endsWith(".md"))
31190
- continue;
31191
- const p = join7(dir, f);
31192
- const text = readFileSync7(p, "utf8");
31193
- const descMatch = text.match(/^description:\s*(.+)$/m);
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 toolLookup(name) {
31206
- const tools = loadTools();
31207
- const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
31208
- if (!t) {
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: false,
31211
- query: name,
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
- return result;
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 listTools() {
31230
- const tools = loadTools();
31231
- const byCat = {};
31232
- for (const t of tools) {
31233
- (byCat[t.category] ??= []).push(t.name);
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
- return { total: tools.length, categories: byCat, tools: tools.map((t) => ({ name: t.name, category: t.category, description: t.description })) };
31236
- }
31237
- function skillLookup(topic) {
31238
- const skills = loadSkills();
31239
- const q = topic.toLowerCase();
31240
- const scored = skills.map((s) => {
31241
- const hay = `${s.name} ${s.description}`.toLowerCase();
31242
- const nameHit = s.name.includes(q);
31243
- const descHit = s.description.includes(q);
31244
- const wordHit = q.split(/\s+/).filter((w) => w && hay.includes(w)).length;
31245
- return { s, score: (nameHit ? 10 : 0) + (descHit ? 5 : 0) + wordHit * 2 };
31246
- }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
31247
- if (scored.length === 0) {
31248
- return { found: false, query: topic, suggestions: skills.map((s) => s.name).slice(0, 10) };
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: true,
31252
- matches: scored.slice(0, 5).map((x) => ({ name: x.s.name, description: x.s.description, file: x.s.file }))
31356
+ found: false,
31357
+ error: "unknown taxonomy kind",
31358
+ usage: "taxonomy(kind='web'|'api'|'cwe'|'asvs', query?)"
31253
31359
  };
31254
31360
  }
31255
- function listSkills() {
31256
- const skills = loadSkills();
31257
- return { total: skills.length, skills: skills.map((s) => ({ name: s.name, description: s.description })) };
31258
- }
31259
- function readSkill(name) {
31260
- const skills = loadSkills();
31261
- const s = skills.find((x) => x.name.toLowerCase() === name.toLowerCase());
31262
- if (!s)
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 text = readFileSync7(join7(ROOT2, "skills", s.file), "utf8");
31266
- return { found: true, name: s.name, content: text.slice(0, 12000) };
31267
- } catch {
31268
- return { found: false, query: name, error: "unreadable" };
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 ensureTool(name) {
31272
- const tools = loadTools();
31273
- const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
31274
- if (!t) {
31275
- return { found: false, query: name, suggestions: tools.slice(0, 5).map((x) => x.name) };
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
- const check = spawnSync(t.check_installed.command, { shell: true, timeout: 15000 });
31278
- if (check.status !== 127 && check.error === undefined) {
31279
- return { name: t.name, installed: true, action: "none", note: "already installed" };
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
- const platform = process.platform;
31282
- const cmd = t.install[platform] ?? t.install.linux;
31283
- if (!cmd) {
31284
- return { name: t.name, installed: false, action: "none", note: `no install command for ${platform}` };
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 r = spawnSync(cmd, { shell: true, timeout: 120000 });
31287
- const ok = r.status === 0;
31288
- return {
31289
- name: t.name,
31290
- installed: ok,
31291
- action: "install",
31292
- command: cmd,
31293
- output: (r.stdout?.toString() ?? "").slice(-400) || (r.stderr?.toString() ?? "").slice(-400)
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
- // src/intel.ts
31298
- import { readFileSync as readFileSync8, existsSync as existsSync5 } from "node:fs";
31299
- import { join as join8 } from "node:path";
31300
- import { fileURLToPath as fileURLToPath5 } from "node:url";
31301
- import { readdirSync as readdirSync3 } from "node:fs";
31302
- import { homedir as homedir3 } from "node:os";
31303
- var ROOT3 = join8(fileURLToPath5(new URL(".", import.meta.url)), "..");
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 detectWaf(headers, body = "") {
31316
- const sigs = loadJson("waf_signatures");
31317
- if (!sigs)
31318
- return { detected: false, wafs: [], error: "waf_signatures.json not loaded" };
31319
- const headerSigs = sigs.header_signatures ?? [];
31320
- const bodySigs = sigs.body_signatures ?? [];
31321
- const matches = [];
31322
- const headerKeys = Object.keys(headers).map((k) => k.toLowerCase());
31323
- for (const s of headerSigs) {
31324
- const h = (s.header ?? "").toLowerCase();
31325
- const pattern = (s.pattern ?? "").toLowerCase();
31326
- if (!h)
31327
- continue;
31328
- if (pattern) {
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
- for (const s of bodySigs) {
31340
- const pattern = (s.pattern ?? "").toLowerCase();
31341
- if (pattern && body.toLowerCase().includes(pattern)) {
31342
- matches.push({ waf: s.waf, pattern: s.pattern, confidence: s.confidence, via: "body" });
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
- const byWaf = new Map;
31346
- for (const m of matches) {
31347
- const key = m.waf;
31348
- const prev = byWaf.get(key);
31349
- if (!prev || m.confidence > prev.confidence)
31350
- byWaf.set(key, m);
31351
- }
31352
- const detected = [...byWaf.values()].sort((a, b) => b.confidence - a.confidence);
31353
- return {
31354
- detected: detected.length > 0,
31355
- wafs: detected,
31356
- block_status_codes: sigs.block_status_codes ?? []
31357
- };
31484
+ } catch {}
31485
+ return { urls: [...out].slice(0, 500), source: "web.archive.org (Wayback CDX)" };
31358
31486
  }
31359
- function techCorrelation(tech) {
31360
- const data = loadJson("tech_correlations");
31361
- if (!data)
31362
- return { found: false, error: "tech_correlations.json not loaded" };
31363
- const key = tech.toLowerCase();
31364
- const entry = data[key];
31365
- if (!entry) {
31366
- const matches = Object.keys(data).filter((k) => k.includes(key) || key.includes(k)).slice(0, 5);
31367
- return { found: false, tech, suggestions: matches };
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: true, tech: key, vulns: entry.vulns ?? [], paths: entry.paths ?? [], detection: entry.detection ?? { vulns: entry.vulns, paths: entry.paths } };
31555
+ return { found, source: "DNS resolution (common subdomain prefixes)" };
31370
31556
  }
31371
- function cveCorrelation(cveId) {
31372
- const data = loadJson("cve_correlations");
31373
- if (!data)
31374
- return { error: "cve_correlations.json not loaded" };
31375
- const key = cveId.toUpperCase();
31376
- const entry = data[key];
31377
- if (!entry)
31378
- return { found: false, cve: key, total_known: Object.keys(data).length };
31379
- return { found: true, cve: key, ...entry };
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 portCorrelation(port) {
31382
- const data = loadJson("port_correlations");
31383
- if (!data)
31384
- return { error: "port_correlations.json not loaded" };
31385
- const key = String(port);
31386
- const entry = data[key];
31387
- if (!entry)
31388
- return { found: false, port: key, total_known: Object.keys(data).length };
31389
- return { found: true, port: key, ...entry };
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
- function fuzzerPayloads(category) {
31392
- const data = loadJson("fuzzer_data");
31393
- if (!data)
31394
- return { error: "fuzzer_data.json not loaded" };
31395
- if (category) {
31396
- const c = data[category] ?? data[category.toUpperCase()];
31397
- return { category, found: Boolean(c), data: c ?? null };
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 { categories: Object.keys(data), fuzz_points: data.FUZZ_POINTS ?? null, payloads: data.FUZZ_PAYLOADS ?? null };
31637
+ return out;
31400
31638
  }
31401
- function intelSummary() {
31402
- const waf = loadJson("waf_signatures");
31403
- const tech = loadJson("tech_correlations");
31404
- const cve = loadJson("cve_correlations");
31405
- const port = loadJson("port_correlations");
31406
- const fuzz = loadJson("fuzzer_data");
31407
- const av = loadJson("attack_vectors");
31408
- return {
31409
- waf_signatures: waf ? (waf.header_signatures?.length ?? 0) + (waf.body_signatures?.length ?? 0) : 0,
31410
- tech_correlations: tech ? Object.keys(tech).length : 0,
31411
- cve_correlations: cve ? Object.keys(cve).length : 0,
31412
- port_correlations: port ? Object.keys(port).length : 0,
31413
- fuzzer_categories: fuzz ? Object.keys(fuzz).length : 0,
31414
- payload_categories: listPayloadCategories().length,
31415
- nuclei_templates: countTemplates(),
31416
- attack_vector_categories: av ? av.categories.length : 0,
31417
- attack_vectors: av ? av.categories.reduce((n, c) => n + c.vectors.length, 0) : 0
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 listAttackVectors() {
31421
- const data = loadJson("attack_vectors");
31422
- if (!data)
31423
- return { error: "attack_vectors.json not loaded" };
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
- total_categories: data.categories.length,
31426
- total_vectors: data.categories.reduce((n, c) => n + c.vectors.length, 0),
31427
- categories: data.categories.map((c) => ({ id: c.id, title: c.title, count: c.vectors.length, focus: c.focus ?? "" }))
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
- function attackVectors(category) {
31431
- const data = loadJson("attack_vectors");
31432
- if (!data)
31433
- return { error: "attack_vectors.json not loaded" };
31434
- const q = category.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
31435
- const found = data.categories.find((c) => c.id === q || c.id.includes(q) || c.title.toLowerCase().includes(category.toLowerCase()));
31436
- if (!found) {
31437
- const matches = data.categories.filter((c) => c.title.toLowerCase().includes(category.toLowerCase()) || c.id.includes(q)).slice(0, 5).map((c) => c.id);
31438
- return { found: false, query: category, suggestions: matches };
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
- return { found: true, id: found.id, title: found.title, focus: found.focus ?? "", count: found.vectors.length, vectors: found.vectors };
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
- var PACKAGE_PAYLOADS = join8(ROOT3, "payloads");
31443
- var PACKAGE_TEMPLATES = join8(ROOT3, "templates");
31444
- var DATA_ROOT = process.env.BLITZSTRIKE_DATA ?? join8(homedir3(), ".blitzstrike", "data");
31445
- function payloadsDir() {
31446
- if (existsSync5(PACKAGE_PAYLOADS))
31447
- return PACKAGE_PAYLOADS;
31448
- return join8(DATA_ROOT, "payloads");
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 templatesDir() {
31451
- if (existsSync5(PACKAGE_TEMPLATES))
31452
- return PACKAGE_TEMPLATES;
31453
- return join8(DATA_ROOT, "templates");
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 PAYLOADS = payloadsDir();
31456
- var TEMPLATES = templatesDir();
31457
- function listPayloadCategories() {
31837
+ var _chainsCache = null;
31838
+ function loadChains() {
31839
+ if (_chainsCache)
31840
+ return _chainsCache;
31458
31841
  try {
31459
- return readdirSync3(PAYLOADS, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
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 payloadLookup(topic) {
31465
- const cats = listPayloadCategories();
31466
- const key = topic.toLowerCase();
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
- return { found: false, topic, suggestions: cats.slice(0, 20) };
31502
- }
31503
- function readPayload(category, file) {
31504
- const cats = listPayloadCategories();
31505
- const exact = cats.find((c) => c.toLowerCase() === category.toLowerCase());
31506
- if (!exact)
31507
- return { found: false, category, suggestions: cats.slice(0, 20) };
31508
- const dir = join8(PAYLOADS, exact);
31509
- const files = listFiles(dir);
31510
- if (!file) {
31511
- const readme = files.find((f) => f.toLowerCase() === "readme.md");
31512
- if (readme) {
31513
- return { found: true, category: exact, file: readme, content: readFileSync8(join8(dir, readme), "utf8").slice(0, 20000) };
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
- const f = files.find((x) => x.toLowerCase() === file.toLowerCase());
31518
- if (!f)
31519
- return { found: false, category: exact, file, suggestions: files };
31520
- return { found: true, category: exact, file: f, content: readFileSync8(join8(dir, f), "utf8").slice(0, 20000) };
31898
+ recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
31899
+ return result;
31521
31900
  }
31522
- function listFiles(dir) {
31523
- try {
31524
- return readdirSync3(dir).filter((f) => f.endsWith(".md") || f.endsWith(".txt") || f.endsWith(".yaml")).sort();
31525
- } catch {
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 countTemplates() {
31530
- try {
31531
- let n = 0;
31532
- const walk = (dir) => {
31533
- for (const e of readdirSync3(dir, { withFileTypes: true })) {
31534
- if (e.isDirectory())
31535
- walk(join8(dir, e.name));
31536
- else if (e.name.endsWith(".yaml"))
31537
- n++;
31538
- }
31539
- };
31540
- walk(TEMPLATES);
31541
- return n;
31542
- } catch {
31543
- return 0;
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 templateLookup(topic, limit = 10) {
31547
- const key = topic.toLowerCase();
31548
- const hits = [];
31549
- const walk = (dir, depth = 0) => {
31550
- if (depth > 6 || hits.length >= limit * 3)
31551
- return;
31552
- let entries;
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
- entries = readdirSync3(dir, { withFileTypes: true });
31964
+ return existsSync5(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
31555
31965
  } catch {
31556
- return;
31966
+ return false;
31557
31967
  }
31558
- for (const e of entries) {
31559
- if (e.isDirectory())
31560
- walk(join8(dir, e.name), depth + 1);
31561
- else if (e.name.endsWith(".yaml")) {
31562
- const name = e.name.toLowerCase();
31563
- if (name.includes(key)) {
31564
- const full = join8(dir, e.name);
31565
- let id = "";
31566
- try {
31567
- const content = readFileSync8(full, "utf8");
31568
- const m = content.match(/^\s*id:\s*(.+)$/m);
31569
- if (m)
31570
- id = m[1].trim();
31571
- } catch {}
31572
- hits.push({ path: full.replace(ROOT3 + "/", ""), name: e.name, id });
31573
- }
31574
- }
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 key = defense.toLowerCase();
31616
- const norm = BYPASS_ALIASES[key] ?? key;
31617
- const match = cats.find((c) => c.id === norm || c.defense.toLowerCase().includes(norm) || norm.includes(c.id));
31618
- if (!match)
31619
- return { found: false, defense, suggestions: cats.map((c) => c.id) };
31620
- return { found: true, defense: match.defense, techniques: match.techniques };
31621
- }
31622
- function chainLinks() {
31623
- const data = loadJson("chain_links");
31624
- if (!data)
31625
- return { found: false, error: "chain_links.json not found" };
31626
- return { found: true, total: data.links.length, links: data.links };
31627
- }
31628
- function retryGuidance(signal) {
31629
- const data = loadJson("retry_guidance");
31630
- if (!data)
31631
- return { found: false, error: "retry_guidance.json not found" };
31632
- if (!signal) {
31633
- return {
31634
- found: true,
31635
- modes: data.modes.map((m) => ({ id: m.id, signal: m.signal, actions: m.actions.length }))
31636
- };
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
- const ALIASES = {
31639
- empty: "empty_output",
31640
- "no output": "empty_output",
31641
- stale: "empty_output",
31642
- blocked: "blocked",
31643
- waf: "blocked",
31644
- filter: "blocked",
31645
- firewall: "blocked",
31646
- sanitized: "sanitized",
31647
- sanitizer: "sanitized",
31648
- neutralized: "sanitized",
31649
- subagent: "subagent_failed",
31650
- "sub-agent": "subagent_failed",
31651
- delegate: "subagent_failed",
31652
- model: "model_failed",
31653
- "rate limit": "model_failed",
31654
- ratelimit: "model_failed",
31655
- quota: "model_failed",
31656
- context: "model_failed",
31657
- unconfirmed: "unconfirmed",
31658
- "not confirmed": "unconfirmed",
31659
- low: "low_severity",
31660
- "low severity": "low_severity"
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
- const key = signal.toLowerCase().trim();
31663
- const norm = ALIASES[key] ?? key.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
31664
- const match = data.modes.find((m) => m.id === norm || norm.includes(m.id) || m.id.includes(norm));
31665
- if (!match)
31666
- return { found: false, signal, suggestions: data.modes.map((m) => m.id) };
31667
- return { found: true, mode: match.id, signal: match.signal, actions: match.actions };
31668
- }
31669
- function orchestration(part) {
31670
- const data = loadJson("orchestration");
31671
- if (!data)
31672
- return { found: false, error: "orchestration.json not found" };
31673
- if (!part) {
31674
- return {
31675
- found: true,
31676
- phases: data.lifecycle.map((p) => ({ phase: p.phase, name: p.name })),
31677
- team: data.team.map((r) => ({ id: r.id, name: r.name }))
31678
- };
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
- const q = part.toLowerCase();
31681
- if (q === "lifecycle" || q === "phases")
31682
- return { found: true, lifecycle: data.lifecycle };
31683
- if (q === "team" || q === "roles")
31684
- return { found: true, team: data.team };
31685
- if (q === "handoff")
31686
- return { found: true, handoff: data.handoff };
31687
- if (q === "termination" || q === "done")
31688
- return { found: true, termination: data.termination };
31689
- const phase = data.lifecycle.find((p) => String(p.phase) === q);
31690
- if (phase)
31691
- return { found: true, ...phase };
31692
- const role = data.team.find((r) => String(r.id) === q);
31693
- if (role)
31694
- return { found: true, ...role };
32091
+ return report;
32092
+ }
32093
+ function listChains() {
32094
+ const chains = loadChains();
31695
32095
  return {
31696
- found: false,
31697
- part,
31698
- suggestions: ["lifecycle", "team", "handoff", "termination", ...data.lifecycle.map((p) => String(p.phase)), ...data.team.map((r) => String(r.id))]
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
- function modelFallback(signal) {
31702
- const data = loadJson("model_fallback");
31703
- if (!data)
31704
- return { found: false, error: "model_fallback.json not found" };
31705
- if (!signal) {
31706
- return {
31707
- found: true,
31708
- error_classes: data.error_classes.map((c) => ({ id: c.id, retryable: c.retryable, backoff: c.backoff })),
31709
- fallback_chain: data.fallback_chain,
31710
- backoff: data.backoff
31711
- };
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
- const q = signal.toLowerCase();
31714
- for (const c of data.error_classes) {
31715
- if (c.signals.some((s) => q.includes(s.toLowerCase()))) {
31716
- return {
31717
- found: true,
31718
- class: c.id,
31719
- retryable: c.retryable,
31720
- backoff: c.backoff,
31721
- action: c.action,
31722
- fallback_chain: data.fallback_chain,
31723
- backoff_schedule: c.backoff ? data.backoff : null
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
- return { found: false, signal, suggestions: data.error_classes.map((c) => c.id) };
32212
+ } catch {}
32213
+ _skillsCache = out;
32214
+ return out;
31728
32215
  }
31729
- function doctrineMap(part) {
31730
- const data = loadJson("doctrine_map");
31731
- if (!data)
31732
- return { found: false, error: "doctrine_map.json not found" };
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: true,
31736
- phases: data.phases.map((p) => ({ phase: p.phase, skill: p.skill, wstg: p.wstg })),
31737
- cross_cutting: data.cross_cutting
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 q = part.toLowerCase();
31741
- const phase = data.phases.find((p) => String(p.phase) === q);
31742
- if (phase)
31743
- return { found: true, ...phase };
31744
- if (q === "cross_cutting" || q === "cross" || q === "moments") {
31745
- return { found: true, cross_cutting: data.cross_cutting };
31746
- }
31747
- return {
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
- const q = category.toUpperCase();
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 techniqueLookup(cls) {
31770
- const data = loadJson("techniques");
31771
- if (!data)
31772
- return { found: false, error: "techniques.json not found" };
31773
- if (!cls) {
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
- const q = cls.toLowerCase();
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 frameworkTricks(framework) {
31786
- const data = loadJson("framework_tricks");
31787
- if (!data)
31788
- return { found: false, error: "framework_tricks.json not found" };
31789
- if (!framework) {
31790
- return { found: true, frameworks: data.frameworks.map((f) => ({ framework: f.framework })) };
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
- const q = framework.toLowerCase();
31793
- const f = data.frameworks.find((x) => String(x.framework).toLowerCase().includes(q));
31794
- if (f)
31795
- return { found: true, ...f };
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 resourceLookup(category) {
31799
- const data = loadJson("resources");
31800
- if (!data)
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 taxonomy(kind, query) {
31815
- if (kind === "api" || kind === "api_top10" || kind === "owasp-api") {
31816
- const data = loadJson("api_top10");
31817
- if (!data)
31818
- return { found: false, error: "api_top10.json not found" };
31819
- if (!query)
31820
- return { found: true, risks: data.risks };
31821
- const q = query.toLowerCase();
31822
- const r = data.risks.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
31823
- if (r)
31824
- return { found: true, ...r };
31825
- return { found: false, query, suggestions: data.risks.map((x) => String(x.id)) };
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
- if (kind === "asvs" || kind === "verification") {
31852
- const data = loadJson("asvs");
31853
- if (!data)
31854
- return { found: false, error: "asvs.json not found" };
31855
- if (!query)
31856
- return { found: true, chapters: data.chapters };
31857
- const q = query.toLowerCase();
31858
- const c = data.chapters.find((x) => String(x.id).toLowerCase() === q || String(x.name).toLowerCase().includes(q));
31859
- if (c)
31860
- return { found: true, ...c };
31861
- return { found: false, query, suggestions: data.chapters.map((x) => String(x.id)) };
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
- found: false,
31865
- error: "unknown taxonomy kind",
31866
- usage: "taxonomy(kind='web'|'api'|'cwe'|'asvs', query?)"
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 normalizeTarget(target) {
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 = normalizeTarget(target);
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 = mergeRe.exec(code)) !== null) {
52930
- const ln = lineNo2(code, m.index);
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 (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
53156
+ if (!userControlled(l) && !userControlled(scope))
52935
53157
  continue;
52936
53158
  out.push({
52937
53159
  file,
52938
53160
  line: ln,
52939
- type: "prototype_pollution",
52940
- category: "Prototype pollution (unsafe merge)",
53161
+ type: "dynamic_method_call",
53162
+ category: "Route confusion (dynamic method dispatch)",
52941
53163
  severity: "high",
52942
53164
  evidence: l.trim(),
52943
- 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."
53165
+ detail: "Dynamic method/static call `" + m[1] + "$var()` — the invoked symbol is attacker-influenced."
52944
53166
  });
52945
53167
  }
52946
53168
  }
52947
- function detectCrlfInjection(code, file, out) {
52948
- const ls = lines2(code);
52949
- const headerRe = /\b(header|setHeader|addHeader|writeHead|appendHeader)\s*\(/g;
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 = headerRe.exec(code)) !== null) {
52952
- const ln = lineNo2(code, m.index);
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 - 3), ln).join(`
53176
+ const scope = ls.slice(Math.max(0, ln - 4), ln).join(`
52955
53177
  `);
52956
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
53178
+ if (!userControlled(l) && !userControlled(scope))
52957
53179
  continue;
52958
53180
  out.push({
52959
53181
  file,
52960
53182
  line: ln,
52961
- type: "crlf_injection",
52962
- category: "CRLF / header injection",
52963
- severity: "medium",
53183
+ type: "dynamic_include",
53184
+ category: "Route confusion (dynamic file include)",
53185
+ severity: "high",
52964
53186
  evidence: l.trim(),
52965
- detail: "User input reaches an HTTP header — CRLF (`%0d%0a`) can split the response, inject headers, or poison cookies. Strip newlines from header values."
53187
+ detail: `\`${m[1]}(…)\` of a variable — potential LFI/RCE when the path is attacker-controlled.`
52966
53188
  });
52967
53189
  }
52968
53190
  }
52969
- function detectPathConfusion(code, file, out) {
52970
- const ls = lines2(code);
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 = fileRe.exec(code)) !== null) {
52974
- const ln = lineNo2(code, m.index);
52975
- const l = ls[ln - 1] ?? "";
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
- if (!SOURCE_TOKENS2.test(l) && !SOURCE_TOKENS2.test(scope))
52979
- continue;
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: "path_confusion",
52987
- category: "Path confusion (unsanitized file path)",
52988
- severity: "medium",
52989
- evidence: l.trim(),
52990
- 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."
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
- // src/guidance.ts
52996
- var SINK_DIRECTIVES = {
52997
- 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.",
52998
- 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).",
52999
- 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.",
53000
- 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.",
53001
- 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://).",
53002
- deserialization: "Deserialization needs a crafted gadget chain — read_tool_manual (ysoserial / PHPGGC) and confirm the entry point is reachable before validating.",
53003
- 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.",
53004
- redirect: "Validate open redirect with strike_verify (marker URL vs control). Confirm the redirect reflects attacker-controlled input.",
53005
- 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.",
53006
- xml_processing: "Validate XXE with strike_verify (external entity marker vs control). Read payload_lookup for xxe payloads.",
53007
- archive_extraction: "Validate the archive/path issue (zip-slip / path traversal) with strike_verify. Confirm the extraction path is attacker-controlled.",
53008
- path_traversal: "Validate path traversal with strike_verify (marker file read vs control). Read payload_lookup for path-traversal payloads.",
53009
- 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.",
53010
- 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.",
53011
- 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."
53012
- };
53013
- function nextSteps(ctx) {
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 extractParams(urls) {
53126
- const params = new Set;
53127
- for (const u of urls) {
53128
- try {
53129
- const q = new URL(u).searchParams;
53130
- for (const k of q.keys())
53131
- params.add(k);
53132
- } catch {}
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
- async function subdomainEnum(domain) {
53137
- const out = new Set;
53138
- try {
53139
- const url = `https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`;
53140
- const r = await httpGet(url);
53141
- if (r.status === 200 && r.body.startsWith("[")) {
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
- async function waybackUrls(domain) {
53156
- const out = new Set;
53157
- const url = `https://web.archive.org/cdx/search/cdx?url=*.${encodeURIComponent(domain)}/*&output=json&fl=original&collapse=urlkey&limit=500`;
53158
- try {
53159
- const controller = new AbortController;
53160
- const t = setTimeout(() => controller.abort(), 60000);
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 COMMON_SUBDOMAINS = [
53176
- "www",
53177
- "mail",
53178
- "ftp",
53179
- "api",
53180
- "dev",
53181
- "staging",
53182
- "test",
53183
- "admin",
53184
- "portal",
53185
- "app",
53186
- "blog",
53187
- "shop",
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
- seen.add(host);
53237
- try {
53238
- const ips = await resolve4(host);
53239
- if (ips.length > 0)
53240
- found.push({ host, ips });
53241
- } catch {}
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
- var COMMON_PORTS = [
53246
- [21, "ftp"],
53247
- [22, "ssh"],
53248
- [25, "smtp"],
53249
- [53, "dns"],
53250
- [80, "http"],
53251
- [110, "pop3"],
53252
- [135, "msrpc"],
53253
- [139, "netbios"],
53254
- [143, "imap"],
53255
- [443, "https"],
53256
- [445, "smb"],
53257
- [993, "imaps"],
53258
- [995, "pop3s"],
53259
- [1433, "mssql"],
53260
- [1521, "oracle"],
53261
- [3306, "mysql"],
53262
- [3389, "rdp"],
53263
- [5432, "postgres"],
53264
- [6379, "redis"],
53265
- [8080, "http-alt"],
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
- socket.once("timeout", () => {
53281
- socket.destroy();
53282
- resolve(false);
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
- socket.once("error", () => {
53285
- socket.destroy();
53286
- resolve(false);
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
- async function scanPorts(host) {
53291
- const results = [];
53292
- for (const [port, service] of COMMON_PORTS) {
53293
- const open = await tcpProbe(host, port);
53294
- if (open) {
53295
- const corr = portCorrelation(port);
53296
- results.push({ port, service, open: true, correlation: corr });
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
- var API_PATHS = [
53302
- "/swagger.json",
53303
- "/swagger-ui.html",
53304
- "/openapi.json",
53305
- "/api-docs",
53306
- "/v2/api-docs",
53307
- "/v3/api-docs",
53308
- "/graphql",
53309
- "/graphiql",
53310
- "/actuator",
53311
- "/actuator/health",
53312
- "/actuator/env",
53313
- "/api/swagger.json",
53314
- "/docs",
53315
- "/redoc"
53316
- ];
53317
- async function discoverApi(base) {
53318
- const out = [];
53319
- for (const p of API_PATHS) {
53320
- const r = await httpGet(`${base}${p}`);
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 extractVersion(headers, body) {
53328
- const out = [];
53329
- const server = headers["server"] ?? headers["x-powered-by"] ?? "";
53330
- const serverVer = /^(nginx|apache|microsoft-iis|openresty|liteSpeed|caddy)\/?([0-9][0-9.\-]*)?/i.exec(server);
53331
- if (serverVer) {
53332
- out.push({ product: serverVer[1].toLowerCase(), version: serverVer[2] ?? "unknown", evidence: server });
53333
- }
53334
- const phpVer = /php\/([0-9][0-9.\-]*)/i.exec(server);
53335
- if (phpVer)
53336
- out.push({ product: "php", version: phpVer[1], evidence: server });
53337
- const wpVer = /content="WordPress\s+([0-9][0-9.]*)/i.exec(body);
53338
- if (wpVer)
53339
- out.push({ product: "wordpress", version: wpVer[1], evidence: `meta generator: WordPress ${wpVer[1]}` });
53340
- const joomlaVer = /Joomla!?\s*([0-9][0-9.]*)/i.exec(body);
53341
- if (joomlaVer)
53342
- out.push({ product: "joomla", version: joomlaVer[1], evidence: `body: Joomla ${joomlaVer[1]}` });
53343
- const drupalVer = /Drupal\s+([0-9][0-9.]*)/i.exec(body);
53344
- if (drupalVer)
53345
- out.push({ product: "drupal", version: drupalVer[1], evidence: `body: Drupal ${drupalVer[1]}` });
53346
- const powered = headers["x-powered-by"] ?? "";
53347
- if (powered && !out.some((o) => powered.includes(o.product))) {
53348
- out.push({ product: powered.split("/")[0].toLowerCase(), version: powered.split("/")[1] ?? "unknown", evidence: powered });
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
- async function liveRecon(target, includeActive = false) {
53365
- const base = normalizeTarget2(target);
53366
- const host = (() => {
53367
- try {
53368
- return new URL(base).hostname;
53369
- } catch {
53370
- return target;
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
- const fingerprint = {
53380
- status: root.status,
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
- const intel = correlateIntel(techs, versions, openPorts);
53425
- const nextSteps = [];
53426
- if (waf.detected)
53427
- nextSteps.push(`WAF detected (${waf.wafs?.map((w) => w.waf).join(", ")}) — consult detect_waf + WAF bypass playbook before exploitation.`);
53428
- for (const t of techs) {
53429
- const c = techCorrelation(t);
53430
- if (c.found)
53431
- nextSteps.push(`Tech '${t}' maps to known vuln classes — read tech_correlation('${t}') and fetch nuclei templates with template_lookup('${t}').`);
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
- for (const v of versions) {
53434
- nextSteps.push(`Version ${v.product}/${v.version} detected — check cve_correlation + nvd_lookup for known CVEs affecting ${v.product} ${v.version}.`);
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
- if (crawl.endpoints.length > 0)
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 (strike_verify per parameter) -> confirmed findings -> inline report.",
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)"),