blitzstrike 1.1.7 → 1.3.0

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