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.
- package/CHANGELOG.md +101 -0
- package/dist/index.js +2293 -1952
- 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/
|
|
30242
|
-
var
|
|
30243
|
-
|
|
30244
|
-
|
|
30245
|
-
|
|
30246
|
-
|
|
30247
|
-
|
|
30248
|
-
|
|
30249
|
-
|
|
30250
|
-
|
|
30251
|
-
|
|
30252
|
-
|
|
30253
|
-
|
|
30254
|
-
|
|
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
|
|
30258
|
-
|
|
30259
|
-
if (
|
|
30260
|
-
|
|
30261
|
-
|
|
30262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30301
|
-
|
|
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
|
|
30304
|
-
|
|
30305
|
-
|
|
30306
|
-
|
|
30307
|
-
|
|
30308
|
-
|
|
30309
|
-
|
|
30310
|
-
|
|
30311
|
-
|
|
30312
|
-
|
|
30313
|
-
|
|
30314
|
-
|
|
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
|
|
30323
|
-
|
|
30324
|
-
|
|
30325
|
-
|
|
30326
|
-
|
|
30327
|
-
|
|
30328
|
-
|
|
30329
|
-
|
|
30330
|
-
|
|
30331
|
-
|
|
30332
|
-
|
|
30333
|
-
|
|
30334
|
-
|
|
30335
|
-
|
|
30336
|
-
|
|
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
|
-
|
|
30340
|
-
|
|
30341
|
-
|
|
30342
|
-
|
|
30343
|
-
|
|
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
|
-
|
|
30348
|
-
|
|
30349
|
-
|
|
30350
|
-
|
|
30351
|
-
|
|
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
|
|
30355
|
-
const
|
|
30356
|
-
|
|
30357
|
-
|
|
30358
|
-
|
|
30359
|
-
|
|
30360
|
-
|
|
30361
|
-
|
|
30362
|
-
|
|
30363
|
-
|
|
30364
|
-
|
|
30365
|
-
|
|
30366
|
-
|
|
30367
|
-
|
|
30368
|
-
|
|
30369
|
-
})
|
|
30370
|
-
|
|
30371
|
-
|
|
30372
|
-
|
|
30373
|
-
|
|
30374
|
-
|
|
30375
|
-
|
|
30376
|
-
|
|
30377
|
-
|
|
30378
|
-
|
|
30379
|
-
|
|
30380
|
-
}
|
|
30381
|
-
|
|
30382
|
-
|
|
30383
|
-
|
|
30384
|
-
|
|
30385
|
-
|
|
30386
|
-
|
|
30387
|
-
|
|
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
|
-
|
|
30418
|
-
|
|
30419
|
-
|
|
30420
|
-
const
|
|
30421
|
-
|
|
30422
|
-
|
|
30423
|
-
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
|
|
30427
|
-
|
|
30428
|
-
|
|
30429
|
-
|
|
30430
|
-
|
|
30431
|
-
|
|
30432
|
-
|
|
30433
|
-
|
|
30434
|
-
|
|
30435
|
-
|
|
30436
|
-
|
|
30437
|
-
|
|
30438
|
-
|
|
30439
|
-
|
|
30440
|
-
|
|
30441
|
-
|
|
30442
|
-
|
|
30443
|
-
|
|
30444
|
-
|
|
30445
|
-
|
|
30446
|
-
|
|
30447
|
-
|
|
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
|
-
|
|
30503
|
+
lines.push("");
|
|
30464
30504
|
}
|
|
30465
|
-
return
|
|
30505
|
+
return lines.join(`
|
|
30506
|
+
`) + `
|
|
30507
|
+
`;
|
|
30466
30508
|
}
|
|
30467
|
-
function
|
|
30468
|
-
const
|
|
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
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
|
|
30476
|
-
|
|
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/
|
|
30482
|
-
|
|
30483
|
-
|
|
30484
|
-
|
|
30485
|
-
|
|
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
|
|
30494
|
-
|
|
30495
|
-
|
|
30496
|
-
|
|
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
|
|
30501
|
-
|
|
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
|
|
30524
|
-
const
|
|
30525
|
-
|
|
30526
|
-
|
|
30527
|
-
|
|
30528
|
-
|
|
30529
|
-
|
|
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
|
|
30548
|
-
const
|
|
30549
|
-
const
|
|
30550
|
-
|
|
30551
|
-
|
|
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
|
-
|
|
30554
|
-
|
|
30555
|
-
|
|
30556
|
-
|
|
30557
|
-
|
|
30558
|
-
|
|
30559
|
-
|
|
30560
|
-
|
|
30561
|
-
|
|
30562
|
-
|
|
30563
|
-
|
|
30564
|
-
|
|
30565
|
-
|
|
30566
|
-
|
|
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
|
-
|
|
30570
|
-
|
|
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
|
|
30574
|
-
|
|
30575
|
-
|
|
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
|
-
|
|
30600
|
-
|
|
30601
|
-
|
|
30602
|
-
|
|
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/
|
|
30616
|
-
|
|
30617
|
-
|
|
30618
|
-
|
|
30619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30723
|
+
_chainsCache = [];
|
|
30724
|
+
return [];
|
|
30631
30725
|
}
|
|
30632
30726
|
}
|
|
30633
|
-
function
|
|
30634
|
-
|
|
30635
|
-
if (
|
|
30636
|
-
|
|
30637
|
-
|
|
30638
|
-
|
|
30639
|
-
|
|
30640
|
-
|
|
30641
|
-
|
|
30642
|
-
const
|
|
30643
|
-
|
|
30644
|
-
|
|
30645
|
-
|
|
30646
|
-
|
|
30647
|
-
|
|
30648
|
-
|
|
30649
|
-
|
|
30650
|
-
|
|
30651
|
-
|
|
30652
|
-
|
|
30653
|
-
|
|
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
|
-
|
|
30658
|
-
|
|
30659
|
-
|
|
30660
|
-
|
|
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
|
-
|
|
30664
|
-
|
|
30665
|
-
|
|
30666
|
-
|
|
30667
|
-
|
|
30668
|
-
|
|
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
|
-
|
|
30673
|
-
|
|
30674
|
-
|
|
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
|
|
30678
|
-
const
|
|
30679
|
-
|
|
30680
|
-
|
|
30681
|
-
|
|
30682
|
-
|
|
30683
|
-
|
|
30684
|
-
|
|
30685
|
-
|
|
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
|
-
|
|
30688
|
-
|
|
30689
|
-
|
|
30690
|
-
|
|
30691
|
-
|
|
30692
|
-
|
|
30693
|
-
|
|
30694
|
-
|
|
30695
|
-
|
|
30696
|
-
|
|
30697
|
-
|
|
30698
|
-
|
|
30699
|
-
|
|
30700
|
-
|
|
30701
|
-
|
|
30702
|
-
|
|
30703
|
-
|
|
30704
|
-
|
|
30705
|
-
|
|
30706
|
-
|
|
30707
|
-
|
|
30708
|
-
|
|
30709
|
-
|
|
30710
|
-
|
|
30711
|
-
|
|
30712
|
-
|
|
30713
|
-
|
|
30714
|
-
|
|
30715
|
-
|
|
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
|
-
|
|
30718
|
-
}
|
|
30719
|
-
|
|
30720
|
-
const
|
|
30721
|
-
|
|
30722
|
-
|
|
30723
|
-
|
|
30724
|
-
|
|
30725
|
-
|
|
30726
|
-
|
|
30727
|
-
|
|
30728
|
-
|
|
30729
|
-
|
|
30730
|
-
|
|
30731
|
-
|
|
30732
|
-
|
|
30733
|
-
|
|
30734
|
-
|
|
30735
|
-
|
|
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
|
|
30739
|
-
const
|
|
30740
|
-
if (!data)
|
|
30741
|
-
return { error: "attack_vectors.json not loaded" };
|
|
30964
|
+
function listChains() {
|
|
30965
|
+
const chains = loadChains();
|
|
30742
30966
|
return {
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
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
|
-
|
|
30749
|
-
|
|
30750
|
-
|
|
30751
|
-
|
|
30752
|
-
|
|
30753
|
-
|
|
30754
|
-
|
|
30755
|
-
|
|
30756
|
-
return
|
|
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
|
-
|
|
30759
|
-
|
|
30760
|
-
|
|
30761
|
-
|
|
30762
|
-
|
|
30763
|
-
|
|
30764
|
-
|
|
30765
|
-
|
|
30766
|
-
|
|
30767
|
-
}
|
|
30768
|
-
|
|
30769
|
-
|
|
30770
|
-
|
|
30771
|
-
|
|
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
|
-
|
|
30774
|
-
|
|
30775
|
-
|
|
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
|
-
|
|
31046
|
+
const raw = JSON.parse(readFileSync7(join7(ROOT2, "tools-catalog.json"), "utf8"));
|
|
31047
|
+
_toolsCache = raw.tools ?? [];
|
|
30778
31048
|
} catch {
|
|
30779
|
-
|
|
31049
|
+
_toolsCache = [];
|
|
30780
31050
|
}
|
|
31051
|
+
return _toolsCache;
|
|
30781
31052
|
}
|
|
30782
|
-
function
|
|
30783
|
-
|
|
30784
|
-
|
|
30785
|
-
const
|
|
30786
|
-
|
|
30787
|
-
|
|
30788
|
-
|
|
30789
|
-
|
|
30790
|
-
|
|
30791
|
-
|
|
30792
|
-
|
|
30793
|
-
|
|
30794
|
-
|
|
30795
|
-
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
|
|
30799
|
-
|
|
30800
|
-
|
|
30801
|
-
|
|
30802
|
-
|
|
30803
|
-
|
|
30804
|
-
|
|
30805
|
-
|
|
30806
|
-
|
|
30807
|
-
|
|
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
|
|
30810
|
-
const
|
|
30811
|
-
|
|
30812
|
-
|
|
30813
|
-
|
|
30814
|
-
|
|
30815
|
-
|
|
30816
|
-
|
|
30817
|
-
|
|
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
|
|
31098
|
+
return result;
|
|
30820
31099
|
}
|
|
30821
|
-
function
|
|
30822
|
-
const
|
|
30823
|
-
const
|
|
30824
|
-
|
|
30825
|
-
|
|
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
|
-
|
|
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
|
|
30841
|
-
|
|
30842
|
-
|
|
30843
|
-
|
|
30844
|
-
|
|
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
|
|
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
|
-
|
|
30850
|
-
|
|
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
|
|
31139
|
+
return { found: false, query: name, error: "unreadable" };
|
|
30862
31140
|
}
|
|
30863
31141
|
}
|
|
30864
|
-
function
|
|
30865
|
-
const
|
|
30866
|
-
const
|
|
30867
|
-
|
|
30868
|
-
|
|
30869
|
-
|
|
30870
|
-
|
|
30871
|
-
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
|
|
30883
|
-
|
|
30884
|
-
|
|
30885
|
-
|
|
30886
|
-
|
|
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/
|
|
30900
|
-
|
|
30901
|
-
|
|
30902
|
-
|
|
30903
|
-
|
|
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
|
-
|
|
30906
|
-
|
|
30907
|
-
|
|
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
|
-
|
|
30986
|
-
const
|
|
30987
|
-
|
|
30988
|
-
|
|
30989
|
-
|
|
30990
|
-
|
|
30991
|
-
|
|
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
|
|
31002
|
-
|
|
31003
|
-
|
|
31004
|
-
|
|
31005
|
-
|
|
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
|
-
|
|
31009
|
-
const
|
|
31010
|
-
|
|
31011
|
-
|
|
31012
|
-
|
|
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
|
-
|
|
31015
|
-
|
|
31016
|
-
|
|
31017
|
-
|
|
31018
|
-
|
|
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
|
-
|
|
31044
|
-
|
|
31045
|
-
|
|
31046
|
-
|
|
31047
|
-
|
|
31048
|
-
|
|
31049
|
-
|
|
31050
|
-
|
|
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
|
|
31240
|
+
return { found: true, tech: key, vulns: entry.vulns ?? [], paths: entry.paths ?? [], detection: entry.detection ?? { vulns: entry.vulns, paths: entry.paths } };
|
|
31068
31241
|
}
|
|
31069
|
-
|
|
31070
|
-
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
|
|
31075
|
-
|
|
31076
|
-
|
|
31077
|
-
|
|
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
|
-
|
|
31090
|
-
|
|
31091
|
-
|
|
31092
|
-
|
|
31093
|
-
|
|
31094
|
-
|
|
31095
|
-
|
|
31096
|
-
|
|
31097
|
-
|
|
31098
|
-
|
|
31099
|
-
|
|
31100
|
-
|
|
31101
|
-
|
|
31102
|
-
|
|
31103
|
-
|
|
31104
|
-
|
|
31105
|
-
|
|
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
|
|
31270
|
+
return { categories: Object.keys(data), fuzz_points: data.FUZZ_POINTS ?? null, payloads: data.FUZZ_PAYLOADS ?? null };
|
|
31116
31271
|
}
|
|
31117
|
-
function
|
|
31118
|
-
|
|
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
|
|
31121
|
-
const
|
|
31122
|
-
|
|
31123
|
-
|
|
31124
|
-
|
|
31125
|
-
|
|
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
|
-
|
|
31330
|
+
return readdirSync3(PAYLOADS, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
31128
31331
|
} catch {
|
|
31129
|
-
return
|
|
31332
|
+
return [];
|
|
31130
31333
|
}
|
|
31131
|
-
|
|
31132
|
-
|
|
31133
|
-
const
|
|
31134
|
-
const
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
|
|
31140
|
-
|
|
31141
|
-
|
|
31142
|
-
|
|
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
|
-
|
|
31145
|
-
|
|
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
|
|
31372
|
+
return { found: false, topic, suggestions: cats.slice(0, 20) };
|
|
31177
31373
|
}
|
|
31178
|
-
function
|
|
31179
|
-
const
|
|
31180
|
-
|
|
31181
|
-
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31188
|
-
|
|
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
|
-
|
|
31201
|
-
|
|
31202
|
-
|
|
31203
|
-
|
|
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
|
|
31215
|
-
|
|
31216
|
-
|
|
31217
|
-
|
|
31218
|
-
|
|
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
|
|
31231
|
-
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
|
|
31235
|
-
|
|
31236
|
-
|
|
31237
|
-
|
|
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
|
|
31322
|
-
|
|
31323
|
-
|
|
31324
|
-
|
|
31325
|
-
|
|
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
|
-
|
|
31367
|
-
|
|
31368
|
-
|
|
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
|
|
31404
|
-
|
|
31405
|
-
|
|
31406
|
-
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31413
|
-
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
|
|
31417
|
-
|
|
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
|
-
|
|
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
|
|
31439
|
-
|
|
31440
|
-
|
|
31441
|
-
|
|
31442
|
-
|
|
31443
|
-
|
|
31444
|
-
|
|
31445
|
-
|
|
31446
|
-
|
|
31447
|
-
|
|
31448
|
-
|
|
31449
|
-
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
31453
|
-
|
|
31454
|
-
|
|
31455
|
-
|
|
31456
|
-
|
|
31457
|
-
|
|
31458
|
-
|
|
31459
|
-
|
|
31460
|
-
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
31464
|
-
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
|
|
31473
|
-
|
|
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
|
-
|
|
31496
|
-
|
|
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
|
|
31536
|
+
return out;
|
|
31499
31537
|
}
|
|
31500
|
-
function
|
|
31501
|
-
const
|
|
31502
|
-
|
|
31503
|
-
|
|
31504
|
-
|
|
31505
|
-
|
|
31506
|
-
|
|
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
|
-
|
|
31509
|
-
|
|
31510
|
-
|
|
31511
|
-
|
|
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
|
-
|
|
31515
|
-
const
|
|
31516
|
-
for (const
|
|
31517
|
-
|
|
31518
|
-
|
|
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
|
-
|
|
31581
|
-
|
|
31582
|
-
|
|
31583
|
-
|
|
31584
|
-
|
|
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
|
|
31587
|
-
|
|
31588
|
-
|
|
31589
|
-
result.
|
|
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/
|
|
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/
|
|
31603
|
-
|
|
31604
|
-
|
|
31605
|
-
|
|
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
|
|
31610
|
-
return
|
|
31764
|
+
function isNode(v) {
|
|
31765
|
+
return v && typeof v === "object" && typeof v.kind === "string";
|
|
31611
31766
|
}
|
|
31612
|
-
function
|
|
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 (
|
|
31775
|
+
if (isNode(c))
|
|
31621
31776
|
out.push(c);
|
|
31622
|
-
} else if (
|
|
31777
|
+
} else if (isNode(v)) {
|
|
31623
31778
|
out.push(v);
|
|
31624
31779
|
}
|
|
31625
31780
|
}
|
|
31626
31781
|
return out;
|
|
31627
31782
|
}
|
|
31628
|
-
function
|
|
31629
|
-
if (!
|
|
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 (
|
|
31632
|
-
return node.name;
|
|
31633
|
-
if (
|
|
31634
|
-
return
|
|
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
|
|
31638
|
-
if (
|
|
31639
|
-
return
|
|
31640
|
-
if (kind === "
|
|
31641
|
-
|
|
31642
|
-
|
|
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
|
|
31645
|
-
|
|
31646
|
-
if (!
|
|
31647
|
-
|
|
31648
|
-
|
|
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
|
-
|
|
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
|
|
31653
|
-
|
|
31654
|
-
|
|
31655
|
-
|
|
31656
|
-
|
|
31657
|
-
|
|
31658
|
-
|
|
31659
|
-
|
|
31660
|
-
}
|
|
31661
|
-
|
|
31662
|
-
|
|
31663
|
-
|
|
31664
|
-
|
|
31665
|
-
|
|
31666
|
-
|
|
31667
|
-
|
|
31668
|
-
|
|
31669
|
-
|
|
31670
|
-
|
|
31671
|
-
|
|
31672
|
-
|
|
31673
|
-
|
|
31674
|
-
|
|
31675
|
-
|
|
31676
|
-
|
|
31677
|
-
|
|
31678
|
-
|
|
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
|
|
31725
|
-
|
|
31726
|
-
if (!isNode2(what))
|
|
31874
|
+
function walkExpression(node, env, summaries, findings, suppressed, file) {
|
|
31875
|
+
if (!isNode(node))
|
|
31727
31876
|
return;
|
|
31728
|
-
if (
|
|
31729
|
-
const
|
|
31730
|
-
|
|
31731
|
-
|
|
31732
|
-
|
|
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 (
|
|
31741
|
-
const
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
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
|
-
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
if (
|
|
31763
|
-
const
|
|
31764
|
-
|
|
31765
|
-
|
|
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
|
-
|
|
31770
|
-
|
|
31771
|
-
|
|
31772
|
-
|
|
31773
|
-
|
|
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
|
|
31782
|
-
|
|
31783
|
-
|
|
31784
|
-
|
|
31785
|
-
|
|
31786
|
-
|
|
31787
|
-
|
|
31788
|
-
|
|
31789
|
-
|
|
31790
|
-
|
|
31791
|
-
|
|
31792
|
-
|
|
31793
|
-
|
|
31794
|
-
|
|
31795
|
-
|
|
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
|
|
31798
|
-
const
|
|
31799
|
-
|
|
31800
|
-
|
|
31801
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
32162
|
+
function isNode2(v) {
|
|
31812
32163
|
return !!v && typeof v === "object" && typeof v.kind === "string";
|
|
31813
32164
|
}
|
|
31814
|
-
function
|
|
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 (
|
|
32173
|
+
if (isNode2(c))
|
|
31823
32174
|
out.push(c);
|
|
31824
|
-
} else if (
|
|
32175
|
+
} else if (isNode2(v)) {
|
|
31825
32176
|
out.push(v);
|
|
31826
32177
|
}
|
|
31827
32178
|
}
|
|
31828
32179
|
return out;
|
|
31829
32180
|
}
|
|
31830
|
-
function
|
|
31831
|
-
if (!
|
|
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 (
|
|
31845
|
-
return
|
|
32186
|
+
if (isNode2(node.name))
|
|
32187
|
+
return nameOf(node.name);
|
|
31846
32188
|
return null;
|
|
31847
32189
|
}
|
|
31848
|
-
function
|
|
31849
|
-
if (
|
|
31850
|
-
return
|
|
31851
|
-
if (
|
|
31852
|
-
return
|
|
31853
|
-
|
|
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
|
|
31860
|
-
|
|
31861
|
-
|
|
31862
|
-
|
|
31863
|
-
|
|
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
|
-
|
|
31868
|
-
|
|
31869
|
-
|
|
31870
|
-
|
|
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
|
-
|
|
31873
|
-
|
|
31874
|
-
|
|
31875
|
-
|
|
31876
|
-
|
|
31877
|
-
|
|
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
|
|
52138
|
+
return out;
|
|
51896
52139
|
}
|
|
51897
|
-
function
|
|
51898
|
-
|
|
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
|
-
|
|
51902
|
-
|
|
51903
|
-
|
|
51904
|
-
|
|
51905
|
-
|
|
51906
|
-
|
|
51907
|
-
|
|
51908
|
-
|
|
51909
|
-
|
|
51910
|
-
|
|
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
|
|
51913
|
-
|
|
51914
|
-
|
|
51915
|
-
|
|
51916
|
-
|
|
51917
|
-
|
|
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
|
-
|
|
51924
|
-
|
|
51925
|
-
|
|
51926
|
-
|
|
51927
|
-
|
|
51928
|
-
|
|
51929
|
-
|
|
51930
|
-
|
|
51931
|
-
|
|
51932
|
-
|
|
51933
|
-
|
|
51934
|
-
|
|
51935
|
-
|
|
51936
|
-
|
|
51937
|
-
|
|
51938
|
-
|
|
51939
|
-
|
|
51940
|
-
|
|
51941
|
-
|
|
51942
|
-
|
|
51943
|
-
|
|
51944
|
-
|
|
51945
|
-
|
|
51946
|
-
|
|
51947
|
-
|
|
51948
|
-
|
|
51949
|
-
|
|
51950
|
-
|
|
51951
|
-
|
|
51952
|
-
|
|
51953
|
-
|
|
51954
|
-
|
|
51955
|
-
|
|
51956
|
-
|
|
51957
|
-
|
|
51958
|
-
|
|
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
|
-
|
|
51967
|
-
|
|
51968
|
-
|
|
51969
|
-
|
|
51970
|
-
|
|
51971
|
-
|
|
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
|
|
51976
|
-
|
|
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
|
-
|
|
51995
|
-
`
|
|
51996
|
-
|
|
51997
|
-
}
|
|
51998
|
-
|
|
51999
|
-
|
|
52000
|
-
|
|
52001
|
-
|
|
52002
|
-
|
|
52003
|
-
|
|
52004
|
-
|
|
52005
|
-
|
|
52006
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
53168
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 =
|
|
53236
|
-
if (which("bun") &&
|
|
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 =
|
|
53243
|
-
if (
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
53283
|
-
const dstDir =
|
|
53284
|
-
if (!
|
|
53623
|
+
const srcDir = join15(ROOT9, "opencode-agents");
|
|
53624
|
+
const dstDir = join15(homedir5(), ".config", "opencode", "agents");
|
|
53625
|
+
if (!existsSync8(srcDir))
|
|
53285
53626
|
return;
|
|
53286
|
-
if (!
|
|
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 =
|
|
53290
|
-
const dst =
|
|
53291
|
-
if (
|
|
53292
|
-
writeFileSync5(dst,
|
|
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 (!
|
|
53640
|
+
if (!existsSync8(dir))
|
|
53300
53641
|
mkdirSync4(dir, { recursive: true });
|
|
53301
|
-
let existing =
|
|
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 (!
|
|
53659
|
+
if (!existsSync8(dir))
|
|
53319
53660
|
mkdirSync4(dir, { recursive: true });
|
|
53320
|
-
let existing =
|
|
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 =
|
|
53688
|
+
const claudeJson = join15(home, ".claude.json");
|
|
53348
53689
|
agents.push({
|
|
53349
53690
|
name: "Claude Code",
|
|
53350
53691
|
detectPath: claudeJson,
|
|
53351
|
-
installed: () =>
|
|
53692
|
+
installed: () => existsSync8(claudeJson) || which("claude"),
|
|
53352
53693
|
write: jsonMcpServersWrite(claudeJson)
|
|
53353
53694
|
});
|
|
53354
|
-
const claudeDesktop =
|
|
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: () =>
|
|
53699
|
+
installed: () => existsSync8(claudeDesktop),
|
|
53359
53700
|
write: jsonMcpServersWrite(claudeDesktop)
|
|
53360
53701
|
});
|
|
53361
|
-
const cursor =
|
|
53702
|
+
const cursor = join15(home, ".cursor", "mcp.json");
|
|
53362
53703
|
agents.push({
|
|
53363
53704
|
name: "Cursor",
|
|
53364
53705
|
detectPath: cursor,
|
|
53365
|
-
installed: () =>
|
|
53706
|
+
installed: () => existsSync8(join15(home, ".cursor")) || which("cursor"),
|
|
53366
53707
|
write: jsonMcpServersWrite(cursor)
|
|
53367
53708
|
});
|
|
53368
|
-
const opencode =
|
|
53709
|
+
const opencode = join15(home, ".config", "opencode", "opencode.jsonc");
|
|
53369
53710
|
agents.push({
|
|
53370
53711
|
name: "OpenCode",
|
|
53371
53712
|
detectPath: opencode,
|
|
53372
|
-
installed: () =>
|
|
53713
|
+
installed: () => existsSync8(join15(home, ".config", "opencode")) || which("opencode"),
|
|
53373
53714
|
write: opencodeWrite(opencode)
|
|
53374
53715
|
});
|
|
53375
|
-
const codex =
|
|
53716
|
+
const codex = join15(home, ".codex", "config.toml");
|
|
53376
53717
|
agents.push({
|
|
53377
53718
|
name: "Codex",
|
|
53378
53719
|
detectPath: codex,
|
|
53379
|
-
installed: () =>
|
|
53720
|
+
installed: () => existsSync8(join15(home, ".codex")) || which("codex"),
|
|
53380
53721
|
write: codexWrite(codex)
|
|
53381
53722
|
});
|
|
53382
|
-
const hermes =
|
|
53723
|
+
const hermes = join15(home, ".hermes", "config.yaml");
|
|
53383
53724
|
agents.push({
|
|
53384
53725
|
name: "Hermes",
|
|
53385
53726
|
detectPath: hermes,
|
|
53386
|
-
installed: () =>
|
|
53727
|
+
installed: () => existsSync8(join15(home, ".hermes")) || which("hermes"),
|
|
53387
53728
|
write: hermesWrite(hermes)
|
|
53388
53729
|
});
|
|
53389
|
-
const gemini =
|
|
53730
|
+
const gemini = join15(home, ".gemini", "settings.json");
|
|
53390
53731
|
agents.push({
|
|
53391
53732
|
name: "Gemini CLI",
|
|
53392
53733
|
detectPath: gemini,
|
|
53393
|
-
installed: () =>
|
|
53734
|
+
installed: () => existsSync8(join15(home, ".gemini")) || which("gemini"),
|
|
53394
53735
|
write: jsonMcpServersWrite(gemini)
|
|
53395
53736
|
});
|
|
53396
|
-
const windsurf =
|
|
53737
|
+
const windsurf = join15(home, ".codeium", "windsurf", "mcp_config.json");
|
|
53397
53738
|
agents.push({
|
|
53398
53739
|
name: "Windsurf",
|
|
53399
53740
|
detectPath: windsurf,
|
|
53400
|
-
installed: () =>
|
|
53741
|
+
installed: () => existsSync8(join15(home, ".codeium")) || which("windsurf"),
|
|
53401
53742
|
write: jsonMcpServersWrite(windsurf)
|
|
53402
53743
|
});
|
|
53403
|
-
const copilot =
|
|
53744
|
+
const copilot = join15(home, ".copilot", "mcp.json");
|
|
53404
53745
|
agents.push({
|
|
53405
53746
|
name: "Copilot",
|
|
53406
53747
|
detectPath: copilot,
|
|
53407
|
-
installed: () =>
|
|
53748
|
+
installed: () => existsSync8(join15(home, ".copilot")),
|
|
53408
53749
|
write: jsonMcpServersWrite(copilot)
|
|
53409
53750
|
});
|
|
53410
|
-
const cline =
|
|
53751
|
+
const cline = join15(home, ".cline", "mcp_settings.json");
|
|
53411
53752
|
agents.push({
|
|
53412
53753
|
name: "Cline",
|
|
53413
53754
|
detectPath: cline,
|
|
53414
|
-
installed: () =>
|
|
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
|
|
53459
|
-
import { join as
|
|
53460
|
-
import { fileURLToPath as
|
|
53461
|
-
var
|
|
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 =
|
|
53465
|
-
if (
|
|
53466
|
-
return JSON.parse(
|
|
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
|
}
|