mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-03 09:57:50 +02:00
repl: idle guardrail, multi-target, results navigation; deeper recon prompts
REPL (v3.5.5): - /timeout <min>: idle guardrail — if no NEW finding lands within the window the run soft-stops and validates what was found (default 5 min; 0 disables). - /target accepts a comma-separated list; /run tests them SEQUENTIALLY (a queue auto-advances to the next target when the current run finishes; one report each). - /results (no arg, interactive): navigation browser — pick target/run → pick vulnerability → full detail; Esc steps back a level (vuln → target → session). - /report (no arg, multiple runs): pick which report to open from a menu. - /show now shows idle-stop; help updated. Agent prompts: - RECON_SYS deepened: crawl + params/headers/cookies, DOWNLOAD & analyze linked JS (endpoints, hidden params, GraphQL, secrets, sourceMappingURL), fingerprint exact versions, response-differential analysis; richer JSON schema. - tool_doctrine adds JS-analysis and request/response-analysis guidance (linkfinder/gau/katana, header/cookie/timing/length differentials).
This commit is contained in:
+169
-28
@@ -119,7 +119,7 @@ struct LiveCheckpoint {
|
||||
const COMMANDS: &[&str] = &[
|
||||
"/help", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target",
|
||||
"/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline",
|
||||
"/votes", "/chain", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
|
||||
"/votes", "/chain", "/timeout", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
|
||||
"/status", "/diff", "/retest", "/integrations", "/quit",
|
||||
];
|
||||
|
||||
@@ -214,6 +214,9 @@ struct Session {
|
||||
vote_n: usize,
|
||||
max_agents: usize,
|
||||
chain_depth: usize,
|
||||
/// Idle guardrail: stop a run if no NEW finding lands in this many seconds
|
||||
/// (0 = disabled). Set in minutes via `/timeout <mins>`.
|
||||
idle_secs: u64,
|
||||
offline: bool,
|
||||
target: Option<String>,
|
||||
repo: Option<String>,
|
||||
@@ -233,6 +236,7 @@ impl Default for Session {
|
||||
vote_n: 3,
|
||||
max_agents: 0,
|
||||
chain_depth: 2,
|
||||
idle_secs: 300, // 5-minute idle guardrail by default
|
||||
offline: false,
|
||||
target: None,
|
||||
repo: None,
|
||||
@@ -353,9 +357,16 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
println!();
|
||||
let mut reader = Reader::new(base);
|
||||
let mut active: Option<ActiveRun> = None;
|
||||
let mut queue: Vec<String> = Vec::new(); // remaining targets for a multi-target /run
|
||||
show(&s);
|
||||
|
||||
loop {
|
||||
// Multi-target queue: when the current run finishes, auto-start the next.
|
||||
if !queue.is_empty() && active.as_ref().map(|a| a.done.load(Ordering::Relaxed)).unwrap_or(true) {
|
||||
let next = queue.remove(0);
|
||||
println!("\n \x1b[1;35m▶ next target\x1b[0m ({} left): {next}", queue.len());
|
||||
active = start_background(base, &s, &mut reader, history.clone(), Some(&next)).await;
|
||||
}
|
||||
println!("{}", context_prompt(&s)); // dim context line above the prompt
|
||||
let Some(line) = reader.read(PROMPT) else { println!("\n bye."); break };
|
||||
let line = line.trim();
|
||||
@@ -404,10 +415,28 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
println!(" subscription: {}", onoff(s.subscription));
|
||||
}
|
||||
"/target" | "/url" => {
|
||||
if arg.is_empty() { println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none) — set with /target <url>, clear with /target clear".into())); }
|
||||
if arg.is_empty() { println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none) — set with /target <url[,url2,...]>, clear with /target clear".into())); }
|
||||
else if arg == "clear" { s.target = None; println!(" target cleared"); }
|
||||
else { let t = if arg.starts_with("http") { arg.to_string() } else { format!("https://{arg}") };
|
||||
s.target = Some(t.clone()); println!(" target: {t}"); }
|
||||
else {
|
||||
// Accept one URL or a comma-separated list; normalize each.
|
||||
let ts: Vec<String> = arg.split(',').map(|x| x.trim()).filter(|x| !x.is_empty())
|
||||
.map(|x| if x.starts_with("http") { x.to_string() } else { format!("https://{x}") })
|
||||
.collect();
|
||||
s.target = Some(ts.join(","));
|
||||
if ts.len() > 1 { println!(" targets ({}): {}", ts.len(), ts.join(", ")); println!(" \x1b[2m/run tests them sequentially, one report each\x1b[0m"); }
|
||||
else { println!(" target: {}", ts.first().cloned().unwrap_or_default()); }
|
||||
}
|
||||
}
|
||||
"/timeout" | "/idle" => {
|
||||
if arg.is_empty() {
|
||||
if s.idle_secs == 0 { println!(" idle guardrail: off — set minutes with /timeout <n> (0 disables)"); }
|
||||
else { println!(" idle guardrail: stop if no new finding in {} min — /timeout <n> (0 disables)", s.idle_secs / 60); }
|
||||
} else {
|
||||
let mins: u64 = arg.trim().parse().unwrap_or(s.idle_secs / 60);
|
||||
s.idle_secs = mins.saturating_mul(60);
|
||||
if mins == 0 { println!(" idle guardrail: off"); }
|
||||
else { println!(" idle guardrail: stop if no new finding in {mins} min"); }
|
||||
}
|
||||
}
|
||||
"/repo" => {
|
||||
if arg.is_empty() { println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none) — set with /repo <path | github-url | owner/repo>, clear with /repo clear".into())); }
|
||||
@@ -472,11 +501,21 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
println!(" a run is already active — /status to check, /stop to halt it.");
|
||||
} else {
|
||||
save_session(&s);
|
||||
match start_background(base, &s, &mut reader, history.clone()).await {
|
||||
// Multiple comma-separated targets → run sequentially (queue the rest).
|
||||
let targets = session_targets(&s);
|
||||
let (first, rest): (Option<String>, Vec<String>) = if targets.len() > 1 {
|
||||
(Some(targets[0].clone()), targets[1..].to_vec())
|
||||
} else { (None, Vec::new()) };
|
||||
queue = rest;
|
||||
if !queue.is_empty() {
|
||||
println!(" \x1b[1;35m▶ multi-target\x1b[0m: {} URLs — running sequentially", targets.len());
|
||||
}
|
||||
match start_background(base, &s, &mut reader, history.clone(), first.as_deref()).await {
|
||||
Some(a) => { active = Some(a); println!(" \x1b[1;35m▶ running in background\x1b[0m — keep typing · \x1b[36m/status\x1b[0m · \x1b[36m/stop\x1b[0m"); }
|
||||
None => { // no external printer (piped) → blocking fallback
|
||||
let mut h = history.lock().unwrap();
|
||||
run(base, &s, &mut h).await; save_runs(base, &h);
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -543,6 +582,8 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
for x in &f { println!(" • [{}] {} \x1b[2m({} · {})\x1b[0m", x.severity, x.title, x.agent, x.endpoint); }
|
||||
if !f.is_empty() { println!(" \x1b[2m/finding — pick one to see the command & PoC\x1b[0m"); }
|
||||
}
|
||||
// No arg + interactive → the full navigation browser (target → vuln → detail, Esc back).
|
||||
_ if arg.is_empty() && std::io::stdin().is_terminal() => browse_results(&history.lock().unwrap()),
|
||||
_ => results(&history.lock().unwrap(), arg),
|
||||
}
|
||||
}
|
||||
@@ -739,13 +780,16 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
|
||||
/// external printer while the REPL keeps accepting commands (/status, /stop).
|
||||
/// Returns None when no external printer is available (piped) → caller blocks.
|
||||
async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
history: Arc<Mutex<Vec<RunRecord>>>) -> Option<ActiveRun> {
|
||||
let (target, mode_s, mode_e, mcp) = match (&s.repo, &s.target) {
|
||||
history: Arc<Mutex<Vec<RunRecord>>>, target_override: Option<&str>) -> Option<ActiveRun> {
|
||||
// `target_override` runs one specific URL (used by the multi-target queue).
|
||||
let ov = target_override.map(|t| t.to_string());
|
||||
let (target, mode_s, mode_e, mcp) = match (&s.repo, ov.as_ref().or(s.target.as_ref())) {
|
||||
(Some(_), Some(t)) => (t.clone(), "greybox", crate::Mode::Grey, s.mcp),
|
||||
(Some(r), None) => (r.clone(), "white-box", crate::Mode::White, false),
|
||||
(None, Some(t)) => (t.clone(), "black-box", crate::Mode::Black, s.mcp),
|
||||
_ => { println!(" \x1b[31m✗ set a /target <url> and/or /repo <path> first.\x1b[0m"); return None; }
|
||||
};
|
||||
let idle_secs = s.idle_secs;
|
||||
let mut cfg = RunConfig::new(&target);
|
||||
cfg.models = s.models.clone();
|
||||
cfg.subscription = s.subscription;
|
||||
@@ -775,28 +819,52 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
let fallback = sp.fallback.clone();
|
||||
let done = Arc::new(AtomicBool::new(false));
|
||||
let choice = Arc::new(Mutex::new(StopMode::Run));
|
||||
let soft_task = soft.clone(); // idle guardrail triggers a soft-stop (validate)
|
||||
let cancel_task = cancel.clone();
|
||||
let (live2, done2, hist2, choice2) = (live.clone(), done.clone(), history, choice.clone());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let crate::Spawned { task, mut rx, workdir, .. } = sp;
|
||||
let mut last_saved = 0usize;
|
||||
while let Some(line) = rx.recv().await {
|
||||
live2.lock().unwrap().ingest(&line);
|
||||
if let Some(out) = crate::render_compact(&line) { let _ = printer.print(out); }
|
||||
// Checkpoint live findings to disk whenever a new one lands, so the
|
||||
// run survives a quit/crash and is recovered on next launch.
|
||||
let snap = {
|
||||
let l = live2.lock().unwrap();
|
||||
if l.full.len() != last_saved {
|
||||
last_saved = l.full.len();
|
||||
Some(LiveCheckpoint {
|
||||
target: l.target.clone(), mode: l.mode.into(), phase: l.phase.clone(),
|
||||
workdir: workdir.display().to_string(),
|
||||
findings: l.full.clone(), commands: l.commands.clone(),
|
||||
})
|
||||
} else { None }
|
||||
};
|
||||
if let Some(c) = snap { save_checkpoint(&c); }
|
||||
let mut last_find = Instant::now(); // time of the last NEW finding
|
||||
let mut idle_fired = false;
|
||||
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(15));
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
loop {
|
||||
tokio::select! {
|
||||
maybe = rx.recv() => {
|
||||
let Some(line) = maybe else { break };
|
||||
live2.lock().unwrap().ingest(&line);
|
||||
if let Some(out) = crate::render_compact(&line) { let _ = printer.print(out); }
|
||||
// Checkpoint on each new finding; also resets the idle clock.
|
||||
let snap = {
|
||||
let l = live2.lock().unwrap();
|
||||
if l.full.len() != last_saved {
|
||||
last_saved = l.full.len();
|
||||
last_find = Instant::now();
|
||||
Some(LiveCheckpoint {
|
||||
target: l.target.clone(), mode: l.mode.into(), phase: l.phase.clone(),
|
||||
workdir: workdir.display().to_string(),
|
||||
findings: l.full.clone(), commands: l.commands.clone(),
|
||||
})
|
||||
} else { None }
|
||||
};
|
||||
if let Some(c) = snap { save_checkpoint(&c); }
|
||||
}
|
||||
_ = ticker.tick() => {
|
||||
// Idle guardrail: no NEW finding within the window → soft-stop
|
||||
// (stop launching exploit agents, validate what was found).
|
||||
if idle_secs > 0 && !idle_fired && last_find.elapsed().as_secs() >= idle_secs
|
||||
&& !soft_task.load(Ordering::Relaxed) && !cancel_task.load(Ordering::Relaxed) {
|
||||
idle_fired = true;
|
||||
*choice2.lock().unwrap() = StopMode::Validate;
|
||||
soft_task.store(true, Ordering::Relaxed);
|
||||
let _ = printer.print(format!(
|
||||
"\x1b[33m⏹ idle guardrail: no new finding in {} min — stopping & validating what was found\x1b[0m",
|
||||
idle_secs / 60));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let task_out = task.await.unwrap_or_default();
|
||||
let mode_choice = *choice2.lock().unwrap();
|
||||
@@ -942,7 +1010,24 @@ fn results(history: &[RunRecord], arg: &str) {
|
||||
}
|
||||
|
||||
fn open_report(history: &[RunRecord], arg: &str) {
|
||||
let Some(r) = pick(history, arg) else { return };
|
||||
if history.is_empty() { println!(" no runs yet — /run first."); return; }
|
||||
// No arg + multiple runs + interactive → let the user pick which report.
|
||||
let chosen: Option<&RunRecord> = if arg.trim().is_empty() && history.len() > 1 && std::io::stdin().is_terminal() {
|
||||
let items: Vec<String> = history.iter().map(|r| {
|
||||
let c = sev_counts(&r.findings);
|
||||
let sev = if c.is_empty() { "0 findings".into() } else { c.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(" ") };
|
||||
format!("#{} {:<9} {:<40} [{}]", r.id, r.mode, trunc(&r.target, 40), sev)
|
||||
}).collect();
|
||||
match dialoguer::Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Select a report to open (↑/↓, enter, Esc)")
|
||||
.items(&items).default(items.len() - 1).interact_opt() {
|
||||
Ok(Some(i)) => history.get(i),
|
||||
_ => return,
|
||||
}
|
||||
} else {
|
||||
pick(history, arg)
|
||||
};
|
||||
let Some(r) = chosen else { return };
|
||||
let dir = Path::new(&r.workdir);
|
||||
let pdf = dir.join("report.pdf");
|
||||
let file = if pdf.is_file() { pdf } else { dir.join("report.html") };
|
||||
@@ -1055,7 +1140,11 @@ fn finding_detail(pool: &[Finding]) {
|
||||
Ok(Some(i)) => i, _ => return,
|
||||
}
|
||||
} else { 0 };
|
||||
let x = &f[idx];
|
||||
print_finding_detail(&f[idx]);
|
||||
}
|
||||
|
||||
/// Full detail card for one finding.
|
||||
fn print_finding_detail(x: &Finding) {
|
||||
println!("\n ┌─ \x1b[1m{}\x1b[0m", x.title);
|
||||
println!(" │ severity : {}", x.severity);
|
||||
println!(" │ cwe / cvss : {} · {}", x.cwe, x.cvss);
|
||||
@@ -1073,6 +1162,49 @@ fn finding_detail(pool: &[Finding]) {
|
||||
println!(" └─────");
|
||||
}
|
||||
|
||||
/// Interactive results browser: pick a target/run → pick a vulnerability → see
|
||||
/// full detail. Esc steps back a level (vuln list → target list → exit to REPL).
|
||||
fn browse_results(history: &[RunRecord]) {
|
||||
if history.is_empty() { println!(" no runs yet — /run first."); return; }
|
||||
if !std::io::stdin().is_terminal() { results(history, ""); return; }
|
||||
loop {
|
||||
// Level 1 — pick a run/target.
|
||||
let run_items: Vec<String> = history.iter().map(|r| {
|
||||
let c = sev_counts(&r.findings);
|
||||
let sev = if c.is_empty() { "0".into() } else { c.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(" ") };
|
||||
format!("#{} {:<9} {:<40} [{}]", r.id, r.mode, trunc(&r.target, 40), sev)
|
||||
}).collect();
|
||||
let ri = match dialoguer::Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Results — select a target/run (Esc to return to the session)")
|
||||
.items(&run_items).default(run_items.len().saturating_sub(1)).interact_opt() {
|
||||
Ok(Some(i)) => i,
|
||||
_ => { println!(" ← back to session"); return; }
|
||||
};
|
||||
let r = &history[ri];
|
||||
if r.findings.is_empty() { println!(" run #{} — no validated findings.", r.id); continue; }
|
||||
let mut f = r.findings.clone();
|
||||
f.sort_by_key(|x| sev_rank(&x.severity));
|
||||
// Level 2 — pick a vulnerability (Esc → back to target list).
|
||||
loop {
|
||||
let items: Vec<String> = f.iter().map(|x| format!("[{}] {} — {}", x.severity, x.title, x.cwe)).collect();
|
||||
let fi = match dialoguer::Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(format!("#{} {} — select a vulnerability (Esc = back)", r.id, trunc(&r.target, 36)))
|
||||
.items(&items).default(0).interact_opt() {
|
||||
Ok(Some(i)) => i,
|
||||
_ => break, // Esc → back to target list
|
||||
};
|
||||
print_finding_detail(&f[fi]);
|
||||
// Enter → back to the vuln list; Esc → back to the target list.
|
||||
match dialoguer::Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("↵ back to vulnerabilities · Esc = back to targets")
|
||||
.items(&["back"]).default(0).interact_opt() {
|
||||
Ok(None) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_status(history: &[RunRecord], arg: &str) {
|
||||
let Some(r) = pick(history, arg) else { return };
|
||||
match std::fs::read_to_string(Path::new(&r.workdir).join("status.json")) {
|
||||
@@ -1097,7 +1229,9 @@ fn show(s: &Session) {
|
||||
println!(" │ auth : {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
|
||||
println!(" │ creds : {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
|
||||
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
|
||||
println!(" │ opts : mcp={} offline={} votes={} chain-depth={} max-agents={}", onoff(s.mcp), onoff(s.offline), s.vote_n, s.chain_depth, s.max_agents);
|
||||
println!(" │ opts : mcp={} offline={} votes={} chain-depth={} max-agents={} idle-stop={}",
|
||||
onoff(s.mcp), onoff(s.offline), s.vote_n, s.chain_depth, s.max_agents,
|
||||
if s.idle_secs == 0 { "off".to_string() } else { format!("{}m", s.idle_secs / 60) });
|
||||
// Integrations at a glance (see /integrations for detail).
|
||||
{
|
||||
let ig = harness::integrations::Integrations::load(&proj_dir());
|
||||
@@ -1126,7 +1260,7 @@ fn help() {
|
||||
println!("\n \x1b[1mNeuroSploit REPL — commands\x1b[0m");
|
||||
|
||||
println!("\n \x1b[2mTARGET & SCOPE\x1b[0m");
|
||||
h("/target <url>", "black-box target URL");
|
||||
h("/target <url[,..]>", "black-box target URL (comma-separated = multi-target, sequential)");
|
||||
h("/repo <path>", "analyse a repo (repo + target = greybox: code + live)");
|
||||
h("/auth <value>", "auth header, e.g. 'Authorization: Bearer <jwt>' (no arg = show)");
|
||||
h("/creds <file.yaml>", "credentials: jwt/header/cookie/login + ssh/windows");
|
||||
@@ -1154,6 +1288,7 @@ fn help() {
|
||||
println!("\n \x1b[2mOPTIONS\x1b[0m");
|
||||
h("/mcp on|off", "Playwright MCP browser /offline on|off self-test");
|
||||
h("/votes <n>", "validator votes /chain <n> attack-chain depth");
|
||||
h("/timeout <min>", "idle guardrail: stop if no new finding in <min> (0 = off)");
|
||||
h("/agents <n>|list", "cap agents · list counts /theme color|mono");
|
||||
h("/show (config)", "/clear /quit");
|
||||
|
||||
@@ -1239,6 +1374,12 @@ fn context_prompt(s: &Session) -> String {
|
||||
/// correctly; color is applied by the Highlighter, not embedded here.
|
||||
const PROMPT: &str = "neurosploit› ";
|
||||
|
||||
/// Split the session target into one or more URLs (comma-separated list).
|
||||
fn session_targets(s: &Session) -> Vec<String> {
|
||||
s.target.as_deref().map(|t| t.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn onoff(b: bool) -> &'static str { if b { "on" } else { "off" } }
|
||||
fn trunc(s: &str, n: usize) -> String {
|
||||
if s.chars().count() <= n { s.to_string() }
|
||||
|
||||
@@ -22,7 +22,13 @@ pub struct RunOutput {
|
||||
pub artifacts: Vec<String>,
|
||||
}
|
||||
|
||||
const RECON_SYS: &str = "You are a web recon specialist on an AUTHORIZED engagement. You have shell tools (curl etc.) — actively fetch the target, enumerate pages/params, and map the real attack surface. Do not ask for permission; proceed. Reply with a compact JSON object (tech, endpoints, params, auth, apis). No prose.";
|
||||
const RECON_SYS: &str = "You are an elite web recon specialist on an AUTHORIZED engagement. Actively fetch the target with your tools and map the REAL attack surface in DEPTH — do not ask for permission, proceed:\n\
|
||||
- Crawl pages, forms and parameters; record every input, header, cookie and redirect.\n\
|
||||
- DOWNLOAD the linked JavaScript bundles (curl each script) and ANALYZE them: extract API endpoints/routes, hidden/undocumented parameters, GraphQL operations, secrets / API keys / tokens, cloud & third-party URLs, feature flags, and `sourceMappingURL` references (fetch source maps if exposed to recover original source).\n\
|
||||
- Fingerprint the tech stack and EXACT versions (server, framework, libraries, CMS, JS libs) from headers, HTML, asset paths and JS.\n\
|
||||
- Analyze responses deeply: status codes, ALL headers, Set-Cookie flags, verbose errors/stack traces, content types, and length/timing differentials.\n\
|
||||
- Map auth (cookie/JWT/OAuth), APIs (REST & GraphQL), and any dev/staging/internal hosts referenced anywhere.\n\
|
||||
Base everything on real observed responses — never assume. Reply with a COMPACT JSON object with keys {tech, versions, endpoints, params, apis, auth, js_findings, secrets, hosts, notes}. No prose.";
|
||||
|
||||
/// Operator directives (focus instructions + auth material) prepended to
|
||||
/// recon/exploit prompts so the engagement is steered as the user asked.
|
||||
@@ -51,11 +57,18 @@ fn tool_doctrine(mcp_on: bool) -> String {
|
||||
};
|
||||
format!(
|
||||
"TOOLING (authorized; best on Kali Linux or the kalilinux/kali-rolling Docker image):\n\
|
||||
- HTTP: `curl` (headers, methods, params, cookies), `wget`.\n\
|
||||
- HTTP: `curl` (dump ALL response headers with -i/-D-, follow/inspect redirects, set methods/params/cookies), `wget`.\n\
|
||||
- Ports/services: `rustscan` if present, else `nmap`; if neither is installed you may \
|
||||
install via apt (`apt install -y nmap`), brew, or cargo (`cargo install rustscan`) — \
|
||||
otherwise probe common ports with `curl`/`nc`.\n\
|
||||
- Content/params: `ffuf`, `gobuster`, `gau`, `katana` when available.\n\
|
||||
- Content/params/URLs: `ffuf`, `gobuster`, `gau`, `katana`, `waybackurls`, `linkfinder` when available.\n\
|
||||
- JS ANALYSIS: download every linked script (`curl -s <script.js>`) and grep it for endpoints/paths, \
|
||||
`fetch(`/`axios`/XHR URLs, API & GraphQL routes, hidden params, and secrets (AKIA…, `api_key`, `token`, \
|
||||
`Bearer `, `authorization`), plus `sourceMappingURL` (fetch the .map to recover original source). \
|
||||
Prefer `linkfinder`/`gau`/`katana` to harvest more URLs when present, else regex with `grep -Eo`.\n\
|
||||
- REQUEST/RESPONSE ANALYSIS: read status codes, every header, Set-Cookie flags, content-type, body length \
|
||||
and response timing; use DIFFERENTIALS (authenticated vs anonymous, valid vs invalid input, existing vs \
|
||||
missing resource) and reflected input / verbose errors to infer behavior and CONFIRM issues with evidence.\n\
|
||||
- {browser}\n\
|
||||
Use only what is installed; degrade gracefully. Never run destructive or DoS actions.\n\n"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user