blitzstrike 1.0.41 → 1.0.44

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 +101 -0
  2. package/dist/index.js +2293 -1952
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -30238,1378 +30238,1533 @@ function rejectFinding(finding, status, reason, evidence) {
30238
30238
  };
30239
30239
  }
30240
30240
 
30241
- // src/orchestrator.ts
30242
- var _chainsCache = null;
30243
- function loadChains() {
30244
- if (_chainsCache)
30245
- return _chainsCache;
30246
- try {
30247
- const dir = join6(fileURLToPath3(new URL(".", import.meta.url)), "..");
30248
- const path = join6(dir, "chains.json");
30249
- const raw = JSON.parse(readFileSync6(path, "utf8"));
30250
- _chainsCache = raw.chains ?? [];
30251
- return _chainsCache;
30252
- } catch {
30253
- _chainsCache = [];
30254
- return [];
30255
- }
30241
+ // src/orchestration.ts
30242
+ var STATE_MACHINE = [
30243
+ { state: "scope_check", description: "Validate target against scope + risk policy.", recommended_tools: ["scope_check"], required_evidence: ["scope_decision"], next: ["plan"], confidence: "high" },
30244
+ { state: "plan", description: "Derive the engagement plan from surface + scope.", recommended_tools: ["list_chains"], required_evidence: ["plan_steps"], next: ["recon"], confidence: "high" },
30245
+ { state: "recon", description: "Map the attack surface (endpoints, tech, WAF).", recommended_tools: ["blitz_scan", "active_scan", "fofa_search", "detect_waf"], required_evidence: ["attack_surface"], next: ["analyze"], confidence: "medium" },
30246
+ { state: "analyze", description: "Trace source→sink flows (interprocedural taint).", recommended_tools: ["eagle_eye2", "blitz_scan", "blitz_file"], required_evidence: ["sink_hits", "taint_flow"], next: ["hypothesis"], confidence: "medium" },
30247
+ { state: "hypothesis", description: "Form defensible hypotheses (static finding).", recommended_tools: ["blitz_file", "enrich_scan"], required_evidence: ["static_finding"], next: ["select_chain"], blocking: "live_validation_required", confidence: "high" },
30248
+ { state: "select_chain", description: "Match the hypothesis to an escalation chain.", recommended_tools: ["list_chains", "read_playbook"], required_evidence: ["matched_chain"], next: ["validate"], confidence: "high" },
30249
+ { state: "validate", description: "Verify live with baseline + marker + negative control.", recommended_tools: ["strike_verify", "browser_validate"], required_evidence: ["baseline", "marker_reflection", "negative_control"], next: ["evidence"], confidence: "medium" },
30250
+ { state: "evidence", description: "Attach hashable, provenance-bound evidence.", recommended_tools: ["finding_attach_evidence"], required_evidence: ["evidence_artifact"], next: ["finding"], confidence: "high" },
30251
+ { state: "finding", description: "Confirm the finding (invariants enforced).", recommended_tools: ["confirm_finding"], required_evidence: ["confirmed_finding"], next: ["report"], confidence: "high" },
30252
+ { state: "report", description: "Produce a reproducible report.", recommended_tools: ["report", "security-report"], required_evidence: ["report"], next: [], confidence: "high" }
30253
+ ];
30254
+ function stateDefinition(state) {
30255
+ return STATE_MACHINE.find((s) => s.state === state);
30256
+ }
30257
+ var RISK_POLICY = [
30258
+ { action: "scope_check", risk: "low", requires_scope: false, destructive: false },
30259
+ { action: "blitz_scan", risk: "low", requires_scope: false, destructive: false },
30260
+ { action: "blitz_file", risk: "low", requires_scope: false, destructive: false },
30261
+ { action: "eagle_eye2", risk: "low", requires_scope: false, destructive: false },
30262
+ { action: "enrich_scan", risk: "low", requires_scope: false, destructive: false },
30263
+ { action: "detect_waf", risk: "low", requires_scope: false, destructive: false },
30264
+ { action: "fofa_search", risk: "low", requires_scope: false, destructive: false },
30265
+ { action: "list_chains", risk: "low", requires_scope: false, destructive: false },
30266
+ { action: "read_playbook", risk: "low", requires_scope: false, destructive: false },
30267
+ { action: "active_scan", risk: "medium", requires_scope: true, destructive: false },
30268
+ { action: "browser_validate", risk: "medium", requires_scope: true, destructive: false },
30269
+ { action: "strike_verify", risk: "high", requires_scope: true, destructive: false },
30270
+ { action: "confirm_finding", risk: "high", requires_scope: true, destructive: false }
30271
+ ];
30272
+ function riskOf(action) {
30273
+ return RISK_POLICY.find((r) => r.action === action) ?? null;
30256
30274
  }
30257
- function scopeCheck(target, scope = "", mode = "bug-bounty") {
30258
- let result;
30259
- if (mode === "ctf" || mode === "reverse-engineering" || mode === "offensive") {
30260
- result = { target, allowed: true, enforcement: "off", reason: `mode=${mode} disables scope enforcement` };
30261
- } else if (!scope.trim()) {
30262
- result = { target, allowed: false, enforcement: "strict", reason: "no scope provided; refusing active testing" };
30275
+ function actionPermitted(action, scopeAllowed, mode) {
30276
+ const rule = riskOf(action);
30277
+ if (!rule)
30278
+ return true;
30279
+ if (mode === "ctf" || mode === "offensive" || mode === "reverse-engineering")
30280
+ return true;
30281
+ if (rule.requires_scope && !scopeAllowed)
30282
+ return false;
30283
+ return true;
30284
+ }
30285
+ function buildPlan(scopeAllowed, mode, executedState) {
30286
+ const order = ["scope_check", "plan", "recon", "analyze", "hypothesis", "select_chain", "validate", "evidence", "finding", "report"];
30287
+ const executedIdx = order.indexOf(executedState);
30288
+ return order.map((state, i) => {
30289
+ const def = stateDefinition(state);
30290
+ const tool = def.recommended_tools[0] ?? "";
30291
+ const rule = riskOf(tool);
30292
+ const permitted = actionPermitted(tool, scopeAllowed, mode);
30293
+ const status = i <= executedIdx ? "done" : permitted ? "pending" : "blocked";
30294
+ return {
30295
+ order: i + 1,
30296
+ state,
30297
+ action: def.description,
30298
+ tool,
30299
+ evidence_required: def.required_evidence,
30300
+ risk: rule?.risk ?? "low",
30301
+ permitted,
30302
+ status
30303
+ };
30304
+ });
30305
+ }
30306
+ function computeGuidance(state, scopeAllowed) {
30307
+ const def = stateDefinition(state);
30308
+ const next = def.next[0];
30309
+ const nextDef = next ? stateDefinition(next) : null;
30310
+ const tool = nextDef?.recommended_tools[0] ?? "";
30311
+ const permitted = actionPermitted(tool, scopeAllowed, "bug-bounty");
30312
+ return {
30313
+ state,
30314
+ confidence: def.confidence,
30315
+ blocking_reason: !scopeAllowed && nextDef && riskOf(tool)?.requires_scope ? "scope_required" : def.blocking ?? null,
30316
+ required_evidence: def.required_evidence,
30317
+ recommended_next_action: nextDef?.description ?? "complete",
30318
+ recommended_tool: permitted ? tool : ""
30319
+ };
30320
+ }
30321
+
30322
+ // src/cvss.ts
30323
+ var AV_WEIGHT = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
30324
+ var AC_WEIGHT = { L: 0.77, H: 0.44 };
30325
+ var PR_WEIGHT = {
30326
+ U: { N: 0.85, L: 0.62, H: 0.27 },
30327
+ C: { N: 0.85, L: 0.68, H: 0.5 }
30328
+ };
30329
+ var UI_WEIGHT = { N: 0.85, R: 0.62 };
30330
+ var CIA_WEIGHT = { H: 0.56, L: 0.22, N: 0 };
30331
+ function roundup(value) {
30332
+ return Math.ceil(value * 10) / 10;
30333
+ }
30334
+ function cvssBaseScore(input) {
30335
+ const AV = input.AV ?? "N";
30336
+ const AC = input.AC ?? "L";
30337
+ const PR = input.PR ?? "N";
30338
+ const UI = input.UI ?? "N";
30339
+ const S = input.S ?? "U";
30340
+ const C = input.C ?? "N";
30341
+ const I = input.I ?? "N";
30342
+ const A = input.A ?? "N";
30343
+ const iscBase = 1 - (1 - CIA_WEIGHT[C]) * (1 - CIA_WEIGHT[I]) * (1 - CIA_WEIGHT[A]);
30344
+ let impact;
30345
+ if (S === "U") {
30346
+ impact = 6.42 * iscBase;
30263
30347
  } else {
30264
- const inScope = [];
30265
- const outScope = [];
30266
- for (const raw of scope.split(/[,\n]/)) {
30267
- const line = raw.trim();
30268
- if (!line)
30269
- continue;
30270
- if (line.startsWith("-"))
30271
- outScope.push(line.slice(1).trim());
30272
- else
30273
- inScope.push(line);
30274
- }
30275
- let host = target;
30276
- host = host.replace(/^https?:\/\//, "");
30277
- host = host.split("/")[0].split(":")[0];
30278
- let decided = false;
30279
- result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' not in scope` };
30280
- for (const exc of outScope) {
30281
- if (exc && (host.includes(exc) || host.endsWith("." + exc.replace(/^\./, "")))) {
30282
- result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' is explicitly out of scope (${exc})` };
30283
- decided = true;
30284
- break;
30285
- }
30286
- }
30287
- if (!decided) {
30288
- for (const allowed of inScope) {
30289
- const a = allowed.replace(/\*/g, "");
30290
- if (!a)
30291
- continue;
30292
- if (host.includes(a) || host.endsWith("." + a.replace(/^\./, ""))) {
30293
- result = { target, allowed: true, enforcement: "strict", reason: `matched in-scope pattern '${allowed}'` };
30294
- decided = true;
30295
- break;
30296
- }
30297
- }
30298
- }
30348
+ impact = 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15);
30299
30349
  }
30300
- recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
30301
- return result;
30350
+ const exploitability = 8.22 * AV_WEIGHT[AV] * AC_WEIGHT[AC] * PR_WEIGHT[S][PR] * UI_WEIGHT[UI];
30351
+ if (impact <= 0)
30352
+ return 0;
30353
+ if (S === "U") {
30354
+ return roundup(Math.min(impact + exploitability, 10));
30355
+ }
30356
+ return roundup(Math.min(1.08 * (impact + exploitability), 10));
30302
30357
  }
30303
- function classifySink(sink) {
30304
- if (/(move_uploaded_file|file_put_contents|fwrite)/.test(sink))
30305
- return "upload_weak_validation";
30306
- if (/unserialize/.test(sink))
30307
- return "unserialize_user_input";
30308
- if (/(exec\(|system\(|shell_exec|passthru|popen|proc_open)/.test(sink))
30309
- return "command_injection";
30310
- if (/(include|require)/.test(sink))
30311
- return "lfi_unvalidated_include";
30312
- if (/extract\(/.test(sink))
30313
- return "extract_without_exstr_skip";
30314
- if (/(\$wpdb|->query)/.test(sink))
30315
- return "sql_injection";
30316
- if (/(wp_remote_get|wp_remote_post|file_get_contents)/.test(sink))
30317
- return "ssrf_unvalidated_url";
30318
- if (/(eval\(|assert\(|create_function)/.test(sink))
30319
- return "unsafe_template_render";
30320
- return "";
30358
+ function cvssVector(input) {
30359
+ const order = [
30360
+ ["AV", input.AV ?? "N"],
30361
+ ["AC", input.AC ?? "L"],
30362
+ ["PR", input.PR ?? "N"],
30363
+ ["UI", input.UI ?? "N"],
30364
+ ["S", input.S ?? "U"],
30365
+ ["C", input.C ?? "N"],
30366
+ ["I", input.I ?? "N"],
30367
+ ["A", input.A ?? "N"]
30368
+ ];
30369
+ return "CVSS:3.1/" + order.map(([k, v]) => `${k}:${v}`).join("/");
30321
30370
  }
30322
- function enrichScan(path, maxFiles = 2000) {
30323
- const expanded = path.replace(/^~/, process.env.HOME ?? "~");
30324
- const files = iterSourceFiles(expanded, maxFiles);
30325
- const hits = [];
30326
- const sinkKinds = new Set;
30327
- for (const p of files) {
30328
- const r = scanFile(p);
30329
- for (const e of r.endpoints) {
30330
- hits.push({ type: "endpoint", file: r.file, line: e.line, detail: e.hook });
30331
- }
30332
- for (const s of r.sinks) {
30333
- hits.push({ type: "sink", file: r.file, line: s.line, detail: `${s.sink} -> ${s.class}` });
30334
- const kind = classifySink(s.sink);
30335
- if (kind)
30336
- sinkKinds.add(kind);
30371
+ function cvssSeverity(score) {
30372
+ if (score === 0)
30373
+ return "none";
30374
+ if (score < 4)
30375
+ return "low";
30376
+ if (score < 7)
30377
+ return "medium";
30378
+ if (score < 9)
30379
+ return "high";
30380
+ return "critical";
30381
+ }
30382
+ function cvssAssess(input) {
30383
+ const score = cvssBaseScore(input);
30384
+ return { score, vector: cvssVector(input), severity: cvssSeverity(score) };
30385
+ }
30386
+
30387
+ // src/dedup.ts
30388
+ function rootCauseSignature(finding) {
30389
+ const sinkType = (finding.sink?.type ?? "unknown").toLowerCase();
30390
+ const sourceType = (finding.source?.type ?? "unknown").toLowerCase();
30391
+ const cwe = (finding.classification?.cwe ?? "").toUpperCase();
30392
+ return [sinkType, sourceType, cwe].filter(Boolean).join("|");
30393
+ }
30394
+ function dedupFindings(findings) {
30395
+ const groups = new Map;
30396
+ for (const f of findings) {
30397
+ const sig = rootCauseSignature(f);
30398
+ const existing = groups.get(sig);
30399
+ if (existing) {
30400
+ existing.count += 1;
30401
+ existing.findings.push(f);
30402
+ } else {
30403
+ groups.set(sig, { signature: sig, count: 1, representative: f, findings: [f] });
30337
30404
  }
30338
30405
  }
30339
- const chains = loadChains();
30340
- const matched = [];
30341
- for (const c of chains) {
30342
- if ((c.required_findings ?? []).some((rf) => sinkKinds.has(rf))) {
30343
- matched.push({ chain_id: c.id, name: c.name, severity: c.severity, steps: c.steps.length });
30344
- }
30406
+ return [...groups.values()];
30407
+ }
30408
+ function uniqueFindings(findings) {
30409
+ return dedupFindings(findings).map((g) => g.representative);
30410
+ }
30411
+
30412
+ // src/report.ts
30413
+ function summarize(findings) {
30414
+ const byStatus = {};
30415
+ const bySeverity = {};
30416
+ let evidenceArtifacts = 0;
30417
+ for (const f of findings) {
30418
+ byStatus[f.status] = (byStatus[f.status] ?? 0) + 1;
30419
+ const sev = f.classification?.severity ?? "informational";
30420
+ bySeverity[sev] = (bySeverity[sev] ?? 0) + 1;
30421
+ evidenceArtifacts += f.evidence?.reduce((n, e) => n + (e.artifacts?.length ?? 0), 0) ?? 0;
30345
30422
  }
30423
+ const groups = dedupFindings(findings);
30346
30424
  return {
30347
- root: expanded,
30348
- files_scanned: files.length,
30349
- hits: hits.slice(0, 2000),
30350
- sink_signals: [...sinkKinds].sort(),
30351
- matched_chains: matched
30425
+ total_findings: findings.length,
30426
+ unique_root_causes: groups.length,
30427
+ by_status: byStatus,
30428
+ by_severity: bySeverity,
30429
+ confirmed: byStatus["confirmed"] ?? 0,
30430
+ false_positives: byStatus["false_positive"] ?? 0,
30431
+ evidence_artifacts: evidenceArtifacts
30352
30432
  };
30353
30433
  }
30354
- function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000, remember = true) {
30355
- const report = {
30356
- target,
30357
- mode,
30358
- tiers: {},
30359
- findings: [],
30360
- scope: null,
30361
- memory: { captured: 0 }
30362
- };
30363
- const isPath = (() => {
30364
- try {
30365
- return existsSync4(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
30366
- } catch {
30367
- return false;
30368
- }
30369
- })();
30370
- if (isPath) {
30371
- report.scope = { target, allowed: true, enforcement: "source-audit", reason: "local source path — no network scope gate" };
30372
- } else {
30373
- const sc = scopeCheck(target, scope, mode);
30374
- report.scope = sc;
30375
- if (!sc.allowed) {
30376
- report.status = "BLOCKED";
30377
- report.reason = sc.reason;
30378
- return report;
30379
- }
30380
- }
30381
- const blitz = enrichScan(target, maxFiles);
30382
- report.tiers = {
30383
- blitz: {
30384
- files_scanned: blitz.files_scanned,
30385
- hits: blitz.hits.length,
30386
- sink_signals: blitz.sink_signals,
30387
- matched_chains: blitz.matched_chains
30388
- }
30389
- };
30390
- const chains = loadChains();
30391
- const chainById = new Map(chains.map((c) => [c.id, c]));
30392
- const eagle = [];
30393
- for (const mc of blitz.matched_chains.slice(0, 10)) {
30394
- const chain = chainById.get(mc.chain_id);
30395
- if (!chain)
30396
- continue;
30397
- eagle.push({
30398
- chain_id: chain.id,
30399
- name: chain.name,
30400
- severity: chain.severity,
30401
- prerequisites: chain.required_findings,
30402
- recommended_tools: chain.tools ?? [],
30403
- tool_manuals: (chain.tools ?? []).map((t) => {
30404
- const m = findManual(t);
30405
- return m.found ? { tool: t, manual_name: m.name, preview: m.content.slice(0, 600) } : { tool: t, manual_name: null, preview: null };
30406
- }),
30407
- steps: chain.steps.map((s) => ({
30408
- order: s.order,
30409
- action: s.action,
30410
- tool_hint: s.tool_hint,
30411
- success_criteria: s.success_criteria,
30412
- invariant_check: s.invariant_check,
30413
- negative_control: s.negative_control
30414
- }))
30415
- });
30434
+ function severityLabel(sev) {
30435
+ const s = sev.toUpperCase();
30436
+ if (s === "CRITICAL")
30437
+ return "\uD83D\uDD34 CRITICAL";
30438
+ if (s === "HIGH")
30439
+ return "\uD83D\uDFE0 HIGH";
30440
+ if (s === "MEDIUM")
30441
+ return "\uD83D\uDFE1 MEDIUM";
30442
+ if (s === "LOW")
30443
+ return "\uD83D\uDD35 LOW";
30444
+ return "⚪ INFORMATIONAL";
30445
+ }
30446
+ function reportMarkdown(findings, opts = {}) {
30447
+ const summary = summarize(findings);
30448
+ const lines = [];
30449
+ lines.push(`# ${opts.title ?? "Blitz Strike — Security Assessment Report"}`);
30450
+ lines.push("");
30451
+ lines.push(`- Scope: ${opts.scope ?? "(unset)"}`);
30452
+ lines.push(`- Blitz Strike version: ${opts.version ?? "unknown"}`);
30453
+ if (opts.include_timestamp)
30454
+ lines.push(`- Generated: ${new Date().toISOString()}`);
30455
+ lines.push("");
30456
+ lines.push("## Summary");
30457
+ lines.push("");
30458
+ lines.push(`| Metric | Value |`);
30459
+ lines.push(`|--------|-------|`);
30460
+ lines.push(`| Total findings | ${summary.total_findings} |`);
30461
+ lines.push(`| Unique root causes | ${summary.unique_root_causes} |`);
30462
+ lines.push(`| Confirmed | ${summary.confirmed} |`);
30463
+ lines.push(`| False positives | ${summary.false_positives} |`);
30464
+ lines.push(`| Evidence artifacts | ${summary.evidence_artifacts} |`);
30465
+ lines.push("");
30466
+ lines.push("### By status");
30467
+ lines.push("");
30468
+ for (const [status, n] of Object.entries(summary.by_status).sort()) {
30469
+ lines.push(`- ${status}: ${n}`);
30416
30470
  }
30417
- report.tiers = { ...report.tiers, eagle_eye: { traced_chains: eagle } };
30418
- const rank = { critical: 0, high: 1, medium: 2, low: 3 };
30419
- const matchedSorted = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
30420
- const findings = matchedSorted.map((f) => {
30421
- const chain = chainById.get(f.chain_id);
30422
- const severity = f.severity ?? "medium";
30423
- const finding = makeFinding({
30424
- title: f.name,
30425
- target: { type: "source", path: target },
30426
- severity,
30427
- source: { type: "static_sink_signal", name: f.chain_id },
30428
- sink: { type: f.chain_id, symbol: f.name },
30429
- chainId: f.chain_id,
30430
- chainName: f.name,
30431
- status: "hypothesis"
30432
- });
30433
- const ev = makeEvidence({
30434
- type: "sink_location",
30435
- description: `Static sink signal matched escalation chain '${f.chain_id}' (${severity}). Hypothesis only — not yet validated.`,
30436
- sink: { chain_id: f.chain_id, name: f.name, severity },
30437
- artifacts: [{ name: "chain_match", kind: "json", content: JSON.stringify({ chain_id: f.chain_id, severity, steps: chain?.steps.length ?? 0 }) }]
30438
- });
30439
- finding.evidence.push(ev);
30440
- return finding;
30441
- });
30442
- report.findings = findings.map((f) => ({
30443
- id: f.id,
30444
- status: f.status,
30445
- title: f.title,
30446
- severity: f.classification.severity,
30447
- confidence: f.confidence,
30448
- confidence_level: f.confidence_level,
30449
- chain_id: f.chain.id,
30450
- evidence_count: f.evidence.length
30451
- }));
30452
- report.status = "COMPLETE";
30453
- 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.";
30454
- if (remember) {
30455
- let captured = 0;
30456
- for (const mc of blitz.matched_chains) {
30457
- const chain = chainById.get(mc.chain_id);
30458
- const content = `Chain '${mc.chain_id}' (${mc.severity}) matched on target '${target}'. ` + `Detected sink signals: ${blitz.sink_signals.join(", ")}. ` + `Prerequisites: ${(chain?.required_findings ?? []).join(", ")}.`;
30459
- if (rememberIfAbsent(mc.chain_id, content, "pattern", [mc.severity, "chain"], "engagement", false)) {
30460
- captured++;
30461
- }
30471
+ lines.push("");
30472
+ lines.push("### By severity");
30473
+ lines.push("");
30474
+ for (const [sev, n] of Object.entries(summary.by_severity).sort()) {
30475
+ lines.push(`- ${sev}: ${n}`);
30476
+ }
30477
+ lines.push("");
30478
+ lines.push("## Findings");
30479
+ lines.push("");
30480
+ if (findings.length === 0) {
30481
+ lines.push("_No findings._");
30482
+ return lines.join(`
30483
+ `) + `
30484
+ `;
30485
+ }
30486
+ for (const f of findings) {
30487
+ lines.push(`### ${f.id} — ${f.title} [${severityLabel(f.classification?.severity ?? "informational")}]`);
30488
+ lines.push("");
30489
+ lines.push(`- **Status**: \`${f.status}\``);
30490
+ lines.push(`- **Confidence**: ${f.confidence} (${f.confidence_level})`);
30491
+ if (f.classification?.cwe)
30492
+ lines.push(`- **CWE**: ${f.classification.cwe}`);
30493
+ if (f.classification?.cvss_score != null)
30494
+ lines.push(`- **CVSS**: ${f.classification.cvss_score} (${f.classification.cvss_vector ?? ""}) — ${cvssSeverity(f.classification.cvss_score)}`);
30495
+ lines.push(`- **Source**: ${f.source?.type} \`${f.source?.name ?? ""}\``);
30496
+ lines.push(`- **Sink**: ${f.sink?.type}${f.sink?.symbol ? ` \`${f.sink.symbol}\`` : ""}`);
30497
+ if (f.target?.host || f.target?.endpoint)
30498
+ lines.push(`- **Target**: ${[f.target.host, f.target.endpoint].filter(Boolean).join("")}`);
30499
+ lines.push(`- **Evidence**: ${f.evidence?.length ?? 0} record(s)`);
30500
+ if (f.remediation && Object.keys(f.remediation).length) {
30501
+ lines.push(`- **Note**: ${JSON.stringify(f.remediation)}`);
30462
30502
  }
30463
- report.memory = { captured, deduped: blitz.matched_chains.length - captured };
30503
+ lines.push("");
30464
30504
  }
30465
- return report;
30505
+ return lines.join(`
30506
+ `) + `
30507
+ `;
30466
30508
  }
30467
- function listChains() {
30468
- const chains = loadChains();
30509
+ function reportJson(findings, opts = {}) {
30510
+ const summary = summarize(findings);
30511
+ const payload = {
30512
+ title: opts.title ?? "Blitz Strike — Security Assessment Report",
30513
+ scope: opts.scope ?? null,
30514
+ version: opts.version ?? null,
30515
+ summary,
30516
+ findings,
30517
+ integrity: sha256(JSON.stringify({ summary, findings }))
30518
+ };
30519
+ return JSON.stringify(payload, null, 2);
30520
+ }
30521
+
30522
+ // src/sarif.ts
30523
+ var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
30524
+ function sarifLevel(severity) {
30525
+ const s = severity.toLowerCase();
30526
+ if (s === "critical" || s === "high")
30527
+ return "error";
30528
+ if (s === "medium")
30529
+ return "warning";
30530
+ return "note";
30531
+ }
30532
+ function toSarif(findings, opts = {}) {
30533
+ const tool = opts.tool ?? "Blitz Strike";
30534
+ const uri = opts.uri ?? "https://github.com/shinthink/blitzstrike";
30535
+ const ruleIds = [...new Set(findings.map((f) => f.ruleId))];
30536
+ const rules = ruleIds.map((id) => ({
30537
+ id,
30538
+ name: id,
30539
+ shortDescription: { text: id }
30540
+ }));
30541
+ const results = findings.map((f) => ({
30542
+ ruleId: f.ruleId,
30543
+ level: f.level,
30544
+ message: { text: f.message },
30545
+ locations: [
30546
+ {
30547
+ physicalLocation: {
30548
+ artifactLocation: { uri: f.file },
30549
+ region: { startLine: Math.max(1, f.line) }
30550
+ }
30551
+ }
30552
+ ]
30553
+ }));
30469
30554
  return {
30470
- total: chains.length,
30471
- chains: chains.map((c) => ({
30472
- id: c.id,
30473
- name: c.name,
30474
- severity: c.severity,
30475
- steps: c.steps.length,
30476
- triggers: c.triggers
30477
- }))
30555
+ version: "2.1.0",
30556
+ $schema: SARIF_SCHEMA,
30557
+ runs: [
30558
+ {
30559
+ tool: {
30560
+ driver: {
30561
+ name: tool,
30562
+ version: VERSION,
30563
+ informationUri: uri,
30564
+ rules
30565
+ }
30566
+ },
30567
+ results
30568
+ }
30569
+ ]
30478
30570
  };
30479
30571
  }
30480
30572
 
30481
- // src/catalog.ts
30482
- import { readFileSync as readFileSync7, readdirSync as readdirSync2 } from "node:fs";
30483
- import { join as join7 } from "node:path";
30484
- import { fileURLToPath as fileURLToPath4 } from "node:url";
30485
- import { spawnSync } from "node:child_process";
30486
- var ROOT2 = join7(fileURLToPath4(new URL(".", import.meta.url)), "..");
30487
- var _toolsCache = null;
30488
- var _skillsCache = null;
30489
- function loadTools() {
30490
- if (_toolsCache)
30491
- return _toolsCache;
30573
+ // src/strike.ts
30574
+ var DEFAULT_TIMEOUT_MS = 15000;
30575
+ async function httpRequest(url, method, data, headers, timeoutMs = DEFAULT_TIMEOUT_MS) {
30576
+ const controller = new AbortController;
30577
+ const t = setTimeout(() => controller.abort(), timeoutMs);
30492
30578
  try {
30493
- const raw = JSON.parse(readFileSync7(join7(ROOT2, "tools-catalog.json"), "utf8"));
30494
- _toolsCache = raw.tools ?? [];
30495
- } catch {
30496
- _toolsCache = [];
30579
+ const res = await fetch(url, {
30580
+ method,
30581
+ body: method === "POST" ? data : undefined,
30582
+ headers: { "User-Agent": "blitzstrike/1.0", ...headers ?? {} },
30583
+ signal: controller.signal,
30584
+ redirect: "follow"
30585
+ });
30586
+ const body = await res.text();
30587
+ const hdrs = {};
30588
+ res.headers.forEach((v, k) => {
30589
+ hdrs[k] = v;
30590
+ });
30591
+ return { status: res.status, body: body.slice(0, 20000), headers: hdrs };
30592
+ } catch (e) {
30593
+ return { status: 0, body: String(e), headers: {} };
30594
+ } finally {
30595
+ clearTimeout(t);
30497
30596
  }
30498
- return _toolsCache;
30499
30597
  }
30500
- function loadSkills() {
30501
- if (_skillsCache)
30502
- return _skillsCache;
30503
- const dir = join7(ROOT2, "skills");
30504
- const out = [];
30505
- try {
30506
- for (const f of readdirSync2(dir)) {
30507
- if (!f.endsWith(".md"))
30508
- continue;
30509
- const p = join7(dir, f);
30510
- const text = readFileSync7(p, "utf8");
30511
- const descMatch = text.match(/^description:\s*(.+)$/m);
30512
- out.push({
30513
- name: f.replace(/\.md$/, ""),
30514
- file: f,
30515
- description: (descMatch?.[1] ?? "").trim().slice(0, 200),
30516
- size: text.length
30517
- });
30518
- }
30519
- } catch {}
30520
- _skillsCache = out;
30521
- return out;
30598
+ function defaultMarker() {
30599
+ return `BS${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
30522
30600
  }
30523
- function toolLookup(name) {
30524
- const tools = loadTools();
30525
- const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
30526
- if (!t) {
30527
- return {
30528
- found: false,
30529
- query: name,
30530
- suggestions: tools.filter((x) => x.name.includes(name.toLowerCase()) || name.toLowerCase().includes(x.name)).slice(0, 5).map((x) => x.name)
30531
- };
30532
- }
30533
- const manual = manualForTool(t.name);
30534
- const result = { found: true, tool: t };
30535
- if (manual && manual.content) {
30536
- result.manual = {
30537
- name: manual.name,
30538
- category: manual.category,
30539
- preview: manual.content.slice(0, 1500),
30540
- full_length: manual.content.length
30541
- };
30542
- } else {
30543
- result.manual = null;
30544
- }
30545
- return result;
30601
+ function inject(target, value, param) {
30602
+ const placeholder = "{{MARKER}}";
30603
+ if (target.includes(placeholder))
30604
+ return target.split(placeholder).join(value);
30605
+ if (target.includes("?"))
30606
+ return `${target}&${param}=${encodeURIComponent(value)}`;
30607
+ return `${target}?${param}=${encodeURIComponent(value)}`;
30546
30608
  }
30547
- function listTools() {
30548
- const tools = loadTools();
30549
- const byCat = {};
30550
- for (const t of tools) {
30551
- (byCat[t.category] ??= []).push(t.name);
30609
+ async function strikeVerify(input) {
30610
+ const method = input.method ?? "GET";
30611
+ const param = input.param ?? "q";
30612
+ const marker = input.marker ?? defaultMarker();
30613
+ const control = input.control ?? defaultMarker();
30614
+ const baseUrl = input.url;
30615
+ const timeoutMs = input.timeout ?? DEFAULT_TIMEOUT_MS;
30616
+ const baselineTarget = inject(baseUrl, "", param).replace(/[?&]q=$/, "").replace(/[?&]$/, "");
30617
+ const markerTarget = inject(baseUrl, marker, param);
30618
+ const controlTarget = inject(baseUrl, control, param);
30619
+ const baseline = await httpRequest(baselineTarget, method, input.data, input.headers, timeoutMs);
30620
+ const markerResp = await httpRequest(markerTarget, method, input.data, input.headers, timeoutMs);
30621
+ const controlResp = await httpRequest(controlTarget, method, input.data, input.headers, timeoutMs);
30622
+ if (baseline.status === 0 || markerResp.status === 0) {
30623
+ return {
30624
+ status: "blocked",
30625
+ marker_reflected: false,
30626
+ control_reflected: false,
30627
+ baseline: { status: baseline.status, body_preview: redactSecrets(baseline.body.slice(0, 400)), body_hash: sha256(baseline.body) },
30628
+ marker_response: { status: markerResp.status, body_preview: redactSecrets(markerResp.body.slice(0, 400)), body_hash: sha256(markerResp.body) },
30629
+ control_response: { status: controlResp.status, body_preview: redactSecrets(controlResp.body.slice(0, 400)), body_hash: sha256(controlResp.body) },
30630
+ reason: "request failed or target unreachable — cannot validate",
30631
+ evidence: []
30632
+ };
30552
30633
  }
30553
- return { total: tools.length, categories: byCat, tools: tools.map((t) => ({ name: t.name, category: t.category, description: t.description })) };
30554
- }
30555
- function skillLookup(topic) {
30556
- const skills = loadSkills();
30557
- const q = topic.toLowerCase();
30558
- const scored = skills.map((s) => {
30559
- const hay = `${s.name} ${s.description}`.toLowerCase();
30560
- const nameHit = s.name.includes(q);
30561
- const descHit = s.description.includes(q);
30562
- const wordHit = q.split(/\s+/).filter((w) => w && hay.includes(w)).length;
30563
- return { s, score: (nameHit ? 10 : 0) + (descHit ? 5 : 0) + wordHit * 2 };
30564
- }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
30565
- if (scored.length === 0) {
30566
- return { found: false, query: topic, suggestions: skills.map((s) => s.name).slice(0, 10) };
30634
+ const markerReflected = markerResp.body.includes(marker);
30635
+ const controlReflected = controlResp.body.includes(control);
30636
+ let status;
30637
+ let reason;
30638
+ if (markerReflected && !controlReflected) {
30639
+ status = "confirmed";
30640
+ reason = "marker reflected; negative control did not — the sink reflects attacker-controlled input (real finding)";
30641
+ } else if (markerReflected && controlReflected) {
30642
+ status = "false_positive";
30643
+ reason = "marker AND negative control both reflected — behaviour is indistinguishable from benign reflection (likely false positive)";
30644
+ } else if (!markerReflected && !controlReflected) {
30645
+ status = "unconfirmed";
30646
+ reason = "marker not reflected — reachability/exploitability could not be demonstrated live";
30647
+ } else {
30648
+ status = "unconfirmed";
30649
+ reason = "negative control reflected but marker did not — inconsistent, requires deeper analysis";
30567
30650
  }
30651
+ const evidence = [
30652
+ {
30653
+ type: "baseline_comparison",
30654
+ description: `Baseline response (status ${baseline.status}) for ${redactSecrets(baselineTarget)}`,
30655
+ artifacts: [{ name: "baseline", kind: "http_response", content: `${baseline.status}
30656
+ ${baseline.body}` }]
30657
+ },
30658
+ {
30659
+ type: "validation_result",
30660
+ description: `Marker '${redactSecrets(marker)}' reflected=${markerReflected}; control reflected=${controlReflected}`,
30661
+ artifacts: [{ name: "marker_response", kind: "http_response", content: `${markerResp.status}
30662
+ ${markerResp.body}` }]
30663
+ },
30664
+ {
30665
+ type: "negative_control",
30666
+ description: `Negative control '${redactSecrets(control)}' reflected=${controlReflected}`,
30667
+ artifacts: [{ name: "control_response", kind: "http_response", content: `${controlResp.status}
30668
+ ${controlResp.body}` }]
30669
+ }
30670
+ ];
30568
30671
  return {
30569
- found: true,
30570
- matches: scored.slice(0, 5).map((x) => ({ name: x.s.name, description: x.s.description, file: x.s.file }))
30672
+ status,
30673
+ marker_reflected: markerReflected,
30674
+ control_reflected: controlReflected,
30675
+ baseline: { status: baseline.status, body_preview: redactSecrets(baseline.body.slice(0, 400)), body_hash: sha256(baseline.body) },
30676
+ marker_response: { status: markerResp.status, body_preview: redactSecrets(markerResp.body.slice(0, 400)), body_hash: sha256(markerResp.body) },
30677
+ control_response: { status: controlResp.status, body_preview: redactSecrets(controlResp.body.slice(0, 400)), body_hash: sha256(controlResp.body) },
30678
+ reason,
30679
+ evidence
30571
30680
  };
30572
30681
  }
30573
- function listSkills() {
30574
- const skills = loadSkills();
30575
- return { total: skills.length, skills: skills.map((s) => ({ name: s.name, description: s.description })) };
30576
- }
30577
- function readSkill(name) {
30578
- const skills = loadSkills();
30579
- const s = skills.find((x) => x.name.toLowerCase() === name.toLowerCase());
30580
- if (!s)
30581
- return { found: false, query: name };
30582
- try {
30583
- const text = readFileSync7(join7(ROOT2, "skills", s.file), "utf8");
30584
- return { found: true, name: s.name, content: text.slice(0, 12000) };
30585
- } catch {
30586
- return { found: false, query: name, error: "unreadable" };
30587
- }
30588
- }
30589
- function ensureTool(name) {
30590
- const tools = loadTools();
30591
- const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
30592
- if (!t) {
30593
- return { found: false, query: name, suggestions: tools.slice(0, 5).map((x) => x.name) };
30594
- }
30595
- const check = spawnSync(t.check_installed.command, { shell: true, timeout: 15000 });
30596
- if (check.status !== 127 && check.error === undefined) {
30597
- return { name: t.name, installed: true, action: "none", note: "already installed" };
30682
+ function resolveFinding(finding, verdict) {
30683
+ let f = finding;
30684
+ if (f.status === "hypothesis") {
30685
+ f = transition(f, "validating");
30598
30686
  }
30599
- const platform = process.platform;
30600
- const cmd = t.install[platform] ?? t.install.linux;
30601
- if (!cmd) {
30602
- return { name: t.name, installed: false, action: "none", note: `no install command for ${platform}` };
30687
+ switch (verdict.status) {
30688
+ case "confirmed":
30689
+ return confirmFinding(f, {
30690
+ evidence: verdict.evidence,
30691
+ negative_control: true,
30692
+ baseline: true
30693
+ });
30694
+ case "false_positive":
30695
+ return rejectFinding(f, "false_positive", verdict.reason, verdict.evidence);
30696
+ case "blocked":
30697
+ return rejectFinding(f, "blocked", verdict.reason, verdict.evidence);
30698
+ case "unconfirmed":
30699
+ case "likely":
30700
+ default: {
30701
+ return {
30702
+ ...f,
30703
+ validation: { performed: true, status: verdict.status === "likely" ? "likely" : "unconfirmed", negative_control: verdict.control_reflected === false, baseline: true },
30704
+ remediation: { ...f.remediation, note: verdict.reason },
30705
+ timestamps: { ...f.timestamps, updated: new Date().toISOString() }
30706
+ };
30707
+ }
30603
30708
  }
30604
- const r = spawnSync(cmd, { shell: true, timeout: 120000 });
30605
- const ok = r.status === 0;
30606
- return {
30607
- name: t.name,
30608
- installed: ok,
30609
- action: "install",
30610
- command: cmd,
30611
- output: (r.stdout?.toString() ?? "").slice(-400) || (r.stderr?.toString() ?? "").slice(-400)
30612
- };
30613
30709
  }
30614
30710
 
30615
- // src/intel.ts
30616
- import { readFileSync as readFileSync8, existsSync as existsSync5 } from "node:fs";
30617
- import { join as join8 } from "node:path";
30618
- import { fileURLToPath as fileURLToPath5 } from "node:url";
30619
- import { readdirSync as readdirSync3 } from "node:fs";
30620
- import { homedir as homedir3 } from "node:os";
30621
- var ROOT3 = join8(fileURLToPath5(new URL(".", import.meta.url)), "..");
30622
- var INTEL = join8(ROOT3, "intelligence");
30623
- function loadJson(name) {
30624
- const p = join8(INTEL, `${name}.json`);
30625
- if (!existsSync5(p))
30626
- return null;
30711
+ // src/orchestrator.ts
30712
+ var _chainsCache = null;
30713
+ function loadChains() {
30714
+ if (_chainsCache)
30715
+ return _chainsCache;
30627
30716
  try {
30628
- return JSON.parse(readFileSync8(p, "utf8"));
30717
+ const dir = join6(fileURLToPath3(new URL(".", import.meta.url)), "..");
30718
+ const path = join6(dir, "chains.json");
30719
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
30720
+ _chainsCache = raw.chains ?? [];
30721
+ return _chainsCache;
30629
30722
  } catch {
30630
- return null;
30723
+ _chainsCache = [];
30724
+ return [];
30631
30725
  }
30632
30726
  }
30633
- function detectWaf(headers, body = "") {
30634
- const sigs = loadJson("waf_signatures");
30635
- if (!sigs)
30636
- return { detected: false, wafs: [], error: "waf_signatures.json not loaded" };
30637
- const headerSigs = sigs.header_signatures ?? [];
30638
- const bodySigs = sigs.body_signatures ?? [];
30639
- const matches = [];
30640
- const headerKeys = Object.keys(headers).map((k) => k.toLowerCase());
30641
- for (const s of headerSigs) {
30642
- const h = (s.header ?? "").toLowerCase();
30643
- const pattern = (s.pattern ?? "").toLowerCase();
30644
- if (!h)
30645
- continue;
30646
- if (pattern) {
30647
- const val = (headers[h] ?? headers[s.header ?? ""] ?? "").toLowerCase();
30648
- if (val.includes(pattern)) {
30649
- matches.push({ waf: s.waf, header: s.header, pattern: s.pattern, confidence: s.confidence, via: "header-value" });
30650
- }
30651
- } else {
30652
- if (headerKeys.includes(h)) {
30653
- matches.push({ waf: s.waf, header: s.header, confidence: s.confidence, via: "header-name" });
30727
+ function scopeCheck(target, scope = "", mode = "bug-bounty") {
30728
+ let result;
30729
+ if (mode === "ctf" || mode === "reverse-engineering" || mode === "offensive") {
30730
+ result = { target, allowed: true, enforcement: "off", reason: `mode=${mode} disables scope enforcement` };
30731
+ } else if (!scope.trim()) {
30732
+ result = { target, allowed: false, enforcement: "strict", reason: "no scope provided; refusing active testing" };
30733
+ } else {
30734
+ const inScope = [];
30735
+ const outScope = [];
30736
+ for (const raw of scope.split(/[,\n]/)) {
30737
+ const line = raw.trim();
30738
+ if (!line)
30739
+ continue;
30740
+ if (line.startsWith("-"))
30741
+ outScope.push(line.slice(1).trim());
30742
+ else
30743
+ inScope.push(line);
30744
+ }
30745
+ let host = target;
30746
+ host = host.replace(/^https?:\/\//, "");
30747
+ host = host.split("/")[0].split(":")[0];
30748
+ let decided = false;
30749
+ result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' not in scope` };
30750
+ for (const exc of outScope) {
30751
+ if (exc && (host.includes(exc) || host.endsWith("." + exc.replace(/^\./, "")))) {
30752
+ result = { target, allowed: false, enforcement: "strict", reason: `target '${host}' is explicitly out of scope (${exc})` };
30753
+ decided = true;
30754
+ break;
30654
30755
  }
30655
30756
  }
30656
- }
30657
- for (const s of bodySigs) {
30658
- const pattern = (s.pattern ?? "").toLowerCase();
30659
- if (pattern && body.toLowerCase().includes(pattern)) {
30660
- matches.push({ waf: s.waf, pattern: s.pattern, confidence: s.confidence, via: "body" });
30757
+ if (!decided) {
30758
+ for (const allowed of inScope) {
30759
+ const a = allowed.replace(/\*/g, "");
30760
+ if (!a)
30761
+ continue;
30762
+ if (host.includes(a) || host.endsWith("." + a.replace(/^\./, ""))) {
30763
+ result = { target, allowed: true, enforcement: "strict", reason: `matched in-scope pattern '${allowed}'` };
30764
+ decided = true;
30765
+ break;
30766
+ }
30767
+ }
30661
30768
  }
30662
30769
  }
30663
- const byWaf = new Map;
30664
- for (const m of matches) {
30665
- const key = m.waf;
30666
- const prev = byWaf.get(key);
30667
- if (!prev || m.confidence > prev.confidence)
30668
- byWaf.set(key, m);
30770
+ recordAudit("scope_checked", { tool: "scope", target, result: result.allowed ? "allowed" : "denied" });
30771
+ return result;
30772
+ }
30773
+ function classifySink(sink) {
30774
+ if (/(move_uploaded_file|file_put_contents|fwrite)/.test(sink))
30775
+ return "upload_weak_validation";
30776
+ if (/unserialize/.test(sink))
30777
+ return "unserialize_user_input";
30778
+ if (/(exec\(|system\(|shell_exec|passthru|popen|proc_open)/.test(sink))
30779
+ return "command_injection";
30780
+ if (/(include|require)/.test(sink))
30781
+ return "lfi_unvalidated_include";
30782
+ if (/extract\(/.test(sink))
30783
+ return "extract_without_exstr_skip";
30784
+ if (/(\$wpdb|->query)/.test(sink))
30785
+ return "sql_injection";
30786
+ if (/(wp_remote_get|wp_remote_post|file_get_contents)/.test(sink))
30787
+ return "ssrf_unvalidated_url";
30788
+ if (/(eval\(|assert\(|create_function)/.test(sink))
30789
+ return "unsafe_template_render";
30790
+ return "";
30791
+ }
30792
+ function enrichScan(path, maxFiles = 2000) {
30793
+ const expanded = path.replace(/^~/, process.env.HOME ?? "~");
30794
+ const files = iterSourceFiles(expanded, maxFiles);
30795
+ const hits = [];
30796
+ const sinkKinds = new Set;
30797
+ for (const p of files) {
30798
+ const r = scanFile(p);
30799
+ for (const e of r.endpoints) {
30800
+ hits.push({ type: "endpoint", file: r.file, line: e.line, detail: e.hook });
30801
+ }
30802
+ for (const s of r.sinks) {
30803
+ hits.push({ type: "sink", file: r.file, line: s.line, detail: `${s.sink} -> ${s.class}` });
30804
+ const kind = classifySink(s.sink);
30805
+ if (kind)
30806
+ sinkKinds.add(kind);
30807
+ }
30808
+ }
30809
+ const chains = loadChains();
30810
+ const matched = [];
30811
+ for (const c of chains) {
30812
+ if ((c.required_findings ?? []).some((rf) => sinkKinds.has(rf))) {
30813
+ matched.push({ chain_id: c.id, name: c.name, severity: c.severity, steps: c.steps.length });
30814
+ }
30669
30815
  }
30670
- const detected = [...byWaf.values()].sort((a, b) => b.confidence - a.confidence);
30671
30816
  return {
30672
- detected: detected.length > 0,
30673
- wafs: detected,
30674
- block_status_codes: sigs.block_status_codes ?? []
30817
+ root: expanded,
30818
+ files_scanned: files.length,
30819
+ hits: hits.slice(0, 2000),
30820
+ sink_signals: [...sinkKinds].sort(),
30821
+ matched_chains: matched
30675
30822
  };
30676
30823
  }
30677
- function techCorrelation(tech) {
30678
- const data = loadJson("tech_correlations");
30679
- if (!data)
30680
- return { found: false, error: "tech_correlations.json not loaded" };
30681
- const key = tech.toLowerCase();
30682
- const entry = data[key];
30683
- if (!entry) {
30684
- const matches = Object.keys(data).filter((k) => k.includes(key) || key.includes(k)).slice(0, 5);
30685
- return { found: false, tech, suggestions: matches };
30824
+ function runEngagement(target, scope = "", mode = "bug-bounty", maxFiles = 2000, remember = true) {
30825
+ const report = {
30826
+ target,
30827
+ mode,
30828
+ tiers: {},
30829
+ findings: [],
30830
+ scope: null,
30831
+ memory: { captured: 0 }
30832
+ };
30833
+ const isPath = (() => {
30834
+ try {
30835
+ return existsSync4(target) && (statSync2(target).isDirectory() || statSync2(target).isFile());
30836
+ } catch {
30837
+ return false;
30838
+ }
30839
+ })();
30840
+ if (isPath) {
30841
+ report.scope = { target, allowed: true, enforcement: "source-audit", reason: "local source path — no network scope gate" };
30842
+ } else {
30843
+ const sc = scopeCheck(target, scope, mode);
30844
+ report.scope = sc;
30845
+ if (!sc.allowed) {
30846
+ report.status = "BLOCKED";
30847
+ report.reason = sc.reason;
30848
+ return report;
30849
+ }
30686
30850
  }
30687
- return { found: true, tech: key, vulns: entry.vulns ?? [], paths: entry.paths ?? [], detection: entry.detection ?? { vulns: entry.vulns, paths: entry.paths } };
30688
- }
30689
- function cveCorrelation(cveId) {
30690
- const data = loadJson("cve_correlations");
30691
- if (!data)
30692
- return { error: "cve_correlations.json not loaded" };
30693
- const key = cveId.toUpperCase();
30694
- const entry = data[key];
30695
- if (!entry)
30696
- return { found: false, cve: key, total_known: Object.keys(data).length };
30697
- return { found: true, cve: key, ...entry };
30698
- }
30699
- function portCorrelation(port) {
30700
- const data = loadJson("port_correlations");
30701
- if (!data)
30702
- return { error: "port_correlations.json not loaded" };
30703
- const key = String(port);
30704
- const entry = data[key];
30705
- if (!entry)
30706
- return { found: false, port: key, total_known: Object.keys(data).length };
30707
- return { found: true, port: key, ...entry };
30708
- }
30709
- function fuzzerPayloads(category) {
30710
- const data = loadJson("fuzzer_data");
30711
- if (!data)
30712
- return { error: "fuzzer_data.json not loaded" };
30713
- if (category) {
30714
- const c = data[category] ?? data[category.toUpperCase()];
30715
- return { category, found: Boolean(c), data: c ?? null };
30851
+ const blitz = enrichScan(target, maxFiles);
30852
+ report.tiers = {
30853
+ blitz: {
30854
+ files_scanned: blitz.files_scanned,
30855
+ hits: blitz.hits.length,
30856
+ sink_signals: blitz.sink_signals,
30857
+ matched_chains: blitz.matched_chains
30858
+ }
30859
+ };
30860
+ const chains = loadChains();
30861
+ const chainById = new Map(chains.map((c) => [c.id, c]));
30862
+ const eagle = [];
30863
+ for (const mc of blitz.matched_chains.slice(0, 10)) {
30864
+ const chain = chainById.get(mc.chain_id);
30865
+ if (!chain)
30866
+ continue;
30867
+ eagle.push({
30868
+ chain_id: chain.id,
30869
+ name: chain.name,
30870
+ severity: chain.severity,
30871
+ prerequisites: chain.required_findings,
30872
+ recommended_tools: chain.tools ?? [],
30873
+ tool_manuals: (chain.tools ?? []).map((t) => {
30874
+ const m = findManual(t);
30875
+ return m.found ? { tool: t, manual_name: m.name, preview: m.content.slice(0, 600) } : { tool: t, manual_name: null, preview: null };
30876
+ }),
30877
+ steps: chain.steps.map((s) => ({
30878
+ order: s.order,
30879
+ action: s.action,
30880
+ tool_hint: s.tool_hint,
30881
+ success_criteria: s.success_criteria,
30882
+ invariant_check: s.invariant_check,
30883
+ negative_control: s.negative_control
30884
+ }))
30885
+ });
30716
30886
  }
30717
- return { categories: Object.keys(data), fuzz_points: data.FUZZ_POINTS ?? null, payloads: data.FUZZ_PAYLOADS ?? null };
30718
- }
30719
- function intelSummary() {
30720
- const waf = loadJson("waf_signatures");
30721
- const tech = loadJson("tech_correlations");
30722
- const cve = loadJson("cve_correlations");
30723
- const port = loadJson("port_correlations");
30724
- const fuzz = loadJson("fuzzer_data");
30725
- const av = loadJson("attack_vectors");
30726
- return {
30727
- waf_signatures: waf ? (waf.header_signatures?.length ?? 0) + (waf.body_signatures?.length ?? 0) : 0,
30728
- tech_correlations: tech ? Object.keys(tech).length : 0,
30729
- cve_correlations: cve ? Object.keys(cve).length : 0,
30730
- port_correlations: port ? Object.keys(port).length : 0,
30731
- fuzzer_categories: fuzz ? Object.keys(fuzz).length : 0,
30732
- payload_categories: listPayloadCategories().length,
30733
- nuclei_templates: countTemplates(),
30734
- attack_vector_categories: av ? av.categories.length : 0,
30735
- attack_vectors: av ? av.categories.reduce((n, c) => n + c.vectors.length, 0) : 0
30887
+ report.tiers = { ...report.tiers, eagle_eye: { traced_chains: eagle } };
30888
+ const rank = { critical: 0, high: 1, medium: 2, low: 3 };
30889
+ const matchedSorted = [...blitz.matched_chains].sort((a, b) => (rank[a.severity] ?? 99) - (rank[b.severity] ?? 99));
30890
+ const findings = matchedSorted.map((f) => {
30891
+ const chain = chainById.get(f.chain_id);
30892
+ const severity = f.severity ?? "medium";
30893
+ const finding = makeFinding({
30894
+ title: f.name,
30895
+ target: { type: "source", path: target },
30896
+ severity,
30897
+ source: { type: "static_sink_signal", name: f.chain_id },
30898
+ sink: { type: f.chain_id, symbol: f.name },
30899
+ chainId: f.chain_id,
30900
+ chainName: f.name,
30901
+ status: "hypothesis"
30902
+ });
30903
+ const ev = makeEvidence({
30904
+ type: "sink_location",
30905
+ description: `Static sink signal matched escalation chain '${f.chain_id}' (${severity}). Hypothesis only — not yet validated.`,
30906
+ sink: { chain_id: f.chain_id, name: f.name, severity },
30907
+ artifacts: [{ name: "chain_match", kind: "json", content: JSON.stringify({ chain_id: f.chain_id, severity, steps: chain?.steps.length ?? 0 }) }]
30908
+ });
30909
+ finding.evidence.push(ev);
30910
+ return finding;
30911
+ });
30912
+ report.findings = findings.map((f) => ({
30913
+ id: f.id,
30914
+ status: f.status,
30915
+ title: f.title,
30916
+ severity: f.classification.severity,
30917
+ confidence: f.confidence,
30918
+ confidence_level: f.confidence_level,
30919
+ chain_id: f.chain.id,
30920
+ evidence_count: f.evidence.length
30921
+ }));
30922
+ const reportTitle = `Blitz Strike — Engagement Report (${target})`;
30923
+ const reportMarkdownText = reportMarkdown(findings, { title: reportTitle, scope: target, version: VERSION });
30924
+ const reportJsonText = reportJson(findings, { title: reportTitle, scope: target, version: VERSION });
30925
+ const sarifDoc = toSarif(findings.map((f) => ({
30926
+ ruleId: f.chain.id || f.classification.severity,
30927
+ level: sarifLevel(f.classification.severity),
30928
+ message: f.title,
30929
+ file: typeof f.target.path === "string" ? f.target.path : target,
30930
+ line: 1
30931
+ })));
30932
+ report.report = {
30933
+ summary: JSON.parse(reportJsonText).summary ?? null,
30934
+ markdown: reportMarkdownText,
30935
+ json: reportJsonText,
30936
+ sarif: sarifDoc
30736
30937
  };
30938
+ report.status = "COMPLETE";
30939
+ 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.";
30940
+ const executedState = "select_chain";
30941
+ const scopeAllowed = report.scope?.allowed ?? false;
30942
+ const plan = buildPlan(scopeAllowed, mode, executedState);
30943
+ report.state = executedState;
30944
+ report.state_description = stateDefinition(executedState).description;
30945
+ report.plan = plan;
30946
+ report.guidance = computeGuidance(executedState, scopeAllowed);
30947
+ report.risk_policy = {
30948
+ note: "High-risk actions (strike_verify, confirm_finding) require scope + are never run server-side automatically.",
30949
+ gated: plan.filter((p) => !p.permitted).map((p) => p.tool)
30950
+ };
30951
+ if (remember) {
30952
+ let captured = 0;
30953
+ for (const mc of blitz.matched_chains) {
30954
+ const chain = chainById.get(mc.chain_id);
30955
+ const content = `Chain '${mc.chain_id}' (${mc.severity}) matched on target '${target}'. ` + `Detected sink signals: ${blitz.sink_signals.join(", ")}. ` + `Prerequisites: ${(chain?.required_findings ?? []).join(", ")}.`;
30956
+ if (rememberIfAbsent(mc.chain_id, content, "pattern", [mc.severity, "chain"], "engagement", false)) {
30957
+ captured++;
30958
+ }
30959
+ }
30960
+ report.memory = { captured, deduped: blitz.matched_chains.length - captured };
30961
+ }
30962
+ return report;
30737
30963
  }
30738
- function listAttackVectors() {
30739
- const data = loadJson("attack_vectors");
30740
- if (!data)
30741
- return { error: "attack_vectors.json not loaded" };
30964
+ function listChains() {
30965
+ const chains = loadChains();
30742
30966
  return {
30743
- total_categories: data.categories.length,
30744
- total_vectors: data.categories.reduce((n, c) => n + c.vectors.length, 0),
30745
- categories: data.categories.map((c) => ({ id: c.id, title: c.title, count: c.vectors.length }))
30967
+ total: chains.length,
30968
+ chains: chains.map((c) => ({
30969
+ id: c.id,
30970
+ name: c.name,
30971
+ severity: c.severity,
30972
+ steps: c.steps.length,
30973
+ triggers: c.triggers
30974
+ }))
30746
30975
  };
30747
30976
  }
30748
- function attackVectors(category) {
30749
- const data = loadJson("attack_vectors");
30750
- if (!data)
30751
- return { error: "attack_vectors.json not loaded" };
30752
- const q = category.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
30753
- const found = data.categories.find((c) => c.id === q || c.id.includes(q) || c.title.toLowerCase().includes(category.toLowerCase()));
30754
- if (!found) {
30755
- const matches = data.categories.filter((c) => c.title.toLowerCase().includes(category.toLowerCase()) || c.id.includes(q)).slice(0, 5).map((c) => c.id);
30756
- return { found: false, query: category, suggestions: matches };
30977
+ var LIVE_SWEEP_PARAMS = ["q", "search", "query", "s", "id", "url", "redirect", "name", "page"];
30978
+ async function runLiveEngagement(url, scope = "", mode = "bug-bounty") {
30979
+ const report = { target: url, mode, findings: [], scope: null, status: "" };
30980
+ const sc = scopeCheck(url, scope, mode);
30981
+ report.scope = sc;
30982
+ if (!sc.allowed) {
30983
+ report.status = "BLOCKED";
30984
+ report.reason = sc.reason;
30985
+ return report;
30757
30986
  }
30758
- return { found: true, id: found.id, title: found.title, count: found.vectors.length, vectors: found.vectors };
30759
- }
30760
- var PACKAGE_PAYLOADS = join8(ROOT3, "payloads");
30761
- var PACKAGE_TEMPLATES = join8(ROOT3, "templates");
30762
- var DATA_ROOT = process.env.BLITZSTRIKE_DATA ?? join8(homedir3(), ".blitzstrike", "data");
30763
- function payloadsDir() {
30764
- if (existsSync5(PACKAGE_PAYLOADS))
30765
- return PACKAGE_PAYLOADS;
30766
- return join8(DATA_ROOT, "payloads");
30767
- }
30768
- function templatesDir() {
30769
- if (existsSync5(PACKAGE_TEMPLATES))
30770
- return PACKAGE_TEMPLATES;
30771
- return join8(DATA_ROOT, "templates");
30987
+ const findings = [];
30988
+ for (const param of LIVE_SWEEP_PARAMS) {
30989
+ const verdict = await strikeVerify({ url, param });
30990
+ const finding = makeFinding({
30991
+ title: `Reflected input in parameter '${param}'`,
30992
+ target: { type: "web", host: url, endpoint: `?${param}=` },
30993
+ severity: "medium",
30994
+ cwe: "CWE-79",
30995
+ cweName: "Cross-site Scripting",
30996
+ source: { type: "request_parameter", name: param },
30997
+ sink: { type: "reflected_input", symbol: param },
30998
+ chainId: "reflected_input",
30999
+ chainName: "Reflected input",
31000
+ status: "hypothesis"
31001
+ });
31002
+ findings.push(resolveFinding(finding, verdict));
31003
+ }
31004
+ report.findings = findings.map((f) => ({
31005
+ id: f.id,
31006
+ status: f.status,
31007
+ title: f.title,
31008
+ severity: f.classification.severity,
31009
+ confidence: f.confidence,
31010
+ confidence_level: f.confidence_level,
31011
+ chain_id: f.chain.id,
31012
+ validation: f.validation
31013
+ }));
31014
+ const title = `Blitz Strike — Live Engagement (${url})`;
31015
+ const markdown = reportMarkdown(findings, { title, scope: url, version: VERSION });
31016
+ const json = reportJson(findings, { title, scope: url, version: VERSION });
31017
+ report.report = {
31018
+ summary: JSON.parse(json).summary ?? null,
31019
+ markdown,
31020
+ json,
31021
+ sarif: toSarif(findings.map((f) => ({
31022
+ ruleId: f.chain.id || f.classification.cwe || f.classification.severity,
31023
+ level: sarifLevel(f.classification.severity),
31024
+ message: f.title,
31025
+ file: url,
31026
+ line: 1
31027
+ })))
31028
+ };
31029
+ report.status = "COMPLETE";
31030
+ 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.";
31031
+ return report;
30772
31032
  }
30773
- var PAYLOADS = payloadsDir();
30774
- var TEMPLATES = templatesDir();
30775
- function listPayloadCategories() {
31033
+
31034
+ // src/catalog.ts
31035
+ import { readFileSync as readFileSync7, readdirSync as readdirSync2 } from "node:fs";
31036
+ import { join as join7 } from "node:path";
31037
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
31038
+ import { spawnSync } from "node:child_process";
31039
+ var ROOT2 = join7(fileURLToPath4(new URL(".", import.meta.url)), "..");
31040
+ var _toolsCache = null;
31041
+ var _skillsCache = null;
31042
+ function loadTools() {
31043
+ if (_toolsCache)
31044
+ return _toolsCache;
30776
31045
  try {
30777
- return readdirSync3(PAYLOADS, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
31046
+ const raw = JSON.parse(readFileSync7(join7(ROOT2, "tools-catalog.json"), "utf8"));
31047
+ _toolsCache = raw.tools ?? [];
30778
31048
  } catch {
30779
- return [];
31049
+ _toolsCache = [];
30780
31050
  }
31051
+ return _toolsCache;
30781
31052
  }
30782
- function payloadLookup(topic) {
30783
- const cats = listPayloadCategories();
30784
- const key = topic.toLowerCase();
30785
- const ALIASES = {
30786
- sqli: "sql injection",
30787
- xss: "xss injection",
30788
- ssrf: "server side request forgery",
30789
- ssti: "server side template injection",
30790
- jwt: "json web token",
30791
- lfi: "file inclusion",
30792
- rfi: "file inclusion",
30793
- xxe: "xxe injection",
30794
- idor: "insecure direct object references",
30795
- csrf: "cross-site request forgery",
30796
- cmdi: "command injection",
30797
- rce: "command injection",
30798
- cors: "cors misconfiguration",
30799
- crlf: "crlf injection",
30800
- prototype: "prototype pollution",
30801
- nosql: "nosql injection",
30802
- upload: "upload insecure files"
30803
- };
30804
- const normalized = ALIASES[key] ?? key;
30805
- const exact = cats.find((c) => c.toLowerCase() === normalized);
30806
- if (exact) {
30807
- return { found: true, category: exact, files: listFiles(join8(PAYLOADS, exact)) };
31053
+ function loadSkills() {
31054
+ if (_skillsCache)
31055
+ return _skillsCache;
31056
+ const dir = join7(ROOT2, "skills");
31057
+ const out = [];
31058
+ try {
31059
+ for (const f of readdirSync2(dir)) {
31060
+ if (!f.endsWith(".md"))
31061
+ continue;
31062
+ const p = join7(dir, f);
31063
+ const text = readFileSync7(p, "utf8");
31064
+ const descMatch = text.match(/^description:\s*(.+)$/m);
31065
+ out.push({
31066
+ name: f.replace(/\.md$/, ""),
31067
+ file: f,
31068
+ description: (descMatch?.[1] ?? "").trim().slice(0, 200),
31069
+ size: text.length
31070
+ });
31071
+ }
31072
+ } catch {}
31073
+ _skillsCache = out;
31074
+ return out;
31075
+ }
31076
+ function toolLookup(name) {
31077
+ const tools = loadTools();
31078
+ const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
31079
+ if (!t) {
31080
+ return {
31081
+ found: false,
31082
+ query: name,
31083
+ suggestions: tools.filter((x) => x.name.includes(name.toLowerCase()) || name.toLowerCase().includes(x.name)).slice(0, 5).map((x) => x.name)
31084
+ };
30808
31085
  }
30809
- const keyTokens = normalized.split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
30810
- const matches = cats.filter((c) => {
30811
- const cl = c.toLowerCase();
30812
- if (cl.includes(normalized) || normalized.includes(cl))
30813
- return true;
30814
- return keyTokens.some((tok) => new RegExp(`(^|[^a-z0-9])${tok}`).test(cl));
30815
- });
30816
- if (matches.length > 0) {
30817
- return { found: true, category: matches[0], files: listFiles(join8(PAYLOADS, matches[0])), alternatives: matches.slice(0, 10) };
31086
+ const manual = manualForTool(t.name);
31087
+ const result = { found: true, tool: t };
31088
+ if (manual && manual.content) {
31089
+ result.manual = {
31090
+ name: manual.name,
31091
+ category: manual.category,
31092
+ preview: manual.content.slice(0, 1500),
31093
+ full_length: manual.content.length
31094
+ };
31095
+ } else {
31096
+ result.manual = null;
30818
31097
  }
30819
- return { found: false, topic, suggestions: cats.slice(0, 20) };
31098
+ return result;
30820
31099
  }
30821
- function readPayload(category, file) {
30822
- const cats = listPayloadCategories();
30823
- const exact = cats.find((c) => c.toLowerCase() === category.toLowerCase());
30824
- if (!exact)
30825
- return { found: false, category, suggestions: cats.slice(0, 20) };
30826
- const dir = join8(PAYLOADS, exact);
30827
- const files = listFiles(dir);
30828
- if (!file) {
30829
- const readme = files.find((f) => f.toLowerCase() === "readme.md");
30830
- if (readme) {
30831
- return { found: true, category: exact, file: readme, content: readFileSync8(join8(dir, readme), "utf8").slice(0, 20000) };
30832
- }
30833
- return { found: true, category: exact, files };
31100
+ function listTools() {
31101
+ const tools = loadTools();
31102
+ const byCat = {};
31103
+ for (const t of tools) {
31104
+ (byCat[t.category] ??= []).push(t.name);
30834
31105
  }
30835
- const f = files.find((x) => x.toLowerCase() === file.toLowerCase());
30836
- if (!f)
30837
- return { found: false, category: exact, file, suggestions: files };
30838
- return { found: true, category: exact, file: f, content: readFileSync8(join8(dir, f), "utf8").slice(0, 20000) };
31106
+ return { total: tools.length, categories: byCat, tools: tools.map((t) => ({ name: t.name, category: t.category, description: t.description })) };
30839
31107
  }
30840
- function listFiles(dir) {
30841
- try {
30842
- return readdirSync3(dir).filter((f) => f.endsWith(".md") || f.endsWith(".txt") || f.endsWith(".yaml")).sort();
30843
- } catch {
30844
- return [];
31108
+ function skillLookup(topic) {
31109
+ const skills = loadSkills();
31110
+ const q = topic.toLowerCase();
31111
+ const scored = skills.map((s) => {
31112
+ const hay = `${s.name} ${s.description}`.toLowerCase();
31113
+ const nameHit = s.name.includes(q);
31114
+ const descHit = s.description.includes(q);
31115
+ const wordHit = q.split(/\s+/).filter((w) => w && hay.includes(w)).length;
31116
+ return { s, score: (nameHit ? 10 : 0) + (descHit ? 5 : 0) + wordHit * 2 };
31117
+ }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score);
31118
+ if (scored.length === 0) {
31119
+ return { found: false, query: topic, suggestions: skills.map((s) => s.name).slice(0, 10) };
30845
31120
  }
31121
+ return {
31122
+ found: true,
31123
+ matches: scored.slice(0, 5).map((x) => ({ name: x.s.name, description: x.s.description, file: x.s.file }))
31124
+ };
30846
31125
  }
30847
- function countTemplates() {
31126
+ function listSkills() {
31127
+ const skills = loadSkills();
31128
+ return { total: skills.length, skills: skills.map((s) => ({ name: s.name, description: s.description })) };
31129
+ }
31130
+ function readSkill(name) {
31131
+ const skills = loadSkills();
31132
+ const s = skills.find((x) => x.name.toLowerCase() === name.toLowerCase());
31133
+ if (!s)
31134
+ return { found: false, query: name };
30848
31135
  try {
30849
- let n = 0;
30850
- const walk = (dir) => {
30851
- for (const e of readdirSync3(dir, { withFileTypes: true })) {
30852
- if (e.isDirectory())
30853
- walk(join8(dir, e.name));
30854
- else if (e.name.endsWith(".yaml"))
30855
- n++;
30856
- }
30857
- };
30858
- walk(TEMPLATES);
30859
- return n;
31136
+ const text = readFileSync7(join7(ROOT2, "skills", s.file), "utf8");
31137
+ return { found: true, name: s.name, content: text.slice(0, 12000) };
30860
31138
  } catch {
30861
- return 0;
31139
+ return { found: false, query: name, error: "unreadable" };
30862
31140
  }
30863
31141
  }
30864
- function templateLookup(topic, limit = 10) {
30865
- const key = topic.toLowerCase();
30866
- const hits = [];
30867
- const walk = (dir, depth = 0) => {
30868
- if (depth > 6 || hits.length >= limit * 3)
30869
- return;
30870
- let entries;
30871
- try {
30872
- entries = readdirSync3(dir, { withFileTypes: true });
30873
- } catch {
30874
- return;
30875
- }
30876
- for (const e of entries) {
30877
- if (e.isDirectory())
30878
- walk(join8(dir, e.name), depth + 1);
30879
- else if (e.name.endsWith(".yaml")) {
30880
- const name = e.name.toLowerCase();
30881
- if (name.includes(key)) {
30882
- const full = join8(dir, e.name);
30883
- let id = "";
30884
- try {
30885
- const content = readFileSync8(full, "utf8");
30886
- const m = content.match(/^\s*id:\s*(.+)$/m);
30887
- if (m)
30888
- id = m[1].trim();
30889
- } catch {}
30890
- hits.push({ path: full.replace(ROOT3 + "/", ""), name: e.name, id });
30891
- }
30892
- }
30893
- }
31142
+ function ensureTool(name) {
31143
+ const tools = loadTools();
31144
+ const t = tools.find((x) => x.name.toLowerCase() === name.toLowerCase());
31145
+ if (!t) {
31146
+ return { found: false, query: name, suggestions: tools.slice(0, 5).map((x) => x.name) };
31147
+ }
31148
+ const check = spawnSync(t.check_installed.command, { shell: true, timeout: 15000 });
31149
+ if (check.status !== 127 && check.error === undefined) {
31150
+ return { name: t.name, installed: true, action: "none", note: "already installed" };
31151
+ }
31152
+ const platform = process.platform;
31153
+ const cmd = t.install[platform] ?? t.install.linux;
31154
+ if (!cmd) {
31155
+ return { name: t.name, installed: false, action: "none", note: `no install command for ${platform}` };
31156
+ }
31157
+ const r = spawnSync(cmd, { shell: true, timeout: 120000 });
31158
+ const ok = r.status === 0;
31159
+ return {
31160
+ name: t.name,
31161
+ installed: ok,
31162
+ action: "install",
31163
+ command: cmd,
31164
+ output: (r.stdout?.toString() ?? "").slice(-400) || (r.stderr?.toString() ?? "").slice(-400)
30894
31165
  };
30895
- walk(TEMPLATES);
30896
- return { found: hits.length > 0, topic, count: hits.length, templates: hits.slice(0, limit) };
30897
31166
  }
30898
31167
 
30899
- // src/active.ts
30900
- var TIMEOUT_MS2 = 20000;
30901
- async function httpFetch(url, init) {
30902
- const controller = new AbortController;
30903
- const t = setTimeout(() => controller.abort(), TIMEOUT_MS2);
31168
+ // src/intel.ts
31169
+ import { readFileSync as readFileSync8, existsSync as existsSync5 } from "node:fs";
31170
+ import { join as join8 } from "node:path";
31171
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
31172
+ import { readdirSync as readdirSync3 } from "node:fs";
31173
+ import { homedir as homedir3 } from "node:os";
31174
+ var ROOT3 = join8(fileURLToPath5(new URL(".", import.meta.url)), "..");
31175
+ var INTEL = join8(ROOT3, "intelligence");
31176
+ function loadJson(name) {
31177
+ const p = join8(INTEL, `${name}.json`);
31178
+ if (!existsSync5(p))
31179
+ return null;
30904
31180
  try {
30905
- const res = await fetch(url, { ...init, signal: controller.signal, redirect: "follow" });
30906
- const body = await res.text();
30907
- const headers = {};
30908
- res.headers.forEach((v, k) => {
30909
- headers[k] = v;
30910
- });
30911
- return { status: res.status, headers, body: body.slice(0, 20000), ok: res.ok };
30912
- } catch (e) {
30913
- return { status: 0, headers: {}, body: String(e), ok: false };
30914
- } finally {
30915
- clearTimeout(t);
30916
- }
30917
- }
30918
- function normalizeTarget(target) {
30919
- let t = target.trim();
30920
- if (!/^https?:\/\//i.test(t))
30921
- t = `https://${t}`;
30922
- return t.replace(/\/+$/, "");
30923
- }
30924
- function extractTitle(body) {
30925
- const m = body.match(/<title[^>]*>([^<]+)<\/title>/i);
30926
- return m ? m[1].trim().slice(0, 200) : "";
30927
- }
30928
- function detectServer(headers) {
30929
- return headers["server"] ?? headers["x-powered-by"] ?? "";
30930
- }
30931
- function detectTechFromBody(body) {
30932
- const hits = [];
30933
- const sigs = [
30934
- [/wp-content|wp-includes|wp-json/i, "wordpress"],
30935
- [/wp-login\.php|wp-admin/i, "wordpress"],
30936
- [/powered by joomla|com_content/i, "joomla"],
30937
- [/laravel|_token|csrf-token/i, "laravel"],
30938
- [/react|__NEXT_DATA__|next\/static/i, "nextjs"],
30939
- [/angular|ng-version/i, "angular"],
30940
- [/vue|__vue__|v-data/i, "vue"],
30941
- [/django|csrftoken|__debug__/i, "django"],
30942
- [/ruby on rails|rails/i, "rails"],
30943
- [/asp\.net|__VIEWSTATE|__EVENTVALIDATION/i, "aspnet"],
30944
- [/phpBB|phpbb/i, "phpbb"],
30945
- [/mybb|mybb/i, "mybb"],
30946
- [/drupal|Drupal\.settings/i, "drupal"],
30947
- [/magento|Mage\./i, "magento"],
30948
- [/shopify|cdn\.shopify/i, "shopify"],
30949
- [/grafana|kibana|elastic/i, "grafana-kibana"]
30950
- ];
30951
- for (const [re, name] of sigs) {
30952
- if (re.test(body))
30953
- hits.push(name);
30954
- }
30955
- return [...new Set(hits)];
30956
- }
30957
- async function probeCommonPaths(base) {
30958
- const paths = [
30959
- "/robots.txt",
30960
- "/sitemap.xml",
30961
- "/.well-known/security.txt",
30962
- "/wp-login.php",
30963
- "/wp-admin/",
30964
- "/wp-json/",
30965
- "/administrator/",
30966
- "/admin/",
30967
- "/login",
30968
- "/api/",
30969
- "/.git/config",
30970
- "/.env",
30971
- "/config.php",
30972
- "/phpinfo.php",
30973
- "/swagger.json",
30974
- "/swagger-ui.html",
30975
- "/graphql",
30976
- "/actuator/health"
30977
- ];
30978
- const out = [];
30979
- for (const p of paths) {
30980
- const r = await httpFetch(`${base}${p}`, { method: "GET" });
30981
- out.push({ path: p, status: r.status });
31181
+ return JSON.parse(readFileSync8(p, "utf8"));
31182
+ } catch {
31183
+ return null;
30982
31184
  }
30983
- return out;
30984
31185
  }
30985
- async function activeScan(target, scope = "", mode = "bug-bounty", allowed = undefined) {
30986
- const base = normalizeTarget(target);
30987
- const host = (() => {
30988
- try {
30989
- return new URL(base).hostname;
30990
- } catch {
30991
- return target;
31186
+ function detectWaf(headers, body = "") {
31187
+ const sigs = loadJson("waf_signatures");
31188
+ if (!sigs)
31189
+ return { detected: false, wafs: [], error: "waf_signatures.json not loaded" };
31190
+ const headerSigs = sigs.header_signatures ?? [];
31191
+ const bodySigs = sigs.body_signatures ?? [];
31192
+ const matches = [];
31193
+ const headerKeys = Object.keys(headers).map((k) => k.toLowerCase());
31194
+ for (const s of headerSigs) {
31195
+ const h = (s.header ?? "").toLowerCase();
31196
+ const pattern = (s.pattern ?? "").toLowerCase();
31197
+ if (!h)
31198
+ continue;
31199
+ if (pattern) {
31200
+ const val = (headers[h] ?? headers[s.header ?? ""] ?? "").toLowerCase();
31201
+ if (val.includes(pattern)) {
31202
+ matches.push({ waf: s.waf, header: s.header, pattern: s.pattern, confidence: s.confidence, via: "header-value" });
31203
+ }
31204
+ } else {
31205
+ if (headerKeys.includes(h)) {
31206
+ matches.push({ waf: s.waf, header: s.header, confidence: s.confidence, via: "header-name" });
31207
+ }
30992
31208
  }
30993
- })();
30994
- if (allowed === false) {
30995
- return {
30996
- target: base,
30997
- gated: true,
30998
- gate_reason: `active scanning not authorized for '${host}' (mode=${mode}, scope='${scope || "unset"}')`
30999
- };
31000
31209
  }
31001
- const result = { target: base, gated: false };
31002
- const root = await httpFetch(base);
31003
- if (root.status === 0) {
31004
- result.gated = true;
31005
- result.gate_reason = `could not reach ${base}: ${root.body.slice(0, 120)}`;
31006
- return result;
31210
+ for (const s of bodySigs) {
31211
+ const pattern = (s.pattern ?? "").toLowerCase();
31212
+ if (pattern && body.toLowerCase().includes(pattern)) {
31213
+ matches.push({ waf: s.waf, pattern: s.pattern, confidence: s.confidence, via: "body" });
31214
+ }
31007
31215
  }
31008
- result.waf = detectWaf(root.headers, root.body);
31009
- const interestingHeaders = {};
31010
- for (const k of ["server", "x-powered-by", "x-aspnet-version", "x-drupal-cache", "x-generator", "via", "x-cache", "cf-ray"]) {
31011
- if (root.headers[k])
31012
- interestingHeaders[k] = root.headers[k];
31216
+ const byWaf = new Map;
31217
+ for (const m of matches) {
31218
+ const key = m.waf;
31219
+ const prev = byWaf.get(key);
31220
+ if (!prev || m.confidence > prev.confidence)
31221
+ byWaf.set(key, m);
31013
31222
  }
31014
- result.fingerprint = {
31015
- status: root.status,
31016
- title: extractTitle(root.body),
31017
- server: detectServer(root.headers),
31018
- headers: interestingHeaders,
31019
- body_size: root.body.length
31020
- };
31021
- const techs = detectTechFromBody(root.body);
31022
- const serverTech = detectServer(root.headers);
31023
- result.tech = {
31024
- detected: techs,
31025
- server: serverTech,
31026
- correlations: techs.map((t) => techCorrelation(t)).filter((c) => c.found === true)
31223
+ const detected = [...byWaf.values()].sort((a, b) => b.confidence - a.confidence);
31224
+ return {
31225
+ detected: detected.length > 0,
31226
+ wafs: detected,
31227
+ block_status_codes: sigs.block_status_codes ?? []
31027
31228
  };
31028
- result.endpoints = await probeCommonPaths(base);
31029
- const recs = [];
31030
- if (result.waf?.detected)
31031
- recs.push(`WAF detected (${result.waf.wafs?.map((w) => w.waf).join(", ")}) — apply WAF bypass before exploitation.`);
31032
- const interesting = result.endpoints?.filter((e) => e.status >= 200 && e.status < 400 && !e.path.includes(".well-known"));
31033
- if (interesting && interesting.length > 0) {
31034
- recs.push(`Live endpoints found: ${interesting.map((e) => e.path).join(", ")} — trace these with EAGLE-EYE / verify with STRIKE.`);
31035
- }
31036
- for (const c of result.tech?.correlations ?? []) {
31037
- recs.push(`Tech '${c.tech}' maps to known vuln classes: ${(c.vulns ?? []).slice(0, 3).join(", ")}`);
31038
- }
31039
- result.recommendations = recs;
31040
- return result;
31041
31229
  }
31042
-
31043
- // src/dataflow.ts
31044
- init_scanner();
31045
- import { readFileSync as readFileSync9 } from "node:fs";
31046
- var SOURCES = {
31047
- $_GET: { id: "http_get", label: "HTTP GET parameter", attacker_controlled: true },
31048
- $_POST: { id: "http_post", label: "HTTP POST body", attacker_controlled: true },
31049
- $_REQUEST: { id: "http_request", label: "HTTP request (merged)", attacker_controlled: true },
31050
- $_COOKIE: { id: "http_cookie", label: "HTTP cookie", attacker_controlled: true },
31051
- $_FILES: { id: "uploaded_file", label: "Uploaded file", attacker_controlled: true },
31052
- $_SERVER: { id: "http_header", label: "HTTP header / server env", attacker_controlled: true },
31053
- "file_get_contents('php://input')": { id: "raw_body", label: "Raw request body", attacker_controlled: true },
31054
- "php://input": { id: "raw_body", label: "Raw request body", attacker_controlled: true },
31055
- json_decode: { id: "json_input", label: "JSON input", attacker_controlled: true },
31056
- getallheaders: { id: "http_header", label: "HTTP header", attacker_controlled: true },
31057
- $argv: { id: "cli_argument", label: "CLI argument", attacker_controlled: false },
31058
- getenv: { id: "environment_variable", label: "Environment variable", attacker_controlled: false },
31059
- "$wpdb->get_results": { id: "database_value", label: "Database value", attacker_controlled: false },
31060
- wp_remote_get: { id: "external_api", label: "External API data", attacker_controlled: false }
31061
- };
31062
- function classifySource(line) {
31063
- for (const [token, cls] of Object.entries(SOURCES)) {
31064
- if (line.includes(token))
31065
- return cls;
31230
+ function techCorrelation(tech) {
31231
+ const data = loadJson("tech_correlations");
31232
+ if (!data)
31233
+ return { found: false, error: "tech_correlations.json not loaded" };
31234
+ const key = tech.toLowerCase();
31235
+ const entry = data[key];
31236
+ if (!entry) {
31237
+ const matches = Object.keys(data).filter((k) => k.includes(key) || key.includes(k)).slice(0, 5);
31238
+ return { found: false, tech, suggestions: matches };
31066
31239
  }
31067
- return null;
31240
+ return { found: true, tech: key, vulns: entry.vulns ?? [], paths: entry.paths ?? [], detection: entry.detection ?? { vulns: entry.vulns, paths: entry.paths } };
31068
31241
  }
31069
- var SINK_CLASS_MAP = [
31070
- [/->query\(|\$wpdb->query|\$wpdb->get_var|\$wpdb->get_results|->whereRaw|->selectRaw/, { id: "sql_execution", category: "SQL execution", cwe: "CWE-89" }],
31071
- [/\beval\(|\bassert\(|\bcreate_function\(|\bcall_user_func\(|\bpreg_replace\(/, { id: "code_execution", category: "Dynamic evaluation / code execution", cwe: "CWE-94" }],
31072
- [/\bsystem\(|\bexec\(|\bshell_exec\(|\bpassthru\(|\bproc_open\(|\bpopen\(/, { id: "command_execution", category: "Command execution", cwe: "CWE-78" }],
31073
- [/\bmove_uploaded_file\(|\bfile_put_contents\(|\bfwrite\(|\bfopen\(|\bunlink\(/, { id: "file_operations", category: "File operations", cwe: "CWE-434" }],
31074
- [/\binclude\(|\brequire\(|\binclude_once\(|\brequire_once\(/, { id: "file_inclusion", category: "File inclusion", cwe: "CWE-98" }],
31075
- [/\bunserialize\(|\bmaybe_unserialize\(/, { id: "deserialization", category: "Deserialization", cwe: "CWE-502" }],
31076
- [/\bwp_remote_get\(|\bwp_remote_post\(|\bfile_get_contents\(|\bcurl_exec\(|\bcurl_setopt\(/, { id: "http_request", category: "HTTP request (SSRF)", cwe: "CWE-918" }],
31077
- [/\bheader\(|\bwp_redirect\(/, { id: "redirect", category: "Redirect handling", cwe: "CWE-601" }],
31078
- [/echo\s|print\s|\bprintf\(/, { id: "html_render", category: "HTML rendering (XSS)", cwe: "CWE-79" }],
31079
- [/\bsimplexml_load_string\(|new SimpleXMLElement|DOMDocument/, { id: "xml_processing", category: "XML processing (XXE)", cwe: "CWE-611" }],
31080
- [/\bZipArchive|\bPharData|->extractTo\(/, { id: "archive_extraction", category: "Archive extraction (zip slip)", cwe: "CWE-22" }]
31081
- ];
31082
- function classifySink2(sink) {
31083
- for (const [re, cls] of SINK_CLASS_MAP) {
31084
- if (re.test(sink))
31085
- return cls;
31086
- }
31087
- return null;
31242
+ function cveCorrelation(cveId) {
31243
+ const data = loadJson("cve_correlations");
31244
+ if (!data)
31245
+ return { error: "cve_correlations.json not loaded" };
31246
+ const key = cveId.toUpperCase();
31247
+ const entry = data[key];
31248
+ if (!entry)
31249
+ return { found: false, cve: key, total_known: Object.keys(data).length };
31250
+ return { found: true, cve: key, ...entry };
31088
31251
  }
31089
- var SANITIZERS = {
31090
- htmlspecialchars: { id: "html_escape", label: "HTML entity encoding", neutralizes: ["html_render"] },
31091
- esc_html: { id: "html_escape", label: "HTML escape", neutralizes: ["html_render"] },
31092
- esc_attr: { id: "html_escape", label: "HTML attribute escape", neutralizes: ["html_render"] },
31093
- htmlentities: { id: "html_escape", label: "HTML entity encoding", neutralizes: ["html_render"] },
31094
- esc_sql: { id: "sql_escape", label: "SQL escape", neutralizes: ["sql_execution"] },
31095
- "$wpdb->prepare": { id: "sql_prepare", label: "Prepared SQL statement", neutralizes: ["sql_execution"] },
31096
- "->prepare(": { id: "sql_prepare", label: "Prepared statement", neutralizes: ["sql_execution"] },
31097
- filter_var: { id: "filter_var", label: "Filter input", neutralizes: ["html_render", "sql_execution", "command_execution"] },
31098
- filter_input: { id: "filter_var", label: "Filter input", neutralizes: ["html_render", "sql_execution", "command_execution"] },
31099
- sanitize_text_field: { id: "wp_sanitize", label: "WordPress sanitize", neutralizes: ["html_render"] },
31100
- sanitize_file_name: { id: "wp_sanitize_file", label: "File-name sanitize", neutralizes: ["file_operations", "file_inclusion"] },
31101
- wp_verify_nonce: { id: "nonce_check", label: "Nonce verification", neutralizes: ["authorization"] },
31102
- check_ajax_referer: { id: "nonce_check", label: "Nonce verification", neutralizes: ["authorization"] },
31103
- escapeshellarg: { id: "shell_escape", label: "Shell argument escape", neutralizes: ["command_execution"] },
31104
- escapeshellcmd: { id: "shell_escape", label: "Shell command escape", neutralizes: ["command_execution"] },
31105
- intval: { id: "int_cast", label: "Integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
31106
- absint: { id: "int_cast", label: "Absolute integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
31107
- "preg_replace.*FILTER": { id: "regex_filter", label: "Regex filter", neutralizes: ["html_render"] }
31108
- };
31109
- function findSanitizers(line) {
31110
- const out = [];
31111
- for (const [token, san] of Object.entries(SANITIZERS)) {
31112
- if (line.includes(token))
31113
- out.push(san);
31252
+ function portCorrelation(port) {
31253
+ const data = loadJson("port_correlations");
31254
+ if (!data)
31255
+ return { error: "port_correlations.json not loaded" };
31256
+ const key = String(port);
31257
+ const entry = data[key];
31258
+ if (!entry)
31259
+ return { found: false, port: key, total_known: Object.keys(data).length };
31260
+ return { found: true, port: key, ...entry };
31261
+ }
31262
+ function fuzzerPayloads(category) {
31263
+ const data = loadJson("fuzzer_data");
31264
+ if (!data)
31265
+ return { error: "fuzzer_data.json not loaded" };
31266
+ if (category) {
31267
+ const c = data[category] ?? data[category.toUpperCase()];
31268
+ return { category, found: Boolean(c), data: c ?? null };
31114
31269
  }
31115
- return out;
31270
+ return { categories: Object.keys(data), fuzz_points: data.FUZZ_POINTS ?? null, payloads: data.FUZZ_PAYLOADS ?? null };
31116
31271
  }
31117
- function isSanitized(sanitizers, sinkCategory) {
31118
- return sanitizers.some((s) => s.neutralizes.includes(sinkCategory) || s.neutralizes.includes("authorization"));
31272
+ function intelSummary() {
31273
+ const waf = loadJson("waf_signatures");
31274
+ const tech = loadJson("tech_correlations");
31275
+ const cve = loadJson("cve_correlations");
31276
+ const port = loadJson("port_correlations");
31277
+ const fuzz = loadJson("fuzzer_data");
31278
+ const av = loadJson("attack_vectors");
31279
+ return {
31280
+ waf_signatures: waf ? (waf.header_signatures?.length ?? 0) + (waf.body_signatures?.length ?? 0) : 0,
31281
+ tech_correlations: tech ? Object.keys(tech).length : 0,
31282
+ cve_correlations: cve ? Object.keys(cve).length : 0,
31283
+ port_correlations: port ? Object.keys(port).length : 0,
31284
+ fuzzer_categories: fuzz ? Object.keys(fuzz).length : 0,
31285
+ payload_categories: listPayloadCategories().length,
31286
+ nuclei_templates: countTemplates(),
31287
+ attack_vector_categories: av ? av.categories.length : 0,
31288
+ attack_vectors: av ? av.categories.reduce((n, c) => n + c.vectors.length, 0) : 0
31289
+ };
31119
31290
  }
31120
- function traceDataFlow(path) {
31121
- const scan = scanFile(path);
31122
- const result = { file: path, candidates: [], sanitized: [], authorized: [] };
31123
- if (scan.error)
31124
- return result;
31125
- let text;
31291
+ function listAttackVectors() {
31292
+ const data = loadJson("attack_vectors");
31293
+ if (!data)
31294
+ return { error: "attack_vectors.json not loaded" };
31295
+ return {
31296
+ total_categories: data.categories.length,
31297
+ total_vectors: data.categories.reduce((n, c) => n + c.vectors.length, 0),
31298
+ categories: data.categories.map((c) => ({ id: c.id, title: c.title, count: c.vectors.length }))
31299
+ };
31300
+ }
31301
+ function attackVectors(category) {
31302
+ const data = loadJson("attack_vectors");
31303
+ if (!data)
31304
+ return { error: "attack_vectors.json not loaded" };
31305
+ const q = category.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
31306
+ const found = data.categories.find((c) => c.id === q || c.id.includes(q) || c.title.toLowerCase().includes(category.toLowerCase()));
31307
+ if (!found) {
31308
+ const matches = data.categories.filter((c) => c.title.toLowerCase().includes(category.toLowerCase()) || c.id.includes(q)).slice(0, 5).map((c) => c.id);
31309
+ return { found: false, query: category, suggestions: matches };
31310
+ }
31311
+ return { found: true, id: found.id, title: found.title, count: found.vectors.length, vectors: found.vectors };
31312
+ }
31313
+ var PACKAGE_PAYLOADS = join8(ROOT3, "payloads");
31314
+ var PACKAGE_TEMPLATES = join8(ROOT3, "templates");
31315
+ var DATA_ROOT = process.env.BLITZSTRIKE_DATA ?? join8(homedir3(), ".blitzstrike", "data");
31316
+ function payloadsDir() {
31317
+ if (existsSync5(PACKAGE_PAYLOADS))
31318
+ return PACKAGE_PAYLOADS;
31319
+ return join8(DATA_ROOT, "payloads");
31320
+ }
31321
+ function templatesDir() {
31322
+ if (existsSync5(PACKAGE_TEMPLATES))
31323
+ return PACKAGE_TEMPLATES;
31324
+ return join8(DATA_ROOT, "templates");
31325
+ }
31326
+ var PAYLOADS = payloadsDir();
31327
+ var TEMPLATES = templatesDir();
31328
+ function listPayloadCategories() {
31126
31329
  try {
31127
- text = readFileSync9(path, "utf8");
31330
+ return readdirSync3(PAYLOADS, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
31128
31331
  } catch {
31129
- return result;
31332
+ return [];
31130
31333
  }
31131
- const lines = text.split(`
31132
- `);
31133
- const seen = new Set;
31134
- const sinks = scan.sinks.filter((s) => {
31135
- const cls = classifySink2(s.sink);
31136
- if (!cls)
31137
- return false;
31138
- const key = `${cls.id}|${s.line}`;
31139
- if (seen.has(key))
31140
- return false;
31141
- seen.add(key);
31142
- return true;
31334
+ }
31335
+ function payloadLookup(topic) {
31336
+ const cats = listPayloadCategories();
31337
+ const key = topic.toLowerCase();
31338
+ const ALIASES = {
31339
+ sqli: "sql injection",
31340
+ xss: "xss injection",
31341
+ ssrf: "server side request forgery",
31342
+ ssti: "server side template injection",
31343
+ jwt: "json web token",
31344
+ lfi: "file inclusion",
31345
+ rfi: "file inclusion",
31346
+ xxe: "xxe injection",
31347
+ idor: "insecure direct object references",
31348
+ csrf: "cross-site request forgery",
31349
+ cmdi: "command injection",
31350
+ rce: "command injection",
31351
+ cors: "cors misconfiguration",
31352
+ crlf: "crlf injection",
31353
+ prototype: "prototype pollution",
31354
+ nosql: "nosql injection",
31355
+ upload: "upload insecure files"
31356
+ };
31357
+ const normalized = ALIASES[key] ?? key;
31358
+ const exact = cats.find((c) => c.toLowerCase() === normalized);
31359
+ if (exact) {
31360
+ return { found: true, category: exact, files: listFiles(join8(PAYLOADS, exact)) };
31361
+ }
31362
+ const keyTokens = normalized.split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
31363
+ const matches = cats.filter((c) => {
31364
+ const cl = c.toLowerCase();
31365
+ if (cl.includes(normalized) || normalized.includes(cl))
31366
+ return true;
31367
+ return keyTokens.some((tok) => new RegExp(`(^|[^a-z0-9])${tok}`).test(cl));
31143
31368
  });
31144
- for (const s of sinks) {
31145
- const sinkClass = classifySink2(s.sink);
31146
- const wideStart = Math.max(0, s.line - 15);
31147
- const wideWindow = lines.slice(wideStart, s.line).join(`
31148
- `);
31149
- const sanitizerWindow = lines.slice(Math.max(0, s.line - 3), s.line + 1).join(`
31150
- `);
31151
- const sinkLine = lines[s.line - 1] ?? "";
31152
- const prevLine = lines[s.line - 2] ?? "";
31153
- const authContext = sinkLine + `
31154
- ` + prevLine;
31155
- const source = classifySource(wideWindow);
31156
- const sanitizers = findSanitizers(sanitizerWindow);
31157
- const authGates = AUTH_GATES.filter((g) => authContext.includes(g));
31158
- const edge = {
31159
- source,
31160
- sanitizers,
31161
- auth_gates: authGates,
31162
- sink: sinkClass,
31163
- sink_line: s.line,
31164
- sink_token: s.sink
31165
- };
31166
- if (authGates.length > 0) {
31167
- result.authorized.push(edge);
31168
- continue;
31169
- }
31170
- if (isSanitized(sanitizers, sinkClass.id)) {
31171
- result.sanitized.push(edge);
31172
- continue;
31173
- }
31174
- result.candidates.push(edge);
31369
+ if (matches.length > 0) {
31370
+ return { found: true, category: matches[0], files: listFiles(join8(PAYLOADS, matches[0])), alternatives: matches.slice(0, 10) };
31175
31371
  }
31176
- return result;
31372
+ return { found: false, topic, suggestions: cats.slice(0, 20) };
31177
31373
  }
31178
- function groupVariants(results) {
31179
- const groups = new Map;
31180
- for (const r of results) {
31181
- for (const c of r.candidates) {
31182
- const sig = `${c.sink?.category ?? "?"}|${c.source?.id ?? "unknown"}`;
31183
- const g = groups.get(sig);
31184
- if (g) {
31185
- g.occurrences += 1;
31186
- if (!g.files.includes(r.file))
31187
- g.files.push(r.file);
31188
- } else {
31189
- groups.set(sig, {
31190
- signature: sig,
31191
- root_cause: `unsanitized ${c.source?.label ?? "unknown"} reaching ${c.sink?.category ?? "sink"}`,
31192
- sink_category: c.sink?.category ?? "?",
31193
- cwe: c.sink?.cwe,
31194
- occurrences: 1,
31195
- files: [r.file]
31196
- });
31197
- }
31374
+ function readPayload(category, file) {
31375
+ const cats = listPayloadCategories();
31376
+ const exact = cats.find((c) => c.toLowerCase() === category.toLowerCase());
31377
+ if (!exact)
31378
+ return { found: false, category, suggestions: cats.slice(0, 20) };
31379
+ const dir = join8(PAYLOADS, exact);
31380
+ const files = listFiles(dir);
31381
+ if (!file) {
31382
+ const readme = files.find((f) => f.toLowerCase() === "readme.md");
31383
+ if (readme) {
31384
+ return { found: true, category: exact, file: readme, content: readFileSync8(join8(dir, readme), "utf8").slice(0, 20000) };
31198
31385
  }
31386
+ return { found: true, category: exact, files };
31199
31387
  }
31200
- return [...groups.values()].sort((a, b) => b.occurrences - a.occurrences);
31201
- }
31202
-
31203
- // src/taint.ts
31204
- init_scanner();
31205
- var import_php_parser = __toESM(require_src(), 1);
31206
- import { readFileSync as readFileSync10 } from "node:fs";
31207
- var parser = new import_php_parser.Engine({
31208
- parser: { extractDoc: false, php7: true },
31209
- ast: { withPositions: true, withSource: false }
31210
- });
31211
- function isNode(v) {
31212
- return v && typeof v === "object" && typeof v.kind === "string";
31388
+ const f = files.find((x) => x.toLowerCase() === file.toLowerCase());
31389
+ if (!f)
31390
+ return { found: false, category: exact, file, suggestions: files };
31391
+ return { found: true, category: exact, file: f, content: readFileSync8(join8(dir, f), "utf8").slice(0, 20000) };
31213
31392
  }
31214
- function children(node) {
31215
- const out = [];
31216
- for (const k of Object.keys(node)) {
31217
- if (k === "loc")
31218
- continue;
31219
- const v = node[k];
31220
- if (Array.isArray(v)) {
31221
- for (const c of v)
31222
- if (isNode(c))
31223
- out.push(c);
31224
- } else if (isNode(v)) {
31225
- out.push(v);
31226
- }
31393
+ function listFiles(dir) {
31394
+ try {
31395
+ return readdirSync3(dir).filter((f) => f.endsWith(".md") || f.endsWith(".txt") || f.endsWith(".yaml")).sort();
31396
+ } catch {
31397
+ return [];
31227
31398
  }
31228
- return out;
31229
31399
  }
31230
- function findKind(node, kind, out = []) {
31231
- if (!isNode(node))
31232
- return out;
31233
- if (node.kind === kind)
31234
- out.push(node);
31235
- for (const c of children(node))
31236
- findKind(c, kind, out);
31237
- return out;
31238
- }
31239
- function rootVarName(node) {
31240
- if (!isNode(node))
31241
- return null;
31242
- if (node.kind === "variable")
31243
- return node.name ?? null;
31244
- if (node.kind === "name")
31245
- return typeof node.name === "string" ? node.name : node.name?.name ?? null;
31246
- if (node.kind === "offsetlookup" || node.kind === "propertylookup")
31247
- return rootVarName(node.what);
31248
- return null;
31249
- }
31250
- function sourceOf(node) {
31251
- if (!isNode(node))
31252
- return null;
31253
- if (node.kind === "offsetlookup" && node.what?.kind === "variable") {
31254
- const n = node.what.name;
31255
- if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
31256
- return "$" + n;
31257
- }
31258
- if (node.kind === "variable") {
31259
- const n = node.name;
31260
- if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
31261
- return "$" + n;
31262
- }
31263
- if (node.kind === "call") {
31264
- const n = rootVarName(node.what);
31265
- if (n === "getallheaders")
31266
- return "getallheaders";
31267
- if (n === "file_get_contents" && JSON.stringify(node).includes("php://input"))
31268
- return "php://input";
31269
- }
31270
- return null;
31271
- }
31272
- function sinkClassOf(call) {
31273
- const what = call.what;
31274
- if (!isNode(what))
31275
- return null;
31276
- let token = "";
31277
- if (what.kind === "propertylookup") {
31278
- const base = rootVarName(what.what);
31279
- const method = what.offset?.name ?? what.offset?.value;
31280
- token = `${base}->${method}(`;
31281
- } else if (what.kind === "name") {
31282
- token = `${what.name}(`;
31283
- }
31284
- if (!token)
31285
- return null;
31286
- const cls = classifySink2(token) ?? classifySink2(what.name ?? "");
31287
- if (!cls)
31288
- return null;
31289
- return { token, cls };
31290
- }
31291
- function analyzeBlock(statements, env, summaries, findings, suppressed, params = [], file) {
31292
- for (const stmt of statements) {
31293
- if (!isNode(stmt))
31294
- continue;
31295
- const kind = stmt.kind;
31296
- if (kind === "expressionstatement") {
31297
- const expr = stmt.expression;
31298
- walkExpression(expr, env, summaries, findings, suppressed, file);
31299
- } else if (kind === "if") {
31300
- const ifEnv = new Map(env);
31301
- const bodyStmts = stmt.body?.kind === "block" ? stmt.body.children : [stmt.body];
31302
- analyzeBlock(bodyStmts, ifEnv, summaries, findings, suppressed, params, file);
31303
- if (stmt.alternate) {
31304
- const altEnv = new Map(env);
31305
- const altStmts = stmt.alternate.kind === "block" ? stmt.alternate.children : [stmt.alternate];
31306
- analyzeBlock(altStmts, altEnv, summaries, findings, suppressed, params, file);
31307
- }
31308
- } else if (kind === "block") {
31309
- analyzeBlock(stmt.children ?? [], env, summaries, findings, suppressed, params, file);
31310
- } else if (kind === "return" || kind === "echo" || kind === "print") {
31311
- walkExpression(stmt, env, summaries, findings, suppressed, file);
31312
- } else {
31313
- for (const c of children(stmt)) {
31314
- if (c.kind === "block" || c.kind === "if") {
31315
- analyzeBlock([c], new Map(env), summaries, findings, suppressed, params, file);
31316
- }
31400
+ function countTemplates() {
31401
+ try {
31402
+ let n = 0;
31403
+ const walk = (dir) => {
31404
+ for (const e of readdirSync3(dir, { withFileTypes: true })) {
31405
+ if (e.isDirectory())
31406
+ walk(join8(dir, e.name));
31407
+ else if (e.name.endsWith(".yaml"))
31408
+ n++;
31317
31409
  }
31318
- }
31410
+ };
31411
+ walk(TEMPLATES);
31412
+ return n;
31413
+ } catch {
31414
+ return 0;
31319
31415
  }
31320
31416
  }
31321
- function walkExpression(node, env, summaries, findings, suppressed, file) {
31322
- if (!isNode(node))
31323
- return;
31324
- if (node.kind === "assign") {
31325
- const varName = rootVarName(node.left);
31326
- const taint = evalTaint(node.right, env, summaries);
31327
- if (varName) {
31328
- env.set(varName, taint);
31329
- }
31330
- return;
31331
- }
31332
- if (node.kind === "call") {
31333
- const sink = sinkClassOf(node);
31334
- if (sink) {
31335
- const argTaints = node.arguments.map((a) => {
31336
- return evalTaint(a, env, summaries);
31337
- });
31338
- const tainted = argTaints.filter((t) => t.tainted);
31339
- if (tainted.length > 0) {
31340
- const sanitizers = tainted.flatMap((t) => t.sanitizers);
31341
- const inlineSan = findSanitizers(JSON.stringify(node));
31342
- const san = [...sanitizers, ...inlineSan].filter((s, i, arr) => arr.indexOf(s) === i);
31343
- const cls = sink.cls;
31344
- if (!isSanitized(san, cls.id)) {
31345
- findings.push({
31346
- sink: sink.token,
31347
- sink_line: node.loc?.start?.line ?? 0,
31348
- category: cls.category,
31349
- cwe: cls.cwe,
31350
- variables: tainted.map((t) => t.source ?? "var"),
31351
- source: tainted[0].source,
31352
- source_line: tainted[0].sourceLine,
31353
- sanitized: false,
31354
- sanitizers: san.map((s) => s.id),
31355
- auth_gated: false,
31356
- interprocedural: false
31357
- });
31358
- } else {
31359
- suppressed.n += 1;
31360
- }
31361
- }
31362
- for (const a of node.arguments)
31363
- walkExpression(a, env, summaries, findings, suppressed, file);
31417
+ function templateLookup(topic, limit = 10) {
31418
+ const key = topic.toLowerCase();
31419
+ const hits = [];
31420
+ const walk = (dir, depth = 0) => {
31421
+ if (depth > 6 || hits.length >= limit * 3)
31364
31422
  return;
31365
- }
31366
- const fnName = rootVarName(node.what);
31367
- if (fnName && summaries.has(fnName)) {
31368
- const sum = summaries.get(fnName);
31369
- const argTaints = node.arguments.map((a) => {
31370
- const v = a;
31371
- return evalTaint(v, env, summaries);
31372
- });
31373
- const taintedArgs = argTaints.filter((t) => t.tainted);
31374
- if (taintedArgs.length > 0 && sum.sinkParams.size > 0) {
31375
- const firstSink = [...sum.sinkParams.values()][0];
31376
- const cls = classifySink2(firstSink.sink);
31377
- if (cls) {
31378
- const san = [
31379
- ...taintedArgs.flatMap((t) => t.sanitizers),
31380
- ...[...sum.sanitizedParams.values()].flat()
31381
- ];
31382
- if (!isSanitized(san, cls.id)) {
31383
- findings.push({
31384
- sink: firstSink.sink,
31385
- sink_line: firstSink.line,
31386
- category: cls.category,
31387
- cwe: cls.cwe,
31388
- variables: taintedArgs.map((t) => t.source ?? "var"),
31389
- source: taintedArgs[0].source,
31390
- source_line: taintedArgs[0].sourceLine,
31391
- sanitized: false,
31392
- sanitizers: san.map((s) => s.id),
31393
- auth_gated: false,
31394
- interprocedural: true
31395
- });
31396
- } else {
31397
- suppressed.n += 1;
31398
- }
31399
- }
31400
- }
31423
+ let entries;
31424
+ try {
31425
+ entries = readdirSync3(dir, { withFileTypes: true });
31426
+ } catch {
31401
31427
  return;
31402
31428
  }
31403
- for (const c of children(node))
31404
- walkExpression(c, env, summaries, findings, suppressed, file);
31405
- return;
31406
- }
31407
- if (node.kind === "echo" || node.kind === "print") {
31408
- const exprs = node.expressions ?? [node.expression];
31409
- for (const e of exprs) {
31410
- const cls = classifySink2(node.kind === "echo" ? "echo " : "print ");
31411
- const t = evalTaint(e, env, summaries);
31412
- if (t.tainted && cls) {
31413
- const san = [...t.sanitizers, ...findSanitizers(JSON.stringify(e))];
31414
- if (!isSanitized(san, cls.id)) {
31415
- findings.push({
31416
- sink: node.kind + " ",
31417
- sink_line: node.loc?.start?.line ?? 0,
31418
- category: cls.category,
31419
- cwe: cls.cwe,
31420
- variables: [t.source ?? "var"],
31421
- source: t.source,
31422
- source_line: t.sourceLine,
31423
- sanitized: false,
31424
- sanitizers: san.map((s) => s.id),
31425
- auth_gated: false,
31426
- interprocedural: false
31427
- });
31428
- } else {
31429
- suppressed.n += 1;
31429
+ for (const e of entries) {
31430
+ if (e.isDirectory())
31431
+ walk(join8(dir, e.name), depth + 1);
31432
+ else if (e.name.endsWith(".yaml")) {
31433
+ const name = e.name.toLowerCase();
31434
+ if (name.includes(key)) {
31435
+ const full = join8(dir, e.name);
31436
+ let id = "";
31437
+ try {
31438
+ const content = readFileSync8(full, "utf8");
31439
+ const m = content.match(/^\s*id:\s*(.+)$/m);
31440
+ if (m)
31441
+ id = m[1].trim();
31442
+ } catch {}
31443
+ hits.push({ path: full.replace(ROOT3 + "/", ""), name: e.name, id });
31430
31444
  }
31431
31445
  }
31432
31446
  }
31433
- return;
31447
+ };
31448
+ walk(TEMPLATES);
31449
+ return { found: hits.length > 0, topic, count: hits.length, templates: hits.slice(0, limit) };
31450
+ }
31451
+
31452
+ // src/active.ts
31453
+ var TIMEOUT_MS2 = 20000;
31454
+ async function httpFetch(url, init) {
31455
+ const controller = new AbortController;
31456
+ const t = setTimeout(() => controller.abort(), TIMEOUT_MS2);
31457
+ try {
31458
+ const res = await fetch(url, { ...init, signal: controller.signal, redirect: "follow" });
31459
+ const body = await res.text();
31460
+ const headers = {};
31461
+ res.headers.forEach((v, k) => {
31462
+ headers[k] = v;
31463
+ });
31464
+ return { status: res.status, headers, body: body.slice(0, 20000), ok: res.ok };
31465
+ } catch (e) {
31466
+ return { status: 0, headers: {}, body: String(e), ok: false };
31467
+ } finally {
31468
+ clearTimeout(t);
31434
31469
  }
31435
- for (const c of children(node))
31436
- walkExpression(c, env, summaries, findings, suppressed, file);
31437
31470
  }
31438
- function evalTaint(node, env, summaries) {
31439
- if (!isNode(node))
31440
- return { tainted: false, sanitizers: [] };
31441
- const src = sourceOf(node);
31442
- if (src)
31443
- return { tainted: true, sanitizers: [], source: src, sourceLine: node.loc?.start?.line };
31444
- if (node.kind === "variable" || node.kind === "offsetlookup") {
31445
- const root = rootVarName(node);
31446
- if (root) {
31447
- const st = env.get(root);
31448
- if (st)
31449
- return { ...st };
31450
- }
31451
- if (node.kind === "offsetlookup") {
31452
- const whatTaint = evalTaint(node.what, env, summaries);
31453
- if (whatTaint.tainted)
31454
- return whatTaint;
31455
- }
31456
- return { tainted: false, sanitizers: [] };
31457
- }
31458
- if (node.kind === "call") {
31459
- const fnName = rootVarName(node.what);
31460
- const san = findSanitizers(fnName ? fnName + "(" : "");
31461
- if (san.length > 0) {
31462
- const inner = node.arguments.map((a) => evalTaint(a, env, summaries));
31463
- const tainted = inner.some((t) => t.tainted);
31464
- return {
31465
- tainted,
31466
- sanitizers: tainted ? [...inner.flatMap((t) => t.sanitizers), ...san] : [],
31467
- source: tainted ? inner.find((t) => t.tainted)?.source : undefined,
31468
- sourceLine: tainted ? inner.find((t) => t.tainted)?.sourceLine : undefined
31469
- };
31470
- }
31471
- if (fnName && summaries.has(fnName)) {
31472
- const sum = summaries.get(fnName);
31473
- const argTaints = node.arguments.map((a) => evalTaint(a, env, summaries));
31474
- const tainted = argTaints.filter((t) => t.tainted);
31475
- if (tainted.length > 0) {
31476
- const helperSan = [...sum.sanitizedParams.values()].flat();
31477
- return {
31478
- tainted: true,
31479
- sanitizers: [...tainted.flatMap((t) => t.sanitizers), ...helperSan],
31480
- source: tainted[0].source,
31481
- sourceLine: tainted[0].sourceLine
31482
- };
31483
- }
31484
- }
31485
- return { tainted: false, sanitizers: [] };
31486
- }
31487
- if (node.kind === "bin" || node.kind === "concat" || node.kind === "encapsed") {
31488
- const parts = children(node);
31489
- const taints = parts.map((p) => evalTaint(p, env, summaries));
31490
- const tainted = taints.find((t) => t.tainted);
31491
- if (tainted)
31492
- return { ...tainted, sanitizers: taints.flatMap((t) => t.sanitizers) };
31493
- return { tainted: false, sanitizers: [] };
31471
+ function normalizeTarget(target) {
31472
+ let t = target.trim();
31473
+ if (!/^https?:\/\//i.test(t))
31474
+ t = `https://${t}`;
31475
+ return t.replace(/\/+$/, "");
31476
+ }
31477
+ function extractTitle(body) {
31478
+ const m = body.match(/<title[^>]*>([^<]+)<\/title>/i);
31479
+ return m ? m[1].trim().slice(0, 200) : "";
31480
+ }
31481
+ function detectServer(headers) {
31482
+ return headers["server"] ?? headers["x-powered-by"] ?? "";
31483
+ }
31484
+ function detectTechFromBody(body) {
31485
+ const hits = [];
31486
+ const sigs = [
31487
+ [/wp-content|wp-includes|wp-json/i, "wordpress"],
31488
+ [/wp-login\.php|wp-admin/i, "wordpress"],
31489
+ [/powered by joomla|com_content/i, "joomla"],
31490
+ [/laravel|_token|csrf-token/i, "laravel"],
31491
+ [/react|__NEXT_DATA__|next\/static/i, "nextjs"],
31492
+ [/angular|ng-version/i, "angular"],
31493
+ [/vue|__vue__|v-data/i, "vue"],
31494
+ [/django|csrftoken|__debug__/i, "django"],
31495
+ [/ruby on rails|rails/i, "rails"],
31496
+ [/asp\.net|__VIEWSTATE|__EVENTVALIDATION/i, "aspnet"],
31497
+ [/phpBB|phpbb/i, "phpbb"],
31498
+ [/mybb|mybb/i, "mybb"],
31499
+ [/drupal|Drupal\.settings/i, "drupal"],
31500
+ [/magento|Mage\./i, "magento"],
31501
+ [/shopify|cdn\.shopify/i, "shopify"],
31502
+ [/grafana|kibana|elastic/i, "grafana-kibana"]
31503
+ ];
31504
+ for (const [re, name] of sigs) {
31505
+ if (re.test(body))
31506
+ hits.push(name);
31494
31507
  }
31495
- if (node.kind === "encapsedpart") {
31496
- return evalTaint(node.expression, env, summaries);
31508
+ return [...new Set(hits)];
31509
+ }
31510
+ async function probeCommonPaths(base) {
31511
+ const paths = [
31512
+ "/robots.txt",
31513
+ "/sitemap.xml",
31514
+ "/.well-known/security.txt",
31515
+ "/wp-login.php",
31516
+ "/wp-admin/",
31517
+ "/wp-json/",
31518
+ "/administrator/",
31519
+ "/admin/",
31520
+ "/login",
31521
+ "/api/",
31522
+ "/.git/config",
31523
+ "/.env",
31524
+ "/config.php",
31525
+ "/phpinfo.php",
31526
+ "/swagger.json",
31527
+ "/swagger-ui.html",
31528
+ "/graphql",
31529
+ "/actuator/health"
31530
+ ];
31531
+ const out = [];
31532
+ for (const p of paths) {
31533
+ const r = await httpFetch(`${base}${p}`, { method: "GET" });
31534
+ out.push({ path: p, status: r.status });
31497
31535
  }
31498
- return { tainted: false, sanitizers: [] };
31536
+ return out;
31499
31537
  }
31500
- function analyzeTaint(path) {
31501
- const result = { file: path, findings: [], suppressed: 0 };
31502
- let code;
31503
- try {
31504
- code = readFileSync10(path, "utf8");
31505
- } catch {
31506
- return result;
31538
+ async function activeScan(target, scope = "", mode = "bug-bounty", allowed = undefined) {
31539
+ const base = normalizeTarget(target);
31540
+ const host = (() => {
31541
+ try {
31542
+ return new URL(base).hostname;
31543
+ } catch {
31544
+ return target;
31545
+ }
31546
+ })();
31547
+ if (allowed === false) {
31548
+ return {
31549
+ target: base,
31550
+ gated: true,
31551
+ gate_reason: `active scanning not authorized for '${host}' (mode=${mode}, scope='${scope || "unset"}')`
31552
+ };
31507
31553
  }
31508
- let ast;
31509
- try {
31510
- ast = parser.parseCode(code, path);
31511
- } catch {
31554
+ const result = { target: base, gated: false };
31555
+ const root = await httpFetch(base);
31556
+ if (root.status === 0) {
31557
+ result.gated = true;
31558
+ result.gate_reason = `could not reach ${base}: ${root.body.slice(0, 120)}`;
31512
31559
  return result;
31513
31560
  }
31514
- const functions = findKind(ast, "function");
31515
- const summaries = new Map;
31516
- for (const fn of functions) {
31517
- const name = typeof fn.name === "string" ? fn.name : fn.name?.name ?? "<anonymous>";
31518
- const params = (fn.arguments ?? []).map((a) => typeof a === "string" ? a : typeof a.name === "string" ? a.name : a.name?.name ?? "?");
31519
- const summary = { name, params, sinkParams: new Map, sanitizedParams: new Map };
31520
- const env = new Map;
31521
- for (const p of params)
31522
- env.set(p, { tainted: false, sanitizers: [] });
31523
- const calls = findKind(fn, "call");
31524
- for (const call of calls) {
31525
- const sink = sinkClassOf(call);
31526
- if (!sink)
31527
- continue;
31528
- const line = call.loc?.start?.line ?? 0;
31529
- const varsInCall = findKind(call, "variable").map((v) => v.name).filter(Boolean);
31530
- for (const v of varsInCall) {
31531
- if (params.includes(v))
31532
- summary.sinkParams.set(v, { sink: sink.token, line });
31533
- }
31534
- const san = findSanitizers(JSON.stringify(call));
31535
- for (const v of varsInCall) {
31536
- if (params.includes(v) && san.length > 0) {
31537
- summary.sanitizedParams.set(v, [...summary.sanitizedParams.get(v) ?? [], ...san]);
31538
- }
31539
- }
31540
- }
31541
- for (const ek of ["echo", "print"]) {
31542
- for (const en of findKind(fn, ek)) {
31543
- const line = en.loc?.start?.line ?? 0;
31544
- const vars = findKind(en, "variable").map((v) => v.name).filter(Boolean);
31545
- const san = findSanitizers(JSON.stringify(en));
31546
- for (const v of vars) {
31547
- if (params.includes(v)) {
31548
- summary.sinkParams.set(v, { sink: ek + " ", line });
31549
- if (san.length > 0) {
31550
- summary.sanitizedParams.set(v, [...summary.sanitizedParams.get(v) ?? [], ...san]);
31551
- }
31552
- }
31553
- }
31554
- }
31555
- }
31556
- const assigns = findKind(fn, "assign");
31557
- for (const a of assigns) {
31558
- const san = findSanitizers(JSON.stringify(a.right));
31559
- if (san.length > 0) {
31560
- const lv = rootVarName(a.left);
31561
- if (lv && params.includes(lv)) {
31562
- summary.sanitizedParams.set(lv, [...summary.sanitizedParams.get(lv) ?? [], ...san]);
31563
- }
31564
- }
31565
- }
31566
- for (const ek of ["echo", "print"]) {
31567
- for (const en of findKind(fn, ek)) {
31568
- const san = findSanitizers(JSON.stringify(en));
31569
- if (san.length > 0) {
31570
- for (const v of findKind(en, "variable").map((v2) => v2.name).filter(Boolean)) {
31571
- if (params.includes(v)) {
31572
- summary.sanitizedParams.set(v, [...summary.sanitizedParams.get(v) ?? [], ...san]);
31573
- }
31574
- }
31575
- }
31576
- }
31577
- }
31578
- summaries.set(name, summary);
31561
+ result.waf = detectWaf(root.headers, root.body);
31562
+ const interestingHeaders = {};
31563
+ for (const k of ["server", "x-powered-by", "x-aspnet-version", "x-drupal-cache", "x-generator", "via", "x-cache", "cf-ray"]) {
31564
+ if (root.headers[k])
31565
+ interestingHeaders[k] = root.headers[k];
31579
31566
  }
31580
- const topStmts = [];
31581
- const body = ast.children ?? [];
31582
- for (const s of body) {
31583
- if (isNode(s) && s.kind !== "function")
31584
- topStmts.push(s);
31567
+ result.fingerprint = {
31568
+ status: root.status,
31569
+ title: extractTitle(root.body),
31570
+ server: detectServer(root.headers),
31571
+ headers: interestingHeaders,
31572
+ body_size: root.body.length
31573
+ };
31574
+ const techs = detectTechFromBody(root.body);
31575
+ const serverTech = detectServer(root.headers);
31576
+ result.tech = {
31577
+ detected: techs,
31578
+ server: serverTech,
31579
+ correlations: techs.map((t) => techCorrelation(t)).filter((c) => c.found === true)
31580
+ };
31581
+ result.endpoints = await probeCommonPaths(base);
31582
+ const recs = [];
31583
+ if (result.waf?.detected)
31584
+ recs.push(`WAF detected (${result.waf.wafs?.map((w) => w.waf).join(", ")}) — apply WAF bypass before exploitation.`);
31585
+ const interesting = result.endpoints?.filter((e) => e.status >= 200 && e.status < 400 && !e.path.includes(".well-known"));
31586
+ if (interesting && interesting.length > 0) {
31587
+ recs.push(`Live endpoints found: ${interesting.map((e) => e.path).join(", ")} — trace these with EAGLE-EYE / verify with STRIKE.`);
31585
31588
  }
31586
- const globalEnv = new Map;
31587
- const suppressRef = { n: 0 };
31588
- analyzeBlock(topStmts, globalEnv, summaries, result.findings, suppressRef, [], path);
31589
- result.suppressed = suppressRef.n;
31589
+ for (const c of result.tech?.correlations ?? []) {
31590
+ recs.push(`Tech '${c.tech}' maps to known vuln classes: ${(c.vulns ?? []).slice(0, 3).join(", ")}`);
31591
+ }
31592
+ result.recommendations = recs;
31590
31593
  return result;
31591
31594
  }
31592
- function taintTree(root, maxFiles = 2000) {
31593
- const files = iterSourceFiles(root, maxFiles);
31594
- return files.map((f) => analyzeTaint(f));
31595
- }
31596
31595
 
31597
- // src/eagle2.ts
31598
- var import_php_parser3 = __toESM(require_src(), 1);
31599
- import { readFileSync as readFileSync12 } from "node:fs";
31596
+ // src/dataflow.ts
31600
31597
  init_scanner();
31598
+ import { readFileSync as readFileSync9 } from "node:fs";
31599
+ var SOURCES = {
31600
+ $_GET: { id: "http_get", label: "HTTP GET parameter", attacker_controlled: true },
31601
+ $_POST: { id: "http_post", label: "HTTP POST body", attacker_controlled: true },
31602
+ $_REQUEST: { id: "http_request", label: "HTTP request (merged)", attacker_controlled: true },
31603
+ $_COOKIE: { id: "http_cookie", label: "HTTP cookie", attacker_controlled: true },
31604
+ $_FILES: { id: "uploaded_file", label: "Uploaded file", attacker_controlled: true },
31605
+ $_SERVER: { id: "http_header", label: "HTTP header / server env", attacker_controlled: true },
31606
+ "file_get_contents('php://input')": { id: "raw_body", label: "Raw request body", attacker_controlled: true },
31607
+ "php://input": { id: "raw_body", label: "Raw request body", attacker_controlled: true },
31608
+ json_decode: { id: "json_input", label: "JSON input", attacker_controlled: true },
31609
+ getallheaders: { id: "http_header", label: "HTTP header", attacker_controlled: true },
31610
+ $argv: { id: "cli_argument", label: "CLI argument", attacker_controlled: false },
31611
+ getenv: { id: "environment_variable", label: "Environment variable", attacker_controlled: false },
31612
+ "$wpdb->get_results": { id: "database_value", label: "Database value", attacker_controlled: false },
31613
+ wp_remote_get: { id: "external_api", label: "External API data", attacker_controlled: false }
31614
+ };
31615
+ function classifySource(line) {
31616
+ for (const [token, cls] of Object.entries(SOURCES)) {
31617
+ if (line.includes(token))
31618
+ return cls;
31619
+ }
31620
+ return null;
31621
+ }
31622
+ var SINK_CLASS_MAP = [
31623
+ [/->query\(|\$wpdb->query|\$wpdb->get_var|\$wpdb->get_results|->whereRaw|->selectRaw/, { id: "sql_execution", category: "SQL execution", cwe: "CWE-89" }],
31624
+ [/\beval\(|\bassert\(|\bcreate_function\(|\bcall_user_func\(|\bpreg_replace\(/, { id: "code_execution", category: "Dynamic evaluation / code execution", cwe: "CWE-94" }],
31625
+ [/\bsystem\(|\bexec\(|\bshell_exec\(|\bpassthru\(|\bproc_open\(|\bpopen\(/, { id: "command_execution", category: "Command execution", cwe: "CWE-78" }],
31626
+ [/\bmove_uploaded_file\(|\bfile_put_contents\(|\bfwrite\(|\bfopen\(|\bunlink\(/, { id: "file_operations", category: "File operations", cwe: "CWE-434" }],
31627
+ [/\binclude\(|\brequire\(|\binclude_once\(|\brequire_once\(/, { id: "file_inclusion", category: "File inclusion", cwe: "CWE-98" }],
31628
+ [/\bunserialize\(|\bmaybe_unserialize\(/, { id: "deserialization", category: "Deserialization", cwe: "CWE-502" }],
31629
+ [/\bwp_remote_get\(|\bwp_remote_post\(|\bfile_get_contents\(|\bcurl_exec\(|\bcurl_setopt\(/, { id: "http_request", category: "HTTP request (SSRF)", cwe: "CWE-918" }],
31630
+ [/\bheader\(|\bwp_redirect\(/, { id: "redirect", category: "Redirect handling", cwe: "CWE-601" }],
31631
+ [/echo\s|print\s|\bprintf\(/, { id: "html_render", category: "HTML rendering (XSS)", cwe: "CWE-79" }],
31632
+ [/\bsimplexml_load_string\(|new SimpleXMLElement|DOMDocument/, { id: "xml_processing", category: "XML processing (XXE)", cwe: "CWE-611" }],
31633
+ [/\bZipArchive|\bPharData|->extractTo\(/, { id: "archive_extraction", category: "Archive extraction (zip slip)", cwe: "CWE-22" }]
31634
+ ];
31635
+ function classifySink2(sink) {
31636
+ for (const [re, cls] of SINK_CLASS_MAP) {
31637
+ if (re.test(sink))
31638
+ return cls;
31639
+ }
31640
+ return null;
31641
+ }
31642
+ var SANITIZERS = {
31643
+ htmlspecialchars: { id: "html_escape", label: "HTML entity encoding", neutralizes: ["html_render"] },
31644
+ esc_html: { id: "html_escape", label: "HTML escape", neutralizes: ["html_render"] },
31645
+ esc_attr: { id: "html_escape", label: "HTML attribute escape", neutralizes: ["html_render"] },
31646
+ htmlentities: { id: "html_escape", label: "HTML entity encoding", neutralizes: ["html_render"] },
31647
+ esc_sql: { id: "sql_escape", label: "SQL escape", neutralizes: ["sql_execution"] },
31648
+ "$wpdb->prepare": { id: "sql_prepare", label: "Prepared SQL statement", neutralizes: ["sql_execution"] },
31649
+ "->prepare(": { id: "sql_prepare", label: "Prepared statement", neutralizes: ["sql_execution"] },
31650
+ filter_var: { id: "filter_var", label: "Filter input", neutralizes: ["html_render", "sql_execution", "command_execution"] },
31651
+ filter_input: { id: "filter_var", label: "Filter input", neutralizes: ["html_render", "sql_execution", "command_execution"] },
31652
+ sanitize_text_field: { id: "wp_sanitize", label: "WordPress sanitize", neutralizes: ["html_render"] },
31653
+ sanitize_file_name: { id: "wp_sanitize_file", label: "File-name sanitize", neutralizes: ["file_operations", "file_inclusion"] },
31654
+ wp_verify_nonce: { id: "nonce_check", label: "Nonce verification", neutralizes: ["authorization"] },
31655
+ check_ajax_referer: { id: "nonce_check", label: "Nonce verification", neutralizes: ["authorization"] },
31656
+ escapeshellarg: { id: "shell_escape", label: "Shell argument escape", neutralizes: ["command_execution"] },
31657
+ escapeshellcmd: { id: "shell_escape", label: "Shell command escape", neutralizes: ["command_execution"] },
31658
+ intval: { id: "int_cast", label: "Integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
31659
+ absint: { id: "int_cast", label: "Absolute integer cast", neutralizes: ["sql_execution", "command_execution", "file_inclusion"] },
31660
+ "preg_replace.*FILTER": { id: "regex_filter", label: "Regex filter", neutralizes: ["html_render"] }
31661
+ };
31662
+ function findSanitizers(line) {
31663
+ const out = [];
31664
+ for (const [token, san] of Object.entries(SANITIZERS)) {
31665
+ if (line.includes(token))
31666
+ out.push(san);
31667
+ }
31668
+ return out;
31669
+ }
31670
+ function isSanitized(sanitizers, sinkCategory) {
31671
+ return sanitizers.some((s) => s.neutralizes.includes(sinkCategory) || s.neutralizes.includes("authorization"));
31672
+ }
31673
+ function traceDataFlow(path) {
31674
+ const scan = scanFile(path);
31675
+ const result = { file: path, candidates: [], sanitized: [], authorized: [] };
31676
+ if (scan.error)
31677
+ return result;
31678
+ let text;
31679
+ try {
31680
+ text = readFileSync9(path, "utf8");
31681
+ } catch {
31682
+ return result;
31683
+ }
31684
+ const lines = text.split(`
31685
+ `);
31686
+ const seen = new Set;
31687
+ const sinks = scan.sinks.filter((s) => {
31688
+ const cls = classifySink2(s.sink);
31689
+ if (!cls)
31690
+ return false;
31691
+ const key = `${cls.id}|${s.line}`;
31692
+ if (seen.has(key))
31693
+ return false;
31694
+ seen.add(key);
31695
+ return true;
31696
+ });
31697
+ for (const s of sinks) {
31698
+ const sinkClass = classifySink2(s.sink);
31699
+ const wideStart = Math.max(0, s.line - 15);
31700
+ const wideWindow = lines.slice(wideStart, s.line).join(`
31701
+ `);
31702
+ const sanitizerWindow = lines.slice(Math.max(0, s.line - 3), s.line + 1).join(`
31703
+ `);
31704
+ const sinkLine = lines[s.line - 1] ?? "";
31705
+ const prevLine = lines[s.line - 2] ?? "";
31706
+ const authContext = sinkLine + `
31707
+ ` + prevLine;
31708
+ const source = classifySource(wideWindow);
31709
+ const sanitizers = findSanitizers(sanitizerWindow);
31710
+ const authGates = AUTH_GATES.filter((g) => authContext.includes(g));
31711
+ const edge = {
31712
+ source,
31713
+ sanitizers,
31714
+ auth_gates: authGates,
31715
+ sink: sinkClass,
31716
+ sink_line: s.line,
31717
+ sink_token: s.sink
31718
+ };
31719
+ if (authGates.length > 0) {
31720
+ result.authorized.push(edge);
31721
+ continue;
31722
+ }
31723
+ if (isSanitized(sanitizers, sinkClass.id)) {
31724
+ result.sanitized.push(edge);
31725
+ continue;
31726
+ }
31727
+ result.candidates.push(edge);
31728
+ }
31729
+ return result;
31730
+ }
31731
+ function groupVariants(results) {
31732
+ const groups = new Map;
31733
+ for (const r of results) {
31734
+ for (const c of r.candidates) {
31735
+ const sig = `${c.sink?.category ?? "?"}|${c.source?.id ?? "unknown"}`;
31736
+ const g = groups.get(sig);
31737
+ if (g) {
31738
+ g.occurrences += 1;
31739
+ if (!g.files.includes(r.file))
31740
+ g.files.push(r.file);
31741
+ } else {
31742
+ groups.set(sig, {
31743
+ signature: sig,
31744
+ root_cause: `unsanitized ${c.source?.label ?? "unknown"} reaching ${c.sink?.category ?? "sink"}`,
31745
+ sink_category: c.sink?.category ?? "?",
31746
+ cwe: c.sink?.cwe,
31747
+ occurrences: 1,
31748
+ files: [r.file]
31749
+ });
31750
+ }
31751
+ }
31752
+ }
31753
+ return [...groups.values()].sort((a, b) => b.occurrences - a.occurrences);
31754
+ }
31601
31755
 
31602
- // src/callgraph.ts
31603
- var import_php_parser2 = __toESM(require_src(), 1);
31604
- import { readFileSync as readFileSync11 } from "node:fs";
31605
- var parser2 = new import_php_parser2.Engine({
31756
+ // src/taint.ts
31757
+ init_scanner();
31758
+ var import_php_parser = __toESM(require_src(), 1);
31759
+ import { readFileSync as readFileSync10 } from "node:fs";
31760
+ var parser = new import_php_parser.Engine({
31606
31761
  parser: { extractDoc: false, php7: true },
31607
31762
  ast: { withPositions: true, withSource: false }
31608
31763
  });
31609
- function isNode2(v) {
31610
- return !!v && typeof v === "object" && typeof v.kind === "string";
31764
+ function isNode(v) {
31765
+ return v && typeof v === "object" && typeof v.kind === "string";
31611
31766
  }
31612
- function children2(node) {
31767
+ function children(node) {
31613
31768
  const out = [];
31614
31769
  for (const k of Object.keys(node)) {
31615
31770
  if (k === "loc")
@@ -31617,201 +31772,397 @@ function children2(node) {
31617
31772
  const v = node[k];
31618
31773
  if (Array.isArray(v)) {
31619
31774
  for (const c of v)
31620
- if (isNode2(c))
31775
+ if (isNode(c))
31621
31776
  out.push(c);
31622
- } else if (isNode2(v)) {
31777
+ } else if (isNode(v)) {
31623
31778
  out.push(v);
31624
31779
  }
31625
31780
  }
31626
31781
  return out;
31627
31782
  }
31628
- function nameOf(node) {
31629
- if (!isNode2(node))
31783
+ function findKind(node, kind, out = []) {
31784
+ if (!isNode(node))
31785
+ return out;
31786
+ if (node.kind === kind)
31787
+ out.push(node);
31788
+ for (const c of children(node))
31789
+ findKind(c, kind, out);
31790
+ return out;
31791
+ }
31792
+ function rootVarName(node) {
31793
+ if (!isNode(node))
31630
31794
  return null;
31631
- if (typeof node.name === "string")
31632
- return node.name;
31633
- if (isNode2(node.name))
31634
- return nameOf(node.name);
31795
+ if (node.kind === "variable")
31796
+ return node.name ?? null;
31797
+ if (node.kind === "name")
31798
+ return typeof node.name === "string" ? node.name : node.name?.name ?? null;
31799
+ if (node.kind === "offsetlookup" || node.kind === "propertylookup")
31800
+ return rootVarName(node.what);
31635
31801
  return null;
31636
31802
  }
31637
- function nodeId(kind, name, cls) {
31638
- if (kind === "function")
31639
- return `func:${name}`;
31640
- if (kind === "method")
31641
- return `method:${cls ?? "?"}::${name}`;
31642
- return "main";
31803
+ function sourceOf(node) {
31804
+ if (!isNode(node))
31805
+ return null;
31806
+ if (node.kind === "offsetlookup" && node.what?.kind === "variable") {
31807
+ const n = node.what.name;
31808
+ if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
31809
+ return "$" + n;
31810
+ }
31811
+ if (node.kind === "variable") {
31812
+ const n = node.name;
31813
+ if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
31814
+ return "$" + n;
31815
+ }
31816
+ if (node.kind === "call") {
31817
+ const n = rootVarName(node.what);
31818
+ if (n === "getallheaders")
31819
+ return "getallheaders";
31820
+ if (n === "file_get_contents" && JSON.stringify(node).includes("php://input"))
31821
+ return "php://input";
31822
+ }
31823
+ return null;
31643
31824
  }
31644
- function ensureNode(g, id, name, kind, file, line, cls) {
31645
- let n = g.nodes.get(id);
31646
- if (!n) {
31647
- n = { id, name, kind, class: cls, file, line, callees: new Set, callers: new Set };
31648
- g.nodes.set(id, n);
31825
+ function sinkClassOf(call) {
31826
+ const what = call.what;
31827
+ if (!isNode(what))
31828
+ return null;
31829
+ let token = "";
31830
+ if (what.kind === "propertylookup") {
31831
+ const base = rootVarName(what.what);
31832
+ const method = what.offset?.name ?? what.offset?.value;
31833
+ token = `${base}->${method}(`;
31834
+ } else if (what.kind === "name") {
31835
+ token = `${what.name}(`;
31649
31836
  }
31650
- return n;
31837
+ if (!token)
31838
+ return null;
31839
+ const cls = classifySink2(token) ?? classifySink2(what.name ?? "");
31840
+ if (!cls)
31841
+ return null;
31842
+ return { token, cls };
31651
31843
  }
31652
- function addEdge(g, caller, callee, name, line, file) {
31653
- g.edges.push({ caller, callee, name, line, file });
31654
- const c = g.nodes.get(caller);
31655
- const t = g.nodes.get(callee);
31656
- if (c)
31657
- c.callees.add(callee);
31658
- if (t)
31659
- t.callers.add(caller);
31660
- }
31661
- function buildCallGraph(path) {
31662
- const g = { nodes: new Map, edges: [] };
31663
- let code;
31664
- try {
31665
- code = readFileSync11(path, "utf8");
31666
- } catch {
31667
- return g;
31668
- }
31669
- let ast;
31670
- try {
31671
- ast = parser2.parseCode(code, path);
31672
- } catch {
31673
- return g;
31674
- }
31675
- const mainId = nodeId("main", "main");
31676
- ensureNode(g, mainId, "<main>", "main", path, 0);
31677
- const funcs = [];
31678
- const methods = [];
31679
- walk(ast, path, (n, ctx) => {
31680
- if (n.kind === "function") {
31681
- const name = nameOf(n.name);
31682
- if (name)
31683
- funcs.push({ name, node: n });
31684
- } else if (n.kind === "method" && ctx.className) {
31685
- const mname = nameOf(n.name);
31686
- if (mname)
31687
- methods.push({ cls: ctx.className, name: mname, node: n });
31844
+ function analyzeBlock(statements, env, summaries, findings, suppressed, params = [], file) {
31845
+ for (const stmt of statements) {
31846
+ if (!isNode(stmt))
31847
+ continue;
31848
+ const kind = stmt.kind;
31849
+ if (kind === "expressionstatement") {
31850
+ const expr = stmt.expression;
31851
+ walkExpression(expr, env, summaries, findings, suppressed, file);
31852
+ } else if (kind === "if") {
31853
+ const ifEnv = new Map(env);
31854
+ const bodyStmts = stmt.body?.kind === "block" ? stmt.body.children : [stmt.body];
31855
+ analyzeBlock(bodyStmts, ifEnv, summaries, findings, suppressed, params, file);
31856
+ if (stmt.alternate) {
31857
+ const altEnv = new Map(env);
31858
+ const altStmts = stmt.alternate.kind === "block" ? stmt.alternate.children : [stmt.alternate];
31859
+ analyzeBlock(altStmts, altEnv, summaries, findings, suppressed, params, file);
31860
+ }
31861
+ } else if (kind === "block") {
31862
+ analyzeBlock(stmt.children ?? [], env, summaries, findings, suppressed, params, file);
31863
+ } else if (kind === "return" || kind === "echo" || kind === "print") {
31864
+ walkExpression(stmt, env, summaries, findings, suppressed, file);
31865
+ } else {
31866
+ for (const c of children(stmt)) {
31867
+ if (c.kind === "block" || c.kind === "if") {
31868
+ analyzeBlock([c], new Map(env), summaries, findings, suppressed, params, file);
31869
+ }
31870
+ }
31688
31871
  }
31689
- });
31690
- for (const f of funcs) {
31691
- const id = nodeId("function", f.name);
31692
- ensureNode(g, id, f.name, "function", path, f.node.loc?.start?.line ?? 0);
31693
- }
31694
- for (const m of methods) {
31695
- const id = nodeId("method", m.name, m.cls);
31696
- ensureNode(g, id, m.name, "method", path, m.node.loc?.start?.line ?? 0, m.cls);
31697
- }
31698
- walk(ast, path, (n, ctx) => {
31699
- if (n.kind !== "call")
31700
- return;
31701
- const line = n.loc?.start?.line ?? 0;
31702
- const callerId = ctx.enclosing ?? mainId;
31703
- resolveCall(g, n, callerId, ctx, path, line);
31704
- });
31705
- return g;
31706
- }
31707
- function walk(node, file, visit, ctx = {}) {
31708
- if (!isNode2(node))
31709
- return;
31710
- let childCtx = ctx;
31711
- if (node.kind === "class") {
31712
- childCtx = { ...ctx, className: nameOf(node.name) ?? undefined };
31713
- } else if (node.kind === "function") {
31714
- const name = nameOf(node.name);
31715
- childCtx = { ...ctx, enclosing: name ? nodeId("function", name) : ctx.enclosing };
31716
- } else if (node.kind === "method") {
31717
- const name = nameOf(node.name);
31718
- childCtx = { ...ctx, enclosing: name ? nodeId("method", name, ctx.className) : ctx.enclosing };
31719
31872
  }
31720
- visit(node, ctx);
31721
- for (const c of children2(node))
31722
- walk(c, file, visit, childCtx);
31723
31873
  }
31724
- function resolveCall(g, call, callerId, ctx, file, line) {
31725
- const what = call.what;
31726
- if (!isNode2(what))
31874
+ function walkExpression(node, env, summaries, findings, suppressed, file) {
31875
+ if (!isNode(node))
31727
31876
  return;
31728
- if (what.kind === "name") {
31729
- const fn = nameOf(what);
31730
- if (!fn)
31731
- return;
31732
- const id = nodeId("function", fn);
31733
- if (g.nodes.has(id)) {
31734
- addEdge(g, callerId, id, fn, line, file);
31735
- } else {
31736
- addEdge(g, callerId, `unknown:${fn}`, fn, line, file);
31877
+ if (node.kind === "assign") {
31878
+ const varName = rootVarName(node.left);
31879
+ const taint = evalTaint(node.right, env, summaries);
31880
+ if (varName) {
31881
+ env.set(varName, taint);
31737
31882
  }
31738
31883
  return;
31739
31884
  }
31740
- if (what.kind === "staticlookup") {
31741
- const base = what.what;
31742
- const baseName = nameOf(base) ?? (isNode2(base) && base.kind === "variable" ? base.name : null);
31743
- const method = nameOf(what.offset);
31744
- if (!method)
31885
+ if (node.kind === "call") {
31886
+ const sink = sinkClassOf(node);
31887
+ if (sink) {
31888
+ const argTaints = node.arguments.map((a) => {
31889
+ return evalTaint(a, env, summaries);
31890
+ });
31891
+ const tainted = argTaints.filter((t) => t.tainted);
31892
+ if (tainted.length > 0) {
31893
+ const sanitizers = tainted.flatMap((t) => t.sanitizers);
31894
+ const inlineSan = findSanitizers(JSON.stringify(node));
31895
+ const san = [...sanitizers, ...inlineSan].filter((s, i, arr) => arr.indexOf(s) === i);
31896
+ const cls = sink.cls;
31897
+ if (!isSanitized(san, cls.id)) {
31898
+ findings.push({
31899
+ sink: sink.token,
31900
+ sink_line: node.loc?.start?.line ?? 0,
31901
+ category: cls.category,
31902
+ cwe: cls.cwe,
31903
+ variables: tainted.map((t) => t.source ?? "var"),
31904
+ source: tainted[0].source,
31905
+ source_line: tainted[0].sourceLine,
31906
+ sanitized: false,
31907
+ sanitizers: san.map((s) => s.id),
31908
+ auth_gated: false,
31909
+ interprocedural: false
31910
+ });
31911
+ } else {
31912
+ suppressed.n += 1;
31913
+ }
31914
+ }
31915
+ for (const a of node.arguments)
31916
+ walkExpression(a, env, summaries, findings, suppressed, file);
31745
31917
  return;
31746
- let cls = baseName;
31747
- if (cls === "self" || cls === "static")
31748
- cls = ctx.className ?? "?";
31749
- if (cls && cls !== "?") {
31750
- const id = nodeId("method", method, cls);
31751
- if (g.nodes.has(id))
31752
- addEdge(g, callerId, id, method, line, file);
31753
31918
  }
31754
- return;
31755
- }
31756
- if (what.kind === "propertylookup") {
31757
- const base = what.what;
31758
- const method = nameOf(what.offset);
31759
- if (!method)
31760
- return;
31761
- if (isNode2(base) && base.kind === "variable" && base.name === "this") {
31762
- if (ctx.className) {
31763
- const id = nodeId("method", method, ctx.className);
31764
- if (g.nodes.has(id))
31765
- addEdge(g, callerId, id, method, line, file);
31919
+ const fnName = rootVarName(node.what);
31920
+ if (fnName && summaries.has(fnName)) {
31921
+ const sum = summaries.get(fnName);
31922
+ const argTaints = node.arguments.map((a) => {
31923
+ const v = a;
31924
+ return evalTaint(v, env, summaries);
31925
+ });
31926
+ const taintedArgs = argTaints.filter((t) => t.tainted);
31927
+ if (taintedArgs.length > 0 && sum.sinkParams.size > 0) {
31928
+ const firstSink = [...sum.sinkParams.values()][0];
31929
+ const cls = classifySink2(firstSink.sink);
31930
+ if (cls) {
31931
+ const san = [
31932
+ ...taintedArgs.flatMap((t) => t.sanitizers),
31933
+ ...[...sum.sanitizedParams.values()].flat()
31934
+ ];
31935
+ if (!isSanitized(san, cls.id)) {
31936
+ findings.push({
31937
+ sink: firstSink.sink,
31938
+ sink_line: firstSink.line,
31939
+ category: cls.category,
31940
+ cwe: cls.cwe,
31941
+ variables: taintedArgs.map((t) => t.source ?? "var"),
31942
+ source: taintedArgs[0].source,
31943
+ source_line: taintedArgs[0].sourceLine,
31944
+ sanitized: false,
31945
+ sanitizers: san.map((s) => s.id),
31946
+ auth_gated: false,
31947
+ interprocedural: true
31948
+ });
31949
+ } else {
31950
+ suppressed.n += 1;
31951
+ }
31952
+ }
31766
31953
  }
31767
31954
  return;
31768
31955
  }
31769
- let matched = false;
31770
- for (const [id, n] of g.nodes) {
31771
- if (n.kind === "method" && n.name === method) {
31772
- addEdge(g, callerId, id, method, line, file);
31773
- matched = true;
31956
+ for (const c of children(node))
31957
+ walkExpression(c, env, summaries, findings, suppressed, file);
31958
+ return;
31959
+ }
31960
+ if (node.kind === "echo" || node.kind === "print") {
31961
+ const exprs = node.expressions ?? [node.expression];
31962
+ for (const e of exprs) {
31963
+ const cls = classifySink2(node.kind === "echo" ? "echo " : "print ");
31964
+ const t = evalTaint(e, env, summaries);
31965
+ if (t.tainted && cls) {
31966
+ const san = [...t.sanitizers, ...findSanitizers(JSON.stringify(e))];
31967
+ if (!isSanitized(san, cls.id)) {
31968
+ findings.push({
31969
+ sink: node.kind + " ",
31970
+ sink_line: node.loc?.start?.line ?? 0,
31971
+ category: cls.category,
31972
+ cwe: cls.cwe,
31973
+ variables: [t.source ?? "var"],
31974
+ source: t.source,
31975
+ source_line: t.sourceLine,
31976
+ sanitized: false,
31977
+ sanitizers: san.map((s) => s.id),
31978
+ auth_gated: false,
31979
+ interprocedural: false
31980
+ });
31981
+ } else {
31982
+ suppressed.n += 1;
31983
+ }
31774
31984
  }
31775
31985
  }
31776
- if (!matched)
31777
- addEdge(g, callerId, `unknown:${method}`, method, line, file);
31778
31986
  return;
31779
31987
  }
31988
+ for (const c of children(node))
31989
+ walkExpression(c, env, summaries, findings, suppressed, file);
31780
31990
  }
31781
- function reachableFrom(g, entry) {
31782
- const seen = new Set;
31783
- const stack = [entry];
31784
- while (stack.length) {
31785
- const cur = stack.pop();
31786
- if (seen.has(cur))
31787
- continue;
31788
- seen.add(cur);
31789
- const n = g.nodes.get(cur);
31790
- if (!n)
31791
- continue;
31792
- for (const c of n.callees)
31793
- stack.push(c);
31794
- }
31795
- return seen;
31991
+ function evalTaint(node, env, summaries) {
31992
+ if (!isNode(node))
31993
+ return { tainted: false, sanitizers: [] };
31994
+ const src = sourceOf(node);
31995
+ if (src)
31996
+ return { tainted: true, sanitizers: [], source: src, sourceLine: node.loc?.start?.line };
31997
+ if (node.kind === "variable" || node.kind === "offsetlookup") {
31998
+ const root = rootVarName(node);
31999
+ if (root) {
32000
+ const st = env.get(root);
32001
+ if (st)
32002
+ return { ...st };
32003
+ }
32004
+ if (node.kind === "offsetlookup") {
32005
+ const whatTaint = evalTaint(node.what, env, summaries);
32006
+ if (whatTaint.tainted)
32007
+ return whatTaint;
32008
+ }
32009
+ return { tainted: false, sanitizers: [] };
32010
+ }
32011
+ if (node.kind === "call") {
32012
+ const fnName = rootVarName(node.what);
32013
+ const san = findSanitizers(fnName ? fnName + "(" : "");
32014
+ if (san.length > 0) {
32015
+ const inner = node.arguments.map((a) => evalTaint(a, env, summaries));
32016
+ const tainted = inner.some((t) => t.tainted);
32017
+ return {
32018
+ tainted,
32019
+ sanitizers: tainted ? [...inner.flatMap((t) => t.sanitizers), ...san] : [],
32020
+ source: tainted ? inner.find((t) => t.tainted)?.source : undefined,
32021
+ sourceLine: tainted ? inner.find((t) => t.tainted)?.sourceLine : undefined
32022
+ };
32023
+ }
32024
+ if (fnName && summaries.has(fnName)) {
32025
+ const sum = summaries.get(fnName);
32026
+ const argTaints = node.arguments.map((a) => evalTaint(a, env, summaries));
32027
+ const tainted = argTaints.filter((t) => t.tainted);
32028
+ if (tainted.length > 0) {
32029
+ const helperSan = [...sum.sanitizedParams.values()].flat();
32030
+ return {
32031
+ tainted: true,
32032
+ sanitizers: [...tainted.flatMap((t) => t.sanitizers), ...helperSan],
32033
+ source: tainted[0].source,
32034
+ sourceLine: tainted[0].sourceLine
32035
+ };
32036
+ }
32037
+ }
32038
+ return { tainted: false, sanitizers: [] };
32039
+ }
32040
+ if (node.kind === "bin" || node.kind === "concat" || node.kind === "encapsed") {
32041
+ const parts = children(node);
32042
+ const taints = parts.map((p) => evalTaint(p, env, summaries));
32043
+ const tainted = taints.find((t) => t.tainted);
32044
+ if (tainted)
32045
+ return { ...tainted, sanitizers: taints.flatMap((t) => t.sanitizers) };
32046
+ return { tainted: false, sanitizers: [] };
32047
+ }
32048
+ if (node.kind === "encapsedpart") {
32049
+ return evalTaint(node.expression, env, summaries);
32050
+ }
32051
+ return { tainted: false, sanitizers: [] };
31796
32052
  }
31797
- function entryPoints(g) {
31798
- const entries = [];
31799
- for (const [id, n] of g.nodes) {
31800
- if (n.callers.size === 0)
31801
- entries.push(id);
32053
+ function analyzeTaint(path) {
32054
+ const result = { file: path, findings: [], suppressed: 0 };
32055
+ let code;
32056
+ try {
32057
+ code = readFileSync10(path, "utf8");
32058
+ } catch {
32059
+ return result;
31802
32060
  }
31803
- return entries;
32061
+ let ast;
32062
+ try {
32063
+ ast = parser.parseCode(code, path);
32064
+ } catch {
32065
+ return result;
32066
+ }
32067
+ const functions = findKind(ast, "function");
32068
+ const summaries = new Map;
32069
+ for (const fn of functions) {
32070
+ const name = typeof fn.name === "string" ? fn.name : fn.name?.name ?? "<anonymous>";
32071
+ const params = (fn.arguments ?? []).map((a) => typeof a === "string" ? a : typeof a.name === "string" ? a.name : a.name?.name ?? "?");
32072
+ const summary = { name, params, sinkParams: new Map, sanitizedParams: new Map };
32073
+ const env = new Map;
32074
+ for (const p of params)
32075
+ env.set(p, { tainted: false, sanitizers: [] });
32076
+ const calls = findKind(fn, "call");
32077
+ for (const call of calls) {
32078
+ const sink = sinkClassOf(call);
32079
+ if (!sink)
32080
+ continue;
32081
+ const line = call.loc?.start?.line ?? 0;
32082
+ const varsInCall = findKind(call, "variable").map((v) => v.name).filter(Boolean);
32083
+ for (const v of varsInCall) {
32084
+ if (params.includes(v))
32085
+ summary.sinkParams.set(v, { sink: sink.token, line });
32086
+ }
32087
+ const san = findSanitizers(JSON.stringify(call));
32088
+ for (const v of varsInCall) {
32089
+ if (params.includes(v) && san.length > 0) {
32090
+ summary.sanitizedParams.set(v, [...summary.sanitizedParams.get(v) ?? [], ...san]);
32091
+ }
32092
+ }
32093
+ }
32094
+ for (const ek of ["echo", "print"]) {
32095
+ for (const en of findKind(fn, ek)) {
32096
+ const line = en.loc?.start?.line ?? 0;
32097
+ const vars = findKind(en, "variable").map((v) => v.name).filter(Boolean);
32098
+ const san = findSanitizers(JSON.stringify(en));
32099
+ for (const v of vars) {
32100
+ if (params.includes(v)) {
32101
+ summary.sinkParams.set(v, { sink: ek + " ", line });
32102
+ if (san.length > 0) {
32103
+ summary.sanitizedParams.set(v, [...summary.sanitizedParams.get(v) ?? [], ...san]);
32104
+ }
32105
+ }
32106
+ }
32107
+ }
32108
+ }
32109
+ const assigns = findKind(fn, "assign");
32110
+ for (const a of assigns) {
32111
+ const san = findSanitizers(JSON.stringify(a.right));
32112
+ if (san.length > 0) {
32113
+ const lv = rootVarName(a.left);
32114
+ if (lv && params.includes(lv)) {
32115
+ summary.sanitizedParams.set(lv, [...summary.sanitizedParams.get(lv) ?? [], ...san]);
32116
+ }
32117
+ }
32118
+ }
32119
+ for (const ek of ["echo", "print"]) {
32120
+ for (const en of findKind(fn, ek)) {
32121
+ const san = findSanitizers(JSON.stringify(en));
32122
+ if (san.length > 0) {
32123
+ for (const v of findKind(en, "variable").map((v2) => v2.name).filter(Boolean)) {
32124
+ if (params.includes(v)) {
32125
+ summary.sanitizedParams.set(v, [...summary.sanitizedParams.get(v) ?? [], ...san]);
32126
+ }
32127
+ }
32128
+ }
32129
+ }
32130
+ }
32131
+ summaries.set(name, summary);
32132
+ }
32133
+ const topStmts = [];
32134
+ const body = ast.children ?? [];
32135
+ for (const s of body) {
32136
+ if (isNode(s) && s.kind !== "function")
32137
+ topStmts.push(s);
32138
+ }
32139
+ const globalEnv = new Map;
32140
+ const suppressRef = { n: 0 };
32141
+ analyzeBlock(topStmts, globalEnv, summaries, result.findings, suppressRef, [], path);
32142
+ result.suppressed = suppressRef.n;
32143
+ return result;
32144
+ }
32145
+ function taintTree(root, maxFiles = 2000) {
32146
+ const files = iterSourceFiles(root, maxFiles);
32147
+ return files.map((f) => analyzeTaint(f));
31804
32148
  }
31805
32149
 
31806
32150
  // src/eagle2.ts
31807
- var parser3 = new import_php_parser3.Engine({
32151
+ var import_php_parser3 = __toESM(require_src(), 1);
32152
+ import { readFileSync as readFileSync12 } from "node:fs";
32153
+ init_scanner();
32154
+
32155
+ // src/callgraph.ts
32156
+ var import_php_parser2 = __toESM(require_src(), 1);
32157
+ import { readFileSync as readFileSync11 } from "node:fs";
32158
+ var parser2 = new import_php_parser2.Engine({
31808
32159
  parser: { extractDoc: false, php7: true },
31809
32160
  ast: { withPositions: true, withSource: false }
31810
32161
  });
31811
- function isNode3(v) {
32162
+ function isNode2(v) {
31812
32163
  return !!v && typeof v === "object" && typeof v.kind === "string";
31813
32164
  }
31814
- function children3(node) {
32165
+ function children2(node) {
31815
32166
  const out = [];
31816
32167
  for (const k of Object.keys(node)) {
31817
32168
  if (k === "loc")
@@ -31819,62 +32170,287 @@ function children3(node) {
31819
32170
  const v = node[k];
31820
32171
  if (Array.isArray(v)) {
31821
32172
  for (const c of v)
31822
- if (isNode3(c))
32173
+ if (isNode2(c))
31823
32174
  out.push(c);
31824
- } else if (isNode3(v)) {
32175
+ } else if (isNode2(v)) {
31825
32176
  out.push(v);
31826
32177
  }
31827
32178
  }
31828
32179
  return out;
31829
32180
  }
31830
- function findKind2(node, kind, out = []) {
31831
- if (!isNode3(node))
31832
- return out;
31833
- if (node.kind === kind)
31834
- out.push(node);
31835
- for (const c of children3(node))
31836
- findKind2(c, kind, out);
31837
- return out;
31838
- }
31839
- function nameOf2(node) {
31840
- if (!isNode3(node))
32181
+ function nameOf(node) {
32182
+ if (!isNode2(node))
31841
32183
  return null;
31842
32184
  if (typeof node.name === "string")
31843
32185
  return node.name;
31844
- if (isNode3(node.name))
31845
- return nameOf2(node.name);
32186
+ if (isNode2(node.name))
32187
+ return nameOf(node.name);
31846
32188
  return null;
31847
32189
  }
31848
- function rootVarName2(node) {
31849
- if (!isNode3(node))
31850
- return null;
31851
- if (node.kind === "variable")
31852
- return node.name ?? null;
31853
- if (node.kind === "name")
31854
- return typeof node.name === "string" ? node.name : node.name?.name ?? null;
31855
- if (node.kind === "offsetlookup" || node.kind === "propertylookup")
31856
- return rootVarName2(node.what);
31857
- return null;
32190
+ function nodeId(kind, name, cls) {
32191
+ if (kind === "function")
32192
+ return `func:${name}`;
32193
+ if (kind === "method")
32194
+ return `method:${cls ?? "?"}::${name}`;
32195
+ return "main";
31858
32196
  }
31859
- function sourceOf2(node) {
31860
- if (!isNode3(node))
31861
- return null;
31862
- if (node.kind === "offsetlookup" && node.what?.kind === "variable") {
31863
- const n = node.what.name;
31864
- if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
31865
- return "$" + n;
32197
+ function ensureNode(g, id, name, kind, file, line, cls) {
32198
+ let n = g.nodes.get(id);
32199
+ if (!n) {
32200
+ n = { id, name, kind, class: cls, file, line, callees: new Set, callers: new Set };
32201
+ g.nodes.set(id, n);
31866
32202
  }
31867
- if (node.kind === "variable") {
31868
- const n = node.name;
31869
- if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
31870
- return "$" + n;
32203
+ return n;
32204
+ }
32205
+ function addEdge(g, caller, callee, name, line, file) {
32206
+ g.edges.push({ caller, callee, name, line, file });
32207
+ const c = g.nodes.get(caller);
32208
+ const t = g.nodes.get(callee);
32209
+ if (c)
32210
+ c.callees.add(callee);
32211
+ if (t)
32212
+ t.callers.add(caller);
32213
+ }
32214
+ function buildCallGraph(path) {
32215
+ const g = { nodes: new Map, edges: [] };
32216
+ let code;
32217
+ try {
32218
+ code = readFileSync11(path, "utf8");
32219
+ } catch {
32220
+ return g;
31871
32221
  }
31872
- if (node.kind === "call") {
31873
- const n = rootVarName2(node.what);
31874
- if (n === "getallheaders")
31875
- return "getallheaders";
31876
- if (n === "file_get_contents" && JSON.stringify(node).includes("php://input"))
31877
- return "php://input";
32222
+ let ast;
32223
+ try {
32224
+ ast = parser2.parseCode(code, path);
32225
+ } catch {
32226
+ return g;
32227
+ }
32228
+ const mainId = nodeId("main", "main");
32229
+ ensureNode(g, mainId, "<main>", "main", path, 0);
32230
+ const funcs = [];
32231
+ const methods = [];
32232
+ walk(ast, path, (n, ctx) => {
32233
+ if (n.kind === "function") {
32234
+ const name = nameOf(n.name);
32235
+ if (name)
32236
+ funcs.push({ name, node: n });
32237
+ } else if (n.kind === "method" && ctx.className) {
32238
+ const mname = nameOf(n.name);
32239
+ if (mname)
32240
+ methods.push({ cls: ctx.className, name: mname, node: n });
32241
+ }
32242
+ });
32243
+ for (const f of funcs) {
32244
+ const id = nodeId("function", f.name);
32245
+ ensureNode(g, id, f.name, "function", path, f.node.loc?.start?.line ?? 0);
32246
+ }
32247
+ for (const m of methods) {
32248
+ const id = nodeId("method", m.name, m.cls);
32249
+ ensureNode(g, id, m.name, "method", path, m.node.loc?.start?.line ?? 0, m.cls);
32250
+ }
32251
+ walk(ast, path, (n, ctx) => {
32252
+ if (n.kind !== "call")
32253
+ return;
32254
+ const line = n.loc?.start?.line ?? 0;
32255
+ const callerId = ctx.enclosing ?? mainId;
32256
+ resolveCall(g, n, callerId, ctx, path, line);
32257
+ });
32258
+ return g;
32259
+ }
32260
+ function walk(node, file, visit, ctx = {}) {
32261
+ if (!isNode2(node))
32262
+ return;
32263
+ let childCtx = ctx;
32264
+ if (node.kind === "class") {
32265
+ childCtx = { ...ctx, className: nameOf(node.name) ?? undefined };
32266
+ } else if (node.kind === "function") {
32267
+ const name = nameOf(node.name);
32268
+ childCtx = { ...ctx, enclosing: name ? nodeId("function", name) : ctx.enclosing };
32269
+ } else if (node.kind === "method") {
32270
+ const name = nameOf(node.name);
32271
+ childCtx = { ...ctx, enclosing: name ? nodeId("method", name, ctx.className) : ctx.enclosing };
32272
+ }
32273
+ visit(node, ctx);
32274
+ for (const c of children2(node))
32275
+ walk(c, file, visit, childCtx);
32276
+ }
32277
+ function resolveCall(g, call, callerId, ctx, file, line) {
32278
+ const what = call.what;
32279
+ if (!isNode2(what))
32280
+ return;
32281
+ if (what.kind === "name") {
32282
+ const fn = nameOf(what);
32283
+ if (!fn)
32284
+ return;
32285
+ const id = nodeId("function", fn);
32286
+ if (g.nodes.has(id)) {
32287
+ addEdge(g, callerId, id, fn, line, file);
32288
+ } else {
32289
+ addEdge(g, callerId, `unknown:${fn}`, fn, line, file);
32290
+ }
32291
+ return;
32292
+ }
32293
+ if (what.kind === "staticlookup") {
32294
+ const base = what.what;
32295
+ const baseName = nameOf(base) ?? (isNode2(base) && base.kind === "variable" ? base.name : null);
32296
+ const method = nameOf(what.offset);
32297
+ if (!method)
32298
+ return;
32299
+ let cls = baseName;
32300
+ if (cls === "self" || cls === "static")
32301
+ cls = ctx.className ?? "?";
32302
+ if (cls && cls !== "?") {
32303
+ const id = nodeId("method", method, cls);
32304
+ if (g.nodes.has(id))
32305
+ addEdge(g, callerId, id, method, line, file);
32306
+ }
32307
+ return;
32308
+ }
32309
+ if (what.kind === "propertylookup") {
32310
+ const base = what.what;
32311
+ const method = nameOf(what.offset);
32312
+ if (!method)
32313
+ return;
32314
+ if (isNode2(base) && base.kind === "variable" && base.name === "this") {
32315
+ if (ctx.className) {
32316
+ const id = nodeId("method", method, ctx.className);
32317
+ if (g.nodes.has(id))
32318
+ addEdge(g, callerId, id, method, line, file);
32319
+ }
32320
+ return;
32321
+ }
32322
+ let matched = false;
32323
+ for (const [id, n] of g.nodes) {
32324
+ if (n.kind === "method" && n.name === method) {
32325
+ addEdge(g, callerId, id, method, line, file);
32326
+ matched = true;
32327
+ }
32328
+ }
32329
+ if (!matched)
32330
+ addEdge(g, callerId, `unknown:${method}`, method, line, file);
32331
+ return;
32332
+ }
32333
+ }
32334
+ function reachableFrom(g, entry) {
32335
+ const seen = new Set;
32336
+ const stack = [entry];
32337
+ while (stack.length) {
32338
+ const cur = stack.pop();
32339
+ if (seen.has(cur))
32340
+ continue;
32341
+ seen.add(cur);
32342
+ const n = g.nodes.get(cur);
32343
+ if (!n)
32344
+ continue;
32345
+ for (const c of n.callees)
32346
+ stack.push(c);
32347
+ }
32348
+ return seen;
32349
+ }
32350
+ function entryPoints(g) {
32351
+ const entries = [];
32352
+ for (const [id, n] of g.nodes) {
32353
+ if (n.callers.size === 0)
32354
+ entries.push(id);
32355
+ }
32356
+ return entries;
32357
+ }
32358
+
32359
+ // src/cache.ts
32360
+ import { createHash as createHash3 } from "node:crypto";
32361
+ var mem = new Map;
32362
+ var hits = 0;
32363
+ var misses = 0;
32364
+ function cacheKey(kind, source) {
32365
+ const h = createHash3("sha256").update(source).digest("hex").slice(0, 20);
32366
+ return `${kind}:${VERSION}:${h}`;
32367
+ }
32368
+ function getCache(kind, source) {
32369
+ const k = cacheKey(kind, source);
32370
+ if (mem.has(k)) {
32371
+ hits += 1;
32372
+ return mem.get(k);
32373
+ }
32374
+ misses += 1;
32375
+ return;
32376
+ }
32377
+ function setCache(kind, source, value) {
32378
+ mem.set(cacheKey(kind, source), value);
32379
+ return value;
32380
+ }
32381
+
32382
+ // src/eagle2.ts
32383
+ var parser3 = new import_php_parser3.Engine({
32384
+ parser: { extractDoc: false, php7: true },
32385
+ ast: { withPositions: true, withSource: false }
32386
+ });
32387
+ function isNode3(v) {
32388
+ return !!v && typeof v === "object" && typeof v.kind === "string";
32389
+ }
32390
+ function children3(node) {
32391
+ const out = [];
32392
+ for (const k of Object.keys(node)) {
32393
+ if (k === "loc")
32394
+ continue;
32395
+ const v = node[k];
32396
+ if (Array.isArray(v)) {
32397
+ for (const c of v)
32398
+ if (isNode3(c))
32399
+ out.push(c);
32400
+ } else if (isNode3(v)) {
32401
+ out.push(v);
32402
+ }
32403
+ }
32404
+ return out;
32405
+ }
32406
+ function findKind2(node, kind, out = []) {
32407
+ if (!isNode3(node))
32408
+ return out;
32409
+ if (node.kind === kind)
32410
+ out.push(node);
32411
+ for (const c of children3(node))
32412
+ findKind2(c, kind, out);
32413
+ return out;
32414
+ }
32415
+ function nameOf2(node) {
32416
+ if (!isNode3(node))
32417
+ return null;
32418
+ if (typeof node.name === "string")
32419
+ return node.name;
32420
+ if (isNode3(node.name))
32421
+ return nameOf2(node.name);
32422
+ return null;
32423
+ }
32424
+ function rootVarName2(node) {
32425
+ if (!isNode3(node))
32426
+ return null;
32427
+ if (node.kind === "variable")
32428
+ return node.name ?? null;
32429
+ if (node.kind === "name")
32430
+ return typeof node.name === "string" ? node.name : node.name?.name ?? null;
32431
+ if (node.kind === "offsetlookup" || node.kind === "propertylookup")
32432
+ return rootVarName2(node.what);
32433
+ return null;
32434
+ }
32435
+ function sourceOf2(node) {
32436
+ if (!isNode3(node))
32437
+ return null;
32438
+ if (node.kind === "offsetlookup" && node.what?.kind === "variable") {
32439
+ const n = node.what.name;
32440
+ if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
32441
+ return "$" + n;
32442
+ }
32443
+ if (node.kind === "variable") {
32444
+ const n = node.name;
32445
+ if (["_GET", "_POST", "_REQUEST", "_COOKIE", "_FILES", "_SERVER"].includes(n))
32446
+ return "$" + n;
32447
+ }
32448
+ if (node.kind === "call") {
32449
+ const n = rootVarName2(node.what);
32450
+ if (n === "getallheaders")
32451
+ return "getallheaders";
32452
+ if (n === "file_get_contents" && JSON.stringify(node).includes("php://input"))
32453
+ return "php://input";
31878
32454
  }
31879
32455
  return null;
31880
32456
  }
@@ -32230,6 +32806,15 @@ function resolveFuncId(name, summaries) {
32230
32806
  return null;
32231
32807
  }
32232
32808
  function analyzeDataFlow2(path) {
32809
+ let cacheCode = "";
32810
+ try {
32811
+ cacheCode = readFileSync12(path, "utf8");
32812
+ } catch {}
32813
+ if (cacheCode) {
32814
+ const cached = getCache("eagle2", cacheCode);
32815
+ if (cached)
32816
+ return cached;
32817
+ }
32233
32818
  const cg = buildCallGraph(path);
32234
32819
  const entries = entryPoints(cg);
32235
32820
  const reachable = new Set;
@@ -32245,7 +32830,7 @@ function analyzeDataFlow2(path) {
32245
32830
  };
32246
32831
  let code;
32247
32832
  try {
32248
- code = readFileSync12(path, "utf8");
32833
+ code = cacheCode || readFileSync12(path, "utf8");
32249
32834
  } catch {
32250
32835
  return result;
32251
32836
  }
@@ -32354,6 +32939,8 @@ function analyzeDataFlow2(path) {
32354
32939
  queue.push({ funcId: calleeId, param: calleeParam, source: item.source, sourceLine: item.sourceLine, viaReturn: false });
32355
32940
  }
32356
32941
  }
32942
+ if (cacheCode)
32943
+ setCache("eagle2", cacheCode, result);
32357
32944
  return result;
32358
32945
  }
32359
32946
 
@@ -51543,469 +52130,131 @@ function extractVersion(headers, body) {
51543
52130
  out.push({ product: "joomla", version: joomlaVer[1], evidence: `body: Joomla ${joomlaVer[1]}` });
51544
52131
  const drupalVer = /Drupal\s+([0-9][0-9.]*)/i.exec(body);
51545
52132
  if (drupalVer)
51546
- out.push({ product: "drupal", version: drupalVer[1], evidence: `body: Drupal ${drupalVer[1]}` });
51547
- const powered = headers["x-powered-by"] ?? "";
51548
- if (powered && !out.some((o) => powered.includes(o.product))) {
51549
- out.push({ product: powered.split("/")[0].toLowerCase(), version: powered.split("/")[1] ?? "unknown", evidence: powered });
51550
- }
51551
- return out;
51552
- }
51553
- function correlateIntel(techs, versionList, portList) {
51554
- const tech = techs.map((t) => ({ tech: t, ...techCorrelation(t) }));
51555
- const versions = versionList.map((v) => ({
51556
- product: v.product,
51557
- version: v.version,
51558
- correlation: techCorrelation(v.product),
51559
- payload_lookup: payloadLookup(v.product),
51560
- template_lookup: templateLookup(v.product, 5)
51561
- }));
51562
- const ports = portList.map((p) => ({ port: p.port, service: p.service, ...portCorrelation(p.port) }));
51563
- return { tech, versions, ports };
51564
- }
51565
- async function liveRecon(target, includeActive = false) {
51566
- const base = normalizeTarget2(target);
51567
- const host = (() => {
51568
- try {
51569
- return new URL(base).hostname;
51570
- } catch {
51571
- return target;
51572
- }
51573
- })();
51574
- const root = await httpGet(base);
51575
- const interestingHeaders = {};
51576
- for (const k of ["server", "x-powered-by", "x-aspnet-version", "x-drupal-cache", "x-generator", "via", "x-cache", "cf-ray"]) {
51577
- if (root.headers[k])
51578
- interestingHeaders[k] = root.headers[k];
51579
- }
51580
- const fingerprint = {
51581
- status: root.status,
51582
- title: (root.body.match(/<title[^>]*>([^<]+)<\/title>/i) ?? [])[1]?.trim().slice(0, 200) ?? "",
51583
- server: root.headers["server"] ?? root.headers["x-powered-by"] ?? "",
51584
- headers: interestingHeaders,
51585
- body_size: root.body.length
51586
- };
51587
- const waf = detectWaf(root.headers, root.body);
51588
- const techSig = [
51589
- [/wp-content|wp-includes|wp-json/i, "wordpress"],
51590
- [/wp-login\.php|wp-admin/i, "wordpress"],
51591
- [/powered by joomla|com_content/i, "joomla"],
51592
- [/laravel|_token|csrf-token/i, "laravel"],
51593
- [/react|__NEXT_DATA__|next\/static/i, "nextjs"],
51594
- [/angular|ng-version/i, "angular"],
51595
- [/vue|__vue__|v-data/i, "vue"],
51596
- [/django|csrftoken|__debug__/i, "django"],
51597
- [/ruby on rails|rails/i, "rails"],
51598
- [/asp\.net|__VIEWSTATE|__EVENTVALIDATION/i, "aspnet"],
51599
- [/phpBB|phpbb/i, "phpbb"],
51600
- [/mybb|mybb/i, "mybb"],
51601
- [/drupal|Drupal\.settings/i, "drupal"],
51602
- [/magento|Mage\./i, "magento"],
51603
- [/shopify|cdn\.shopify/i, "shopify"],
51604
- [/grafana|kibana|elastic/i, "grafana-kibana"],
51605
- [/node\.js|express/i, "nodejs"],
51606
- [/flask|werkzeug/i, "flask"],
51607
- [/spring|actuator/i, "spring"]
51608
- ];
51609
- const techs = [...new Set(techSig.filter(([re]) => re.test(root.body)).map(([, t]) => t))];
51610
- const versions = extractVersion(root.headers, root.body);
51611
- const { links, scripts } = crawlLinks(base, root.body);
51612
- const { robots, sitemap } = await fetchRobotsAndSitemap(base);
51613
- const allUrls = [...links, ...scripts, ...robots, ...sitemap];
51614
- const crawl = { links, scripts, endpoints: [...new Set(allUrls)], robots, sitemap, count: allUrls.length };
51615
- const params = extractParams(allUrls);
51616
- const { subdomains } = await subdomainEnum(host);
51617
- const { found: dnsFindings } = await dnsBrute(host);
51618
- const { urls: wayback } = await waybackUrls(host);
51619
- const apiEndpoints = (await discoverApi(base)).map(({ path, status }) => ({ path, status }));
51620
- let openPorts = [];
51621
- if (includeActive && root.status > 0) {
51622
- const scan = await scanPorts(host);
51623
- openPorts = scan.map(({ port, service }) => ({ port, service }));
51624
- }
51625
- const intel = correlateIntel(techs, versions, openPorts);
51626
- const nextSteps = [];
51627
- if (waf.detected)
51628
- nextSteps.push(`WAF detected (${waf.wafs?.map((w) => w.waf).join(", ")}) — consult detect_waf + WAF bypass playbook before exploitation.`);
51629
- for (const t of techs) {
51630
- const c = techCorrelation(t);
51631
- if (c.found)
51632
- nextSteps.push(`Tech '${t}' maps to known vuln classes — read tech_correlation('${t}') and fetch nuclei templates with template_lookup('${t}').`);
51633
- }
51634
- for (const v of versions) {
51635
- nextSteps.push(`Version ${v.product}/${v.version} detected — check cve_correlation + nvd_lookup for known CVEs affecting ${v.product} ${v.version}.`);
51636
- }
51637
- if (crawl.endpoints.length > 0)
51638
- nextSteps.push(`Found ${crawl.endpoints.length} endpoints — feed them to taint_file/blitz_scan for source analysis, or strike_verify for live validation.`);
51639
- if (params.length > 0)
51640
- nextSteps.push(`Discovered input params: ${params.join(", ")} — these are attack-surface entry points; fuzz them with payload_lookup after scope confirms.`);
51641
- if (subdomains.length > 0)
51642
- nextSteps.push(`Found ${subdomains.length} subdomains — each is a separate scope surface; enumerate further with fofa_search.`);
51643
- if (dnsFindings.length > 0)
51644
- 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.`);
51645
- if (wayback.length > 0)
51646
- 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.`);
51647
- if (openPorts.length > 0)
51648
- 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.`);
51649
- if (apiEndpoints.length > 0)
51650
- nextSteps.push(`API endpoints exposed (${apiEndpoints.map((e) => e.path).join(", ")}) — audit them with api-security playbook; check for BOLA/IDOR/auth gaps.`);
51651
- if (nextSteps.length === 0)
51652
- nextSteps.push("Nothing exposed on the surface — try subdomain enumeration, port scan (active), or request source access for EAGLE-EYE analysis.");
51653
- nextSteps.push("Every confirmed finding: record with finding_create, attach confidence_score, redact secrets. A hit is a hypothesis until verified.");
51654
- return {
51655
- target: base,
51656
- host,
51657
- fingerprint,
51658
- waf,
51659
- tech: { detected: techs, server: fingerprint.server },
51660
- versions,
51661
- crawl,
51662
- params,
51663
- subdomains,
51664
- dns_findings: dnsFindings,
51665
- wayback_urls: wayback,
51666
- api_endpoints: apiEndpoints,
51667
- open_ports: openPorts,
51668
- intel,
51669
- next_steps: nextSteps
51670
- };
51671
- }
51672
-
51673
- // src/strike.ts
51674
- var DEFAULT_TIMEOUT_MS = 15000;
51675
- async function httpRequest(url, method, data, headers, timeoutMs = DEFAULT_TIMEOUT_MS) {
51676
- const controller = new AbortController;
51677
- const t = setTimeout(() => controller.abort(), timeoutMs);
51678
- try {
51679
- const res = await fetch(url, {
51680
- method,
51681
- body: method === "POST" ? data : undefined,
51682
- headers: { "User-Agent": "blitzstrike/1.0", ...headers ?? {} },
51683
- signal: controller.signal,
51684
- redirect: "follow"
51685
- });
51686
- const body = await res.text();
51687
- const hdrs = {};
51688
- res.headers.forEach((v, k) => {
51689
- hdrs[k] = v;
51690
- });
51691
- return { status: res.status, body: body.slice(0, 20000), headers: hdrs };
51692
- } catch (e) {
51693
- return { status: 0, body: String(e), headers: {} };
51694
- } finally {
51695
- clearTimeout(t);
51696
- }
51697
- }
51698
- function defaultMarker() {
51699
- return `BS${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
51700
- }
51701
- function inject(target, value, param) {
51702
- const placeholder = "{{MARKER}}";
51703
- if (target.includes(placeholder))
51704
- return target.split(placeholder).join(value);
51705
- if (target.includes("?"))
51706
- return `${target}&${param}=${encodeURIComponent(value)}`;
51707
- return `${target}?${param}=${encodeURIComponent(value)}`;
51708
- }
51709
- async function strikeVerify(input) {
51710
- const method = input.method ?? "GET";
51711
- const param = input.param ?? "q";
51712
- const marker = input.marker ?? defaultMarker();
51713
- const control = input.control ?? defaultMarker();
51714
- const baseUrl = input.url;
51715
- const timeoutMs = input.timeout ?? DEFAULT_TIMEOUT_MS;
51716
- const baselineTarget = inject(baseUrl, "", param).replace(/[?&]q=$/, "").replace(/[?&]$/, "");
51717
- const markerTarget = inject(baseUrl, marker, param);
51718
- const controlTarget = inject(baseUrl, control, param);
51719
- const baseline = await httpRequest(baselineTarget, method, input.data, input.headers, timeoutMs);
51720
- const markerResp = await httpRequest(markerTarget, method, input.data, input.headers, timeoutMs);
51721
- const controlResp = await httpRequest(controlTarget, method, input.data, input.headers, timeoutMs);
51722
- if (baseline.status === 0 || markerResp.status === 0) {
51723
- return {
51724
- status: "blocked",
51725
- marker_reflected: false,
51726
- control_reflected: false,
51727
- baseline: { status: baseline.status, body_preview: redactSecrets(baseline.body.slice(0, 400)), body_hash: sha256(baseline.body) },
51728
- marker_response: { status: markerResp.status, body_preview: redactSecrets(markerResp.body.slice(0, 400)), body_hash: sha256(markerResp.body) },
51729
- control_response: { status: controlResp.status, body_preview: redactSecrets(controlResp.body.slice(0, 400)), body_hash: sha256(controlResp.body) },
51730
- reason: "request failed or target unreachable — cannot validate",
51731
- evidence: []
51732
- };
51733
- }
51734
- const markerReflected = markerResp.body.includes(marker);
51735
- const controlReflected = controlResp.body.includes(control);
51736
- let status;
51737
- let reason;
51738
- if (markerReflected && !controlReflected) {
51739
- status = "confirmed";
51740
- reason = "marker reflected; negative control did not — the sink reflects attacker-controlled input (real finding)";
51741
- } else if (markerReflected && controlReflected) {
51742
- status = "false_positive";
51743
- reason = "marker AND negative control both reflected — behaviour is indistinguishable from benign reflection (likely false positive)";
51744
- } else if (!markerReflected && !controlReflected) {
51745
- status = "unconfirmed";
51746
- reason = "marker not reflected — reachability/exploitability could not be demonstrated live";
51747
- } else {
51748
- status = "unconfirmed";
51749
- reason = "negative control reflected but marker did not — inconsistent, requires deeper analysis";
51750
- }
51751
- const evidence = [
51752
- {
51753
- type: "baseline_comparison",
51754
- description: `Baseline response (status ${baseline.status}) for ${redactSecrets(baselineTarget)}`,
51755
- artifacts: [{ name: "baseline", kind: "http_response", content: `${baseline.status}
51756
- ${baseline.body}` }]
51757
- },
51758
- {
51759
- type: "validation_result",
51760
- description: `Marker '${redactSecrets(marker)}' reflected=${markerReflected}; control reflected=${controlReflected}`,
51761
- artifacts: [{ name: "marker_response", kind: "http_response", content: `${markerResp.status}
51762
- ${markerResp.body}` }]
51763
- },
51764
- {
51765
- type: "negative_control",
51766
- description: `Negative control '${redactSecrets(control)}' reflected=${controlReflected}`,
51767
- artifacts: [{ name: "control_response", kind: "http_response", content: `${controlResp.status}
51768
- ${controlResp.body}` }]
51769
- }
51770
- ];
51771
- return {
51772
- status,
51773
- marker_reflected: markerReflected,
51774
- control_reflected: controlReflected,
51775
- baseline: { status: baseline.status, body_preview: redactSecrets(baseline.body.slice(0, 400)), body_hash: sha256(baseline.body) },
51776
- marker_response: { status: markerResp.status, body_preview: redactSecrets(markerResp.body.slice(0, 400)), body_hash: sha256(markerResp.body) },
51777
- control_response: { status: controlResp.status, body_preview: redactSecrets(controlResp.body.slice(0, 400)), body_hash: sha256(controlResp.body) },
51778
- reason,
51779
- evidence
51780
- };
51781
- }
51782
- function resolveFinding(finding, verdict) {
51783
- let f = finding;
51784
- if (f.status === "hypothesis") {
51785
- f = transition(f, "validating");
51786
- }
51787
- switch (verdict.status) {
51788
- case "confirmed":
51789
- return confirmFinding(f, {
51790
- evidence: verdict.evidence,
51791
- negative_control: true,
51792
- baseline: true
51793
- });
51794
- case "false_positive":
51795
- return rejectFinding(f, "false_positive", verdict.reason, verdict.evidence);
51796
- case "blocked":
51797
- return rejectFinding(f, "blocked", verdict.reason, verdict.evidence);
51798
- case "unconfirmed":
51799
- case "likely":
51800
- default: {
51801
- return {
51802
- ...f,
51803
- validation: { performed: true, status: verdict.status === "likely" ? "likely" : "unconfirmed", negative_control: verdict.control_reflected === false, baseline: true },
51804
- remediation: { ...f.remediation, note: verdict.reason },
51805
- timestamps: { ...f.timestamps, updated: new Date().toISOString() }
51806
- };
51807
- }
51808
- }
51809
- }
51810
-
51811
- // src/cvss.ts
51812
- var AV_WEIGHT = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
51813
- var AC_WEIGHT = { L: 0.77, H: 0.44 };
51814
- var PR_WEIGHT = {
51815
- U: { N: 0.85, L: 0.62, H: 0.27 },
51816
- C: { N: 0.85, L: 0.68, H: 0.5 }
51817
- };
51818
- var UI_WEIGHT = { N: 0.85, R: 0.62 };
51819
- var CIA_WEIGHT = { H: 0.56, L: 0.22, N: 0 };
51820
- function roundup(value) {
51821
- return Math.ceil(value * 10) / 10;
51822
- }
51823
- function cvssBaseScore(input) {
51824
- const AV = input.AV ?? "N";
51825
- const AC = input.AC ?? "L";
51826
- const PR = input.PR ?? "N";
51827
- const UI = input.UI ?? "N";
51828
- const S = input.S ?? "U";
51829
- const C = input.C ?? "N";
51830
- const I = input.I ?? "N";
51831
- const A = input.A ?? "N";
51832
- const iscBase = 1 - (1 - CIA_WEIGHT[C]) * (1 - CIA_WEIGHT[I]) * (1 - CIA_WEIGHT[A]);
51833
- let impact;
51834
- if (S === "U") {
51835
- impact = 6.42 * iscBase;
51836
- } else {
51837
- impact = 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15);
51838
- }
51839
- const exploitability = 8.22 * AV_WEIGHT[AV] * AC_WEIGHT[AC] * PR_WEIGHT[S][PR] * UI_WEIGHT[UI];
51840
- if (impact <= 0)
51841
- return 0;
51842
- if (S === "U") {
51843
- return roundup(Math.min(impact + exploitability, 10));
51844
- }
51845
- return roundup(Math.min(1.08 * (impact + exploitability), 10));
51846
- }
51847
- function cvssVector(input) {
51848
- const order = [
51849
- ["AV", input.AV ?? "N"],
51850
- ["AC", input.AC ?? "L"],
51851
- ["PR", input.PR ?? "N"],
51852
- ["UI", input.UI ?? "N"],
51853
- ["S", input.S ?? "U"],
51854
- ["C", input.C ?? "N"],
51855
- ["I", input.I ?? "N"],
51856
- ["A", input.A ?? "N"]
51857
- ];
51858
- return "CVSS:3.1/" + order.map(([k, v]) => `${k}:${v}`).join("/");
51859
- }
51860
- function cvssSeverity(score) {
51861
- if (score === 0)
51862
- return "none";
51863
- if (score < 4)
51864
- return "low";
51865
- if (score < 7)
51866
- return "medium";
51867
- if (score < 9)
51868
- return "high";
51869
- return "critical";
51870
- }
51871
- function cvssAssess(input) {
51872
- const score = cvssBaseScore(input);
51873
- return { score, vector: cvssVector(input), severity: cvssSeverity(score) };
51874
- }
51875
-
51876
- // src/dedup.ts
51877
- function rootCauseSignature(finding) {
51878
- const sinkType = (finding.sink?.type ?? "unknown").toLowerCase();
51879
- const sourceType = (finding.source?.type ?? "unknown").toLowerCase();
51880
- const cwe = (finding.classification?.cwe ?? "").toUpperCase();
51881
- return [sinkType, sourceType, cwe].filter(Boolean).join("|");
51882
- }
51883
- function dedupFindings(findings) {
51884
- const groups = new Map;
51885
- for (const f of findings) {
51886
- const sig = rootCauseSignature(f);
51887
- const existing = groups.get(sig);
51888
- if (existing) {
51889
- existing.count += 1;
51890
- existing.findings.push(f);
51891
- } else {
51892
- groups.set(sig, { signature: sig, count: 1, representative: f, findings: [f] });
51893
- }
52133
+ out.push({ product: "drupal", version: drupalVer[1], evidence: `body: Drupal ${drupalVer[1]}` });
52134
+ const powered = headers["x-powered-by"] ?? "";
52135
+ if (powered && !out.some((o) => powered.includes(o.product))) {
52136
+ out.push({ product: powered.split("/")[0].toLowerCase(), version: powered.split("/")[1] ?? "unknown", evidence: powered });
51894
52137
  }
51895
- return [...groups.values()];
52138
+ return out;
51896
52139
  }
51897
- function uniqueFindings(findings) {
51898
- return dedupFindings(findings).map((g) => g.representative);
52140
+ function correlateIntel(techs, versionList, portList) {
52141
+ const tech = techs.map((t) => ({ tech: t, ...techCorrelation(t) }));
52142
+ const versions = versionList.map((v) => ({
52143
+ product: v.product,
52144
+ version: v.version,
52145
+ correlation: techCorrelation(v.product),
52146
+ payload_lookup: payloadLookup(v.product),
52147
+ template_lookup: templateLookup(v.product, 5)
52148
+ }));
52149
+ const ports = portList.map((p) => ({ port: p.port, service: p.service, ...portCorrelation(p.port) }));
52150
+ return { tech, versions, ports };
51899
52151
  }
51900
-
51901
- // src/report.ts
51902
- function summarize(findings) {
51903
- const byStatus = {};
51904
- const bySeverity = {};
51905
- let evidenceArtifacts = 0;
51906
- for (const f of findings) {
51907
- byStatus[f.status] = (byStatus[f.status] ?? 0) + 1;
51908
- const sev = f.classification?.severity ?? "informational";
51909
- bySeverity[sev] = (bySeverity[sev] ?? 0) + 1;
51910
- evidenceArtifacts += f.evidence?.reduce((n, e) => n + (e.artifacts?.length ?? 0), 0) ?? 0;
52152
+ async function liveRecon(target, includeActive = false) {
52153
+ const base = normalizeTarget2(target);
52154
+ const host = (() => {
52155
+ try {
52156
+ return new URL(base).hostname;
52157
+ } catch {
52158
+ return target;
52159
+ }
52160
+ })();
52161
+ const root = await httpGet(base);
52162
+ const interestingHeaders = {};
52163
+ for (const k of ["server", "x-powered-by", "x-aspnet-version", "x-drupal-cache", "x-generator", "via", "x-cache", "cf-ray"]) {
52164
+ if (root.headers[k])
52165
+ interestingHeaders[k] = root.headers[k];
51911
52166
  }
51912
- const groups = dedupFindings(findings);
51913
- return {
51914
- total_findings: findings.length,
51915
- unique_root_causes: groups.length,
51916
- by_status: byStatus,
51917
- by_severity: bySeverity,
51918
- confirmed: byStatus["confirmed"] ?? 0,
51919
- false_positives: byStatus["false_positive"] ?? 0,
51920
- evidence_artifacts: evidenceArtifacts
52167
+ const fingerprint = {
52168
+ status: root.status,
52169
+ title: (root.body.match(/<title[^>]*>([^<]+)<\/title>/i) ?? [])[1]?.trim().slice(0, 200) ?? "",
52170
+ server: root.headers["server"] ?? root.headers["x-powered-by"] ?? "",
52171
+ headers: interestingHeaders,
52172
+ body_size: root.body.length
51921
52173
  };
51922
- }
51923
- function severityLabel(sev) {
51924
- const s = sev.toUpperCase();
51925
- if (s === "CRITICAL")
51926
- return "\uD83D\uDD34 CRITICAL";
51927
- if (s === "HIGH")
51928
- return "\uD83D\uDFE0 HIGH";
51929
- if (s === "MEDIUM")
51930
- return "\uD83D\uDFE1 MEDIUM";
51931
- if (s === "LOW")
51932
- return "\uD83D\uDD35 LOW";
51933
- return "⚪ INFORMATIONAL";
51934
- }
51935
- function reportMarkdown(findings, opts = {}) {
51936
- const summary = summarize(findings);
51937
- const lines = [];
51938
- lines.push(`# ${opts.title ?? "Blitz Strike — Security Assessment Report"}`);
51939
- lines.push("");
51940
- lines.push(`- Scope: ${opts.scope ?? "(unset)"}`);
51941
- lines.push(`- Blitz Strike version: ${opts.version ?? "unknown"}`);
51942
- if (opts.include_timestamp)
51943
- lines.push(`- Generated: ${new Date().toISOString()}`);
51944
- lines.push("");
51945
- lines.push("## Summary");
51946
- lines.push("");
51947
- lines.push(`| Metric | Value |`);
51948
- lines.push(`|--------|-------|`);
51949
- lines.push(`| Total findings | ${summary.total_findings} |`);
51950
- lines.push(`| Unique root causes | ${summary.unique_root_causes} |`);
51951
- lines.push(`| Confirmed | ${summary.confirmed} |`);
51952
- lines.push(`| False positives | ${summary.false_positives} |`);
51953
- lines.push(`| Evidence artifacts | ${summary.evidence_artifacts} |`);
51954
- lines.push("");
51955
- lines.push("### By status");
51956
- lines.push("");
51957
- for (const [status, n] of Object.entries(summary.by_status).sort()) {
51958
- lines.push(`- ${status}: ${n}`);
51959
- }
51960
- lines.push("");
51961
- lines.push("### By severity");
51962
- lines.push("");
51963
- for (const [sev, n] of Object.entries(summary.by_severity).sort()) {
51964
- lines.push(`- ${sev}: ${n}`);
52174
+ const waf = detectWaf(root.headers, root.body);
52175
+ const techSig = [
52176
+ [/wp-content|wp-includes|wp-json/i, "wordpress"],
52177
+ [/wp-login\.php|wp-admin/i, "wordpress"],
52178
+ [/powered by joomla|com_content/i, "joomla"],
52179
+ [/laravel|_token|csrf-token/i, "laravel"],
52180
+ [/react|__NEXT_DATA__|next\/static/i, "nextjs"],
52181
+ [/angular|ng-version/i, "angular"],
52182
+ [/vue|__vue__|v-data/i, "vue"],
52183
+ [/django|csrftoken|__debug__/i, "django"],
52184
+ [/ruby on rails|rails/i, "rails"],
52185
+ [/asp\.net|__VIEWSTATE|__EVENTVALIDATION/i, "aspnet"],
52186
+ [/phpBB|phpbb/i, "phpbb"],
52187
+ [/mybb|mybb/i, "mybb"],
52188
+ [/drupal|Drupal\.settings/i, "drupal"],
52189
+ [/magento|Mage\./i, "magento"],
52190
+ [/shopify|cdn\.shopify/i, "shopify"],
52191
+ [/grafana|kibana|elastic/i, "grafana-kibana"],
52192
+ [/node\.js|express/i, "nodejs"],
52193
+ [/flask|werkzeug/i, "flask"],
52194
+ [/spring|actuator/i, "spring"]
52195
+ ];
52196
+ const techs = [...new Set(techSig.filter(([re]) => re.test(root.body)).map(([, t]) => t))];
52197
+ const versions = extractVersion(root.headers, root.body);
52198
+ const { links, scripts } = crawlLinks(base, root.body);
52199
+ const { robots, sitemap } = await fetchRobotsAndSitemap(base);
52200
+ const allUrls = [...links, ...scripts, ...robots, ...sitemap];
52201
+ const crawl = { links, scripts, endpoints: [...new Set(allUrls)], robots, sitemap, count: allUrls.length };
52202
+ const params = extractParams(allUrls);
52203
+ const { subdomains } = await subdomainEnum(host);
52204
+ const { found: dnsFindings } = await dnsBrute(host);
52205
+ const { urls: wayback } = await waybackUrls(host);
52206
+ const apiEndpoints = (await discoverApi(base)).map(({ path, status }) => ({ path, status }));
52207
+ let openPorts = [];
52208
+ if (includeActive && root.status > 0) {
52209
+ const scan = await scanPorts(host);
52210
+ openPorts = scan.map(({ port, service }) => ({ port, service }));
51965
52211
  }
51966
- lines.push("");
51967
- lines.push("## Findings");
51968
- lines.push("");
51969
- if (findings.length === 0) {
51970
- lines.push("_No findings._");
51971
- return lines.join(`
51972
- `) + `
51973
- `;
52212
+ const intel = correlateIntel(techs, versions, openPorts);
52213
+ const nextSteps = [];
52214
+ if (waf.detected)
52215
+ nextSteps.push(`WAF detected (${waf.wafs?.map((w) => w.waf).join(", ")}) — consult detect_waf + WAF bypass playbook before exploitation.`);
52216
+ for (const t of techs) {
52217
+ const c = techCorrelation(t);
52218
+ if (c.found)
52219
+ nextSteps.push(`Tech '${t}' maps to known vuln classes — read tech_correlation('${t}') and fetch nuclei templates with template_lookup('${t}').`);
51974
52220
  }
51975
- for (const f of findings) {
51976
- lines.push(`### ${f.id} — ${f.title} [${severityLabel(f.classification?.severity ?? "informational")}]`);
51977
- lines.push("");
51978
- lines.push(`- **Status**: \`${f.status}\``);
51979
- lines.push(`- **Confidence**: ${f.confidence} (${f.confidence_level})`);
51980
- if (f.classification?.cwe)
51981
- lines.push(`- **CWE**: ${f.classification.cwe}`);
51982
- if (f.classification?.cvss_score != null)
51983
- lines.push(`- **CVSS**: ${f.classification.cvss_score} (${f.classification.cvss_vector ?? ""}) — ${cvssSeverity(f.classification.cvss_score)}`);
51984
- lines.push(`- **Source**: ${f.source?.type} \`${f.source?.name ?? ""}\``);
51985
- lines.push(`- **Sink**: ${f.sink?.type}${f.sink?.symbol ? ` \`${f.sink.symbol}\`` : ""}`);
51986
- if (f.target?.host || f.target?.endpoint)
51987
- lines.push(`- **Target**: ${[f.target.host, f.target.endpoint].filter(Boolean).join("")}`);
51988
- lines.push(`- **Evidence**: ${f.evidence?.length ?? 0} record(s)`);
51989
- if (f.remediation && Object.keys(f.remediation).length) {
51990
- lines.push(`- **Note**: ${JSON.stringify(f.remediation)}`);
51991
- }
51992
- lines.push("");
52221
+ for (const v of versions) {
52222
+ nextSteps.push(`Version ${v.product}/${v.version} detected — check cve_correlation + nvd_lookup for known CVEs affecting ${v.product} ${v.version}.`);
51993
52223
  }
51994
- return lines.join(`
51995
- `) + `
51996
- `;
51997
- }
51998
- function reportJson(findings, opts = {}) {
51999
- const summary = summarize(findings);
52000
- const payload = {
52001
- title: opts.title ?? "Blitz Strike — Security Assessment Report",
52002
- scope: opts.scope ?? null,
52003
- version: opts.version ?? null,
52004
- summary,
52005
- findings,
52006
- integrity: sha256(JSON.stringify({ summary, findings }))
52224
+ if (crawl.endpoints.length > 0)
52225
+ nextSteps.push(`Found ${crawl.endpoints.length} endpoints — feed them to taint_file/blitz_scan for source analysis, or strike_verify for live validation.`);
52226
+ if (params.length > 0)
52227
+ nextSteps.push(`Discovered input params: ${params.join(", ")} — these are attack-surface entry points; fuzz them with payload_lookup after scope confirms.`);
52228
+ if (subdomains.length > 0)
52229
+ nextSteps.push(`Found ${subdomains.length} subdomains — each is a separate scope surface; enumerate further with fofa_search.`);
52230
+ if (dnsFindings.length > 0)
52231
+ 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.`);
52232
+ if (wayback.length > 0)
52233
+ 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.`);
52234
+ if (openPorts.length > 0)
52235
+ 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.`);
52236
+ if (apiEndpoints.length > 0)
52237
+ nextSteps.push(`API endpoints exposed (${apiEndpoints.map((e) => e.path).join(", ")}) — audit them with api-security playbook; check for BOLA/IDOR/auth gaps.`);
52238
+ if (nextSteps.length === 0)
52239
+ nextSteps.push("Nothing exposed on the surface — try subdomain enumeration, port scan (active), or request source access for EAGLE-EYE analysis.");
52240
+ nextSteps.push("Every confirmed finding: record with finding_create, attach confidence_score, redact secrets. A hit is a hypothesis until verified.");
52241
+ return {
52242
+ target: base,
52243
+ host,
52244
+ fingerprint,
52245
+ waf,
52246
+ tech: { detected: techs, server: fingerprint.server },
52247
+ versions,
52248
+ crawl,
52249
+ params,
52250
+ subdomains,
52251
+ dns_findings: dnsFindings,
52252
+ wayback_urls: wayback,
52253
+ api_endpoints: apiEndpoints,
52254
+ open_ports: openPorts,
52255
+ intel,
52256
+ next_steps: nextSteps
52007
52257
  };
52008
- return JSON.stringify(payload, null, 2);
52009
52258
  }
52010
52259
 
52011
52260
  // src/benchmark.ts
@@ -52248,7 +52497,7 @@ function createServer() {
52248
52497
  name: "blitzstrike",
52249
52498
  version: "1.0.0"
52250
52499
  }, {
52251
- 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" + "1. SCOPE FIRST. Call scope_check(target, scope, mode) before ANY active testing. " + " Passive analysis (reading source, fingerprinting public pages) needs no gate; " + " active probing (exploitation, fuzzing, path brute-force) is blocked until scope " + " authorizes it.\\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 gated alternative. Both re-check 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).\\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: full one-call audit (scope gate -> triage -> chain enrichment " + " -> findings with tool manuals attached).\\n" + "- list_languages / taint_file: multi-language taint (PHP, JS/TS, Python, Java).\\n" + "- list_chains: 57 escalation chains (source -> sink -> impact).\\n" + "- tool_lookup / ensure_tool / list_tools: 130-tool catalog, auto-install.\\n" + "- skill_lookup / read_skill / list_skills: 85 playbooks (WP/PHP 0-day, AD, " + " malware, mobile, cloud, etc.).\\n" + "- read_playbook / read_tool_manual / list_manuals: 270 tool manuals + engagement " + " playbooks.\\n" + "- detect_waf / tech_correlation / cve_correlation / port_correlation: signature " + " + correlation intelligence.\\n" + "- nvd_lookup / fofa_search: CVE + asset reconnaissance (FOFA needs env creds).\\n" + "- remember / memory_lookup / memory_list: persist + recall verified knowledge.\\n\\n" + "IRON RULES:\\n" + "- Scope first; authorized targets only. Refuse out-of-scope or destructive/DoS work.\\n" + "- A hit is a hypothesis; a verified exploit (or proven data-flow) is the finding.\\n" + "- Prefer the one-call run_engagement path; fall back to granular tools when you " + " need finer control."
52500
+ 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" + "1. SCOPE FIRST. Call scope_check(target, scope, mode) before ANY active testing. " + " Passive analysis (reading source, fingerprinting public pages) needs no gate; " + " active probing (exploitation, fuzzing, path brute-force) is blocked until scope " + " authorizes it.\\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 gated alternative. Both re-check 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).\\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_languages / taint_file: multi-language taint (PHP, JS/TS, Python, Java).\\n" + "- list_chains: 57 escalation chains (source -> sink -> impact).\\n" + "- tool_lookup / ensure_tool / list_tools: 130-tool catalog, auto-install.\\n" + "- skill_lookup / read_skill / list_skills: 85 playbooks (WP/PHP 0-day, AD, " + " malware, mobile, cloud, etc.).\\n" + "- read_playbook / read_tool_manual / list_manuals: 270 tool manuals + engagement " + " playbooks.\\n" + "- detect_waf / tech_correlation / cve_correlation / port_correlation: signature " + " + correlation intelligence.\\n" + "- nvd_lookup / fofa_search: CVE + asset reconnaissance (FOFA needs env creds).\\n" + "- remember / memory_lookup / memory_list: persist + recall verified knowledge.\\n\\n" + "IRON RULES:\\n" + "- Scope first; authorized targets only. Refuse out-of-scope or destructive/DoS work.\\n" + "- A hit is a hypothesis; a verified exploit (or proven data-flow) is the finding.\\n" + "- Prefer the one-call run_engagement path; fall back to granular tools when you " + " need finer control."
52252
52501
  });
52253
52502
  server.registerTool("blitz_scan", {
52254
52503
  title: "BLITZ scan",
@@ -52437,7 +52686,7 @@ function createServer() {
52437
52686
  });
52438
52687
  server.registerTool("run_engagement", {
52439
52688
  title: "STRIKE full engagement",
52440
- description: "STRIKE: run a full 3-tier audit server-side (scope gate -> triage -> chain enrichment -> findings). One call.",
52689
+ description: "STRIKE: run a full audit in ONE call (no babysitting). Source path -> scope gate -> triage -> chain enrichment -> findings -> inline report (markdown+JSON+SARIF). URL target -> autonomous live marker sweep (strike_verify per parameter) -> confirmed findings -> inline report. Scope-gated: URL targets require an authorized scope.",
52441
52690
  inputSchema: {
52442
52691
  target: string2().describe("Source path or URL"),
52443
52692
  scope: string2().optional().describe("Scope (for URL targets)"),
@@ -52446,7 +52695,8 @@ function createServer() {
52446
52695
  remember: boolean2().optional().describe("Auto-capture matched chains to memory (default true)")
52447
52696
  }
52448
52697
  }, async ({ target, scope, mode, max_files, remember }) => {
52449
- const r = runEngagement(target, scope ?? "", mode ?? "bug-bounty", max_files ?? 2000, remember ?? true);
52698
+ const isUrl = /^https?:\/\//i.test(target);
52699
+ const r = isUrl ? await runLiveEngagement(target, scope ?? "", mode ?? "bug-bounty") : runEngagement(target, scope ?? "", mode ?? "bug-bounty", max_files ?? 2000, remember ?? true);
52450
52700
  return {
52451
52701
  content: [{ type: "text", text: JSON.stringify(r) }]
52452
52702
  };
@@ -53164,12 +53414,89 @@ async function serve() {
53164
53414
  }
53165
53415
 
53166
53416
  // src/cli.ts
53167
- import { readFileSync as readFileSync18, existsSync as existsSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "node:fs";
53168
- import { join as join13, dirname as dirname2 } from "node:path";
53417
+ import { readFileSync as readFileSync20, existsSync as existsSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "node:fs";
53418
+ import { join as join15, dirname as dirname2 } from "node:path";
53169
53419
  import { homedir as homedir5 } from "node:os";
53170
- import { fileURLToPath as fileURLToPath9 } from "node:url";
53420
+ import { fileURLToPath as fileURLToPath11 } from "node:url";
53171
53421
  import { spawnSync as spawnSync3 } from "node:child_process";
53422
+
53423
+ // src/release.ts
53424
+ import { readFileSync as readFileSync18, existsSync as existsSync7 } from "node:fs";
53425
+ import { join as join13 } from "node:path";
53426
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
53427
+ import { execSync as execSync2 } from "node:child_process";
53172
53428
  var ROOT7 = join13(fileURLToPath9(new URL(".", import.meta.url)), "..");
53429
+ function validateRelease() {
53430
+ const checks = [];
53431
+ checks.push({ name: "package.json version", ok: true, detail: VERSION });
53432
+ try {
53433
+ const changelog = readFileSync18(join13(ROOT7, "CHANGELOG.md"), "utf8");
53434
+ const m = changelog.match(/^##\s+\[([0-9]+\.[0-9]+\.[0-9]+)\]/m);
53435
+ const changelogVersion = m?.[1] ?? null;
53436
+ checks.push({
53437
+ name: "CHANGELOG version",
53438
+ ok: changelogVersion === VERSION,
53439
+ detail: changelogVersion ?? "no version header found"
53440
+ });
53441
+ } catch {
53442
+ checks.push({ name: "CHANGELOG version", ok: false, detail: "CHANGELOG.md not found" });
53443
+ }
53444
+ try {
53445
+ const tag = execSync2("git describe --tags --abbrev=0", { cwd: ROOT7, encoding: "utf8" }).trim();
53446
+ checks.push({ name: "Git tag", ok: tag === `v${VERSION}`, detail: tag, soft: true });
53447
+ } catch {
53448
+ checks.push({ name: "Git tag", ok: true, detail: "unavailable (no git)", soft: true });
53449
+ }
53450
+ try {
53451
+ const published = execSync2("npm view blitzstrike version", { encoding: "utf8", timeout: 8000 }).trim();
53452
+ checks.push({ name: "npm registry version", ok: published === VERSION, detail: published, soft: true });
53453
+ } catch {
53454
+ checks.push({ name: "npm registry version", ok: true, detail: "unavailable (offline)", soft: true });
53455
+ }
53456
+ checks.push({
53457
+ name: "Lockfile",
53458
+ ok: lockfilePresent(),
53459
+ detail: lockfilePresent() ? "present (bun.lock / package-lock.json)" : "missing",
53460
+ soft: true
53461
+ });
53462
+ return { version: VERSION, checks, consistent: checks.filter((c) => !c.soft).every((c) => c.ok) };
53463
+ }
53464
+ function lockfilePresent() {
53465
+ return existsSync7(join13(ROOT7, "package-lock.json")) || existsSync7(join13(ROOT7, "bun.lock")) || existsSync7(join13(ROOT7, "bun.lockb"));
53466
+ }
53467
+
53468
+ // src/dependency.ts
53469
+ import { readFileSync as readFileSync19 } from "node:fs";
53470
+ import { join as join14 } from "node:path";
53471
+ import { fileURLToPath as fileURLToPath10 } from "node:url";
53472
+ var ROOT8 = join14(fileURLToPath10(new URL(".", import.meta.url)), "..");
53473
+ function checkDependencies() {
53474
+ let pkg = {};
53475
+ try {
53476
+ pkg = JSON.parse(readFileSync19(join14(ROOT8, "package.json"), "utf8"));
53477
+ } catch {
53478
+ return { install_scripts: [], git_dependencies: [], total_dependencies: 0, safe: false };
53479
+ }
53480
+ const installScriptNames = ["preinstall", "install", "postinstall", "prepare"];
53481
+ const installScripts = installScriptNames.filter((n) => pkg.scripts?.[n]).map((n) => ({ name: n, command: pkg.scripts[n] }));
53482
+ const allDeps = {
53483
+ ...pkg.dependencies ?? {},
53484
+ ...pkg.devDependencies ?? {},
53485
+ ...pkg.peerDependencies ?? {},
53486
+ ...pkg.optionalDependencies ?? {}
53487
+ };
53488
+ const gitDependencies = Object.entries(allDeps).filter(([, v]) => /^(git\+|git:|github:|gitlab:|bitbucket:|\w+:\/\/)/.test(v)).map(([name, v]) => `${name}@${v}`);
53489
+ const total = Object.keys(allDeps).length;
53490
+ return {
53491
+ install_scripts: installScripts,
53492
+ git_dependencies: gitDependencies,
53493
+ total_dependencies: total,
53494
+ safe: installScripts.length === 0 && gitDependencies.length === 0
53495
+ };
53496
+ }
53497
+
53498
+ // src/cli.ts
53499
+ var ROOT9 = join15(fileURLToPath11(new URL(".", import.meta.url)), "..");
53173
53500
  function which(cmd) {
53174
53501
  return spawnSync3(`command -v ${cmd}`, { shell: true }).status === 0;
53175
53502
  }
@@ -53204,13 +53531,27 @@ function runDoctor() {
53204
53531
  detail: fofa ? "set" : "FOFA_EMAIL/FOFA_KEY not set",
53205
53532
  fix: fofa ? undefined : "export FOFA_EMAIL=... && export FOFA_KEY=... (enables fofa_search)"
53206
53533
  });
53207
- const dataOk = existsSync7(join13(ROOT7, "chains.json")) && existsSync7(join13(ROOT7, "tools-catalog.json"));
53534
+ const dataOk = existsSync8(join15(ROOT9, "chains.json")) && existsSync8(join15(ROOT9, "tools-catalog.json"));
53208
53535
  issues.push({
53209
53536
  name: "Data layers (chains + tools-catalog)",
53210
53537
  status: dataOk ? "ok" : "fail",
53211
53538
  detail: dataOk ? "present" : "chains.json or tools-catalog.json missing",
53212
53539
  fix: dataOk ? undefined : "re-install blitzstrike"
53213
53540
  });
53541
+ const release = validateRelease();
53542
+ issues.push({
53543
+ name: "Release metadata",
53544
+ status: release.consistent ? "ok" : "fail",
53545
+ detail: release.consistent ? `v${release.version} consistent` : release.checks.filter((c) => !c.ok && !c.soft).map((c) => c.name).join(", ") || "drift",
53546
+ fix: release.consistent ? undefined : "align package.json version with the latest CHANGELOG entry"
53547
+ });
53548
+ const deps = checkDependencies();
53549
+ issues.push({
53550
+ name: "Dependency surface",
53551
+ status: deps.safe ? "ok" : "warn",
53552
+ detail: deps.safe ? `${deps.total_dependencies} deps, no install scripts` : `${deps.install_scripts.length} install script(s) + ${deps.git_dependencies.length} git dep(s)`,
53553
+ fix: deps.safe ? undefined : "remove install-time scripts and git dependencies"
53554
+ });
53214
53555
  const symbol = { ok: "OK ", warn: "WARN", fail: "FAIL" };
53215
53556
  for (const i of issues) {
53216
53557
  console.log(`${symbol[i.status]} ${i.name} ${i.detail}`);
@@ -53232,21 +53573,21 @@ BlitzStrike — ${verdict}`);
53232
53573
  function resolveCommand() {
53233
53574
  if (which("blitzstrike"))
53234
53575
  return { command: "blitzstrike", args: ["serve", "--mcp"] };
53235
- const src = join13(ROOT7, "src", "index.ts");
53236
- if (which("bun") && existsSync7(src))
53576
+ const src = join15(ROOT9, "src", "index.ts");
53577
+ if (which("bun") && existsSync8(src))
53237
53578
  return { command: "bun", args: ["run", src, "serve", "--mcp"] };
53238
53579
  if (which("npx"))
53239
53580
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
53240
53581
  if (which("bunx"))
53241
53582
  return { command: "bunx", args: ["blitzstrike", "serve", "--mcp"] };
53242
- const dist = join13(ROOT7, "dist", "index.js");
53243
- if (existsSync7(dist))
53583
+ const dist = join15(ROOT9, "dist", "index.js");
53584
+ if (existsSync8(dist))
53244
53585
  return { command: "bun", args: [dist, "serve", "--mcp"] };
53245
53586
  return { command: "npx", args: ["-y", "blitzstrike", "serve", "--mcp"] };
53246
53587
  }
53247
53588
  function readJson(p) {
53248
53589
  try {
53249
- return JSON.parse(readFileSync18(p, "utf8"));
53590
+ return JSON.parse(readFileSync20(p, "utf8"));
53250
53591
  } catch {
53251
53592
  return null;
53252
53593
  }
@@ -53254,7 +53595,7 @@ function readJson(p) {
53254
53595
  function jsonMcpServersWrite(p) {
53255
53596
  return (command, args) => {
53256
53597
  const dir = dirname2(p);
53257
- if (!existsSync7(dir))
53598
+ if (!existsSync8(dir))
53258
53599
  mkdirSync4(dir, { recursive: true });
53259
53600
  const existing = readJson(p) ?? {};
53260
53601
  existing.mcpServers = {
@@ -53267,7 +53608,7 @@ function jsonMcpServersWrite(p) {
53267
53608
  function opencodeWrite(p) {
53268
53609
  return (_command, _args) => {
53269
53610
  const dir = dirname2(p);
53270
- if (!existsSync7(dir))
53611
+ if (!existsSync8(dir))
53271
53612
  mkdirSync4(dir, { recursive: true });
53272
53613
  const existing = readJson(p) ?? {};
53273
53614
  existing.mcp = {
@@ -53279,26 +53620,26 @@ function opencodeWrite(p) {
53279
53620
  };
53280
53621
  }
53281
53622
  function copyOpenCodeAgents() {
53282
- const srcDir = join13(ROOT7, "opencode-agents");
53283
- const dstDir = join13(homedir5(), ".config", "opencode", "agents");
53284
- if (!existsSync7(srcDir))
53623
+ const srcDir = join15(ROOT9, "opencode-agents");
53624
+ const dstDir = join15(homedir5(), ".config", "opencode", "agents");
53625
+ if (!existsSync8(srcDir))
53285
53626
  return;
53286
- if (!existsSync7(dstDir))
53627
+ if (!existsSync8(dstDir))
53287
53628
  mkdirSync4(dstDir, { recursive: true });
53288
53629
  for (const f of ["Blitz Strike.md", "Blitz.md", "Eagle Eye.md", "Strike.md"]) {
53289
- const src = join13(srcDir, f);
53290
- const dst = join13(dstDir, f);
53291
- if (existsSync7(src)) {
53292
- writeFileSync5(dst, readFileSync18(src, "utf8"));
53630
+ const src = join15(srcDir, f);
53631
+ const dst = join15(dstDir, f);
53632
+ if (existsSync8(src)) {
53633
+ writeFileSync5(dst, readFileSync20(src, "utf8"));
53293
53634
  }
53294
53635
  }
53295
53636
  }
53296
53637
  function codexWrite(p) {
53297
53638
  return (command, args) => {
53298
53639
  const dir = dirname2(p);
53299
- if (!existsSync7(dir))
53640
+ if (!existsSync8(dir))
53300
53641
  mkdirSync4(dir, { recursive: true });
53301
- let existing = existsSync7(p) ? readFileSync18(p, "utf8") : "";
53642
+ let existing = existsSync8(p) ? readFileSync20(p, "utf8") : "";
53302
53643
  if (!existing.trimEnd().endsWith(`
53303
53644
  `))
53304
53645
  existing += `
@@ -53315,9 +53656,9 @@ args = ${JSON.stringify(args)}
53315
53656
  function hermesWrite(p) {
53316
53657
  return (command, args) => {
53317
53658
  const dir = dirname2(p);
53318
- if (!existsSync7(dir))
53659
+ if (!existsSync8(dir))
53319
53660
  mkdirSync4(dir, { recursive: true });
53320
- let existing = existsSync7(p) ? readFileSync18(p, "utf8") : "";
53661
+ let existing = existsSync8(p) ? readFileSync20(p, "utf8") : "";
53321
53662
  existing = existing.replace(/^ blitzstrike:\n(?: .*\n?)*/m, "");
53322
53663
  if (!existing.trimEnd().endsWith(`
53323
53664
  `))
@@ -53344,74 +53685,74 @@ mcp_servers:
53344
53685
  function detectAgents() {
53345
53686
  const home = homedir5();
53346
53687
  const agents = [];
53347
- const claudeJson = join13(home, ".claude.json");
53688
+ const claudeJson = join15(home, ".claude.json");
53348
53689
  agents.push({
53349
53690
  name: "Claude Code",
53350
53691
  detectPath: claudeJson,
53351
- installed: () => existsSync7(claudeJson) || which("claude"),
53692
+ installed: () => existsSync8(claudeJson) || which("claude"),
53352
53693
  write: jsonMcpServersWrite(claudeJson)
53353
53694
  });
53354
- const claudeDesktop = join13(home, ".config", "Claude", "claude_desktop_config.json");
53695
+ const claudeDesktop = join15(home, ".config", "Claude", "claude_desktop_config.json");
53355
53696
  agents.push({
53356
53697
  name: "Claude Desktop",
53357
53698
  detectPath: claudeDesktop,
53358
- installed: () => existsSync7(claudeDesktop),
53699
+ installed: () => existsSync8(claudeDesktop),
53359
53700
  write: jsonMcpServersWrite(claudeDesktop)
53360
53701
  });
53361
- const cursor = join13(home, ".cursor", "mcp.json");
53702
+ const cursor = join15(home, ".cursor", "mcp.json");
53362
53703
  agents.push({
53363
53704
  name: "Cursor",
53364
53705
  detectPath: cursor,
53365
- installed: () => existsSync7(join13(home, ".cursor")) || which("cursor"),
53706
+ installed: () => existsSync8(join15(home, ".cursor")) || which("cursor"),
53366
53707
  write: jsonMcpServersWrite(cursor)
53367
53708
  });
53368
- const opencode = join13(home, ".config", "opencode", "opencode.jsonc");
53709
+ const opencode = join15(home, ".config", "opencode", "opencode.jsonc");
53369
53710
  agents.push({
53370
53711
  name: "OpenCode",
53371
53712
  detectPath: opencode,
53372
- installed: () => existsSync7(join13(home, ".config", "opencode")) || which("opencode"),
53713
+ installed: () => existsSync8(join15(home, ".config", "opencode")) || which("opencode"),
53373
53714
  write: opencodeWrite(opencode)
53374
53715
  });
53375
- const codex = join13(home, ".codex", "config.toml");
53716
+ const codex = join15(home, ".codex", "config.toml");
53376
53717
  agents.push({
53377
53718
  name: "Codex",
53378
53719
  detectPath: codex,
53379
- installed: () => existsSync7(join13(home, ".codex")) || which("codex"),
53720
+ installed: () => existsSync8(join15(home, ".codex")) || which("codex"),
53380
53721
  write: codexWrite(codex)
53381
53722
  });
53382
- const hermes = join13(home, ".hermes", "config.yaml");
53723
+ const hermes = join15(home, ".hermes", "config.yaml");
53383
53724
  agents.push({
53384
53725
  name: "Hermes",
53385
53726
  detectPath: hermes,
53386
- installed: () => existsSync7(join13(home, ".hermes")) || which("hermes"),
53727
+ installed: () => existsSync8(join15(home, ".hermes")) || which("hermes"),
53387
53728
  write: hermesWrite(hermes)
53388
53729
  });
53389
- const gemini = join13(home, ".gemini", "settings.json");
53730
+ const gemini = join15(home, ".gemini", "settings.json");
53390
53731
  agents.push({
53391
53732
  name: "Gemini CLI",
53392
53733
  detectPath: gemini,
53393
- installed: () => existsSync7(join13(home, ".gemini")) || which("gemini"),
53734
+ installed: () => existsSync8(join15(home, ".gemini")) || which("gemini"),
53394
53735
  write: jsonMcpServersWrite(gemini)
53395
53736
  });
53396
- const windsurf = join13(home, ".codeium", "windsurf", "mcp_config.json");
53737
+ const windsurf = join15(home, ".codeium", "windsurf", "mcp_config.json");
53397
53738
  agents.push({
53398
53739
  name: "Windsurf",
53399
53740
  detectPath: windsurf,
53400
- installed: () => existsSync7(join13(home, ".codeium")) || which("windsurf"),
53741
+ installed: () => existsSync8(join15(home, ".codeium")) || which("windsurf"),
53401
53742
  write: jsonMcpServersWrite(windsurf)
53402
53743
  });
53403
- const copilot = join13(home, ".copilot", "mcp.json");
53744
+ const copilot = join15(home, ".copilot", "mcp.json");
53404
53745
  agents.push({
53405
53746
  name: "Copilot",
53406
53747
  detectPath: copilot,
53407
- installed: () => existsSync7(join13(home, ".copilot")),
53748
+ installed: () => existsSync8(join15(home, ".copilot")),
53408
53749
  write: jsonMcpServersWrite(copilot)
53409
53750
  });
53410
- const cline = join13(home, ".cline", "mcp_settings.json");
53751
+ const cline = join15(home, ".cline", "mcp_settings.json");
53411
53752
  agents.push({
53412
53753
  name: "Cline",
53413
53754
  detectPath: cline,
53414
- installed: () => existsSync7(join13(home, ".cline")),
53755
+ installed: () => existsSync8(join15(home, ".cline")),
53415
53756
  write: jsonMcpServersWrite(cline)
53416
53757
  });
53417
53758
  return agents;
@@ -53455,15 +53796,15 @@ Registered with ${ok}/${installed.length} agent(s).`);
53455
53796
  }
53456
53797
 
53457
53798
  // src/index.ts
53458
- import { readFileSync as readFileSync19, existsSync as existsSync8 } from "node:fs";
53459
- import { join as join14 } from "node:path";
53460
- import { fileURLToPath as fileURLToPath10 } from "node:url";
53461
- var ROOT8 = join14(fileURLToPath10(new URL(".", import.meta.url)), "..");
53799
+ import { readFileSync as readFileSync21, existsSync as existsSync9 } from "node:fs";
53800
+ import { join as join16 } from "node:path";
53801
+ import { fileURLToPath as fileURLToPath12 } from "node:url";
53802
+ var ROOT10 = join16(fileURLToPath12(new URL(".", import.meta.url)), "..");
53462
53803
  function readVersion() {
53463
53804
  try {
53464
- const p = join14(ROOT8, "package.json");
53465
- if (existsSync8(p))
53466
- return JSON.parse(readFileSync19(p, "utf8")).version ?? "1.0.0";
53805
+ const p = join16(ROOT10, "package.json");
53806
+ if (existsSync9(p))
53807
+ return JSON.parse(readFileSync21(p, "utf8")).version ?? "1.0.0";
53467
53808
  } catch {}
53468
53809
  return "1.0.0";
53469
53810
  }