mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-02 17:45:46 +02:00
identification/attribution + multi-role access-control auth (v3.5.5)
Attribution (anti-plagiarism), multiple layers: - Identifying User-Agent on every request (default NeuroSploit/<ver> + an X-NeuroSploit-Scan header), overridable via /ua or NEUROSPLOIT_UA env; shown in the run banner. RunConfig.user_agent + Session.user_agent wired through. - Every finding is stamped "Identified and validated by NeuroSploit …" (in finish() and the raw-report path) so provenance travels in the finding text, findings.json and the report. Multi-role authentication for access-control testing (IDOR/BOLA/BFLA/privesc): - creds.yaml gains named identity blocks (admin:/user:/victim:/…), each with jwt | header | cookie | apikey | login+username+password. With >=2 roles the harness injects a cross-role access-control directive (authorized-vs-unauthorized proof) and defaults the primary auth to the first role. Also: /help now lists one command per line (fixes smushed OPTIONS/RUN columns); /ua command + Session field; docs (README + RELEASE) updated.
This commit is contained in:
@@ -243,6 +243,39 @@ auth: **AWS** access keys or profile; **GCP** a service-account JSON
|
||||
|
||||
---
|
||||
|
||||
## 👥 Multiple identities — access-control testing (IDOR / BOLA / BFLA)
|
||||
|
||||
Give NeuroSploit two or more **named roles** in `creds.yaml` and it authenticates
|
||||
as each and tests **cross-role** access (a low-priv role reaching another user's
|
||||
object or an admin function is a finding):
|
||||
|
||||
```yaml
|
||||
admin:
|
||||
jwt: eyJ... # per role: jwt | header (raw) | cookie | apikey | login+username+password
|
||||
user:
|
||||
apikey: abc123 # → X-Api-Key: abc123
|
||||
victim:
|
||||
cookie: "session=deadbeef"
|
||||
```
|
||||
|
||||
```bash
|
||||
neurosploit run https://app.example --creds creds.yaml \
|
||||
--subscription --model anthropic:claude-opus-4-8 -v
|
||||
```
|
||||
|
||||
Each finding is proven with the **authorized vs unauthorized** request pair, under
|
||||
the data-safety guardrail (read-only, PII masked).
|
||||
|
||||
## 🏷️ Identification & attribution (anti-plagiarism)
|
||||
|
||||
Every request is tagged with an identifying **User-Agent** (default
|
||||
`NeuroSploit/<ver> …`, change with **`/ua`** or `NEUROSPLOIT_UA`) plus an
|
||||
`X-NeuroSploit-Scan` header, and every finding is **stamped** "Identified and
|
||||
validated by NeuroSploit" — so provenance travels in the traffic, the finding
|
||||
text, `findings.json` and the report footer.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
|
||||
+29
@@ -92,6 +92,35 @@ interactive line-editing.
|
||||
- **Rate-limit testing** is a first-class control check (small non-disruptive
|
||||
burst → look for 429/lockout/Retry-After), never a DoS.
|
||||
|
||||
## Multi-role auth & access-control testing
|
||||
|
||||
- **Named identities in `creds.yaml`** for IDOR / BOLA / BFLA / privilege-escalation
|
||||
testing. Define two or more roles and the agent authenticates as each and tests
|
||||
**cross-role access** (control vs unauthorized request):
|
||||
```yaml
|
||||
admin:
|
||||
jwt: eyJ... # or header:/cookie:/apikey:/login+username+password
|
||||
user:
|
||||
apikey: abc123 # → X-Api-Key: abc123
|
||||
victim:
|
||||
cookie: "session=..."
|
||||
```
|
||||
Supported per role: `jwt`, `header` (raw), `cookie`, `apikey`, or a
|
||||
`login`/`username`/`password` self-login. With ≥2 roles the harness injects an
|
||||
access-control directive (capture one role's object IDs/functions, attempt them
|
||||
as another role, prove authorized-vs-denied) under the data-safety guardrail.
|
||||
|
||||
## Attribution & identification (anti-plagiarism)
|
||||
|
||||
- **Identifying User-Agent** on every request — default
|
||||
`NeuroSploit/<ver> (authorized security assessment; +github…)`, plus an
|
||||
`X-NeuroSploit-Scan` header. Change it with **`/ua <string>`** (REPL) or the
|
||||
`NEUROSPLOIT_UA` env var; the run banner shows it.
|
||||
- **Attribution stamped into every finding** ("Identified and validated by
|
||||
NeuroSploit — multi-model adversarial validation …") so provenance travels with
|
||||
the finding across the report, `findings.json` and any copy — in the traffic,
|
||||
the finding text, and the report footer, so the work can't be silently re-badged.
|
||||
|
||||
## Notes
|
||||
|
||||
- Additive/back-compatible. Provider count is 14 (Azure OpenAI added in v3.5.2).
|
||||
|
||||
@@ -467,6 +467,16 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
|
||||
if cfg.auth.is_none() {
|
||||
cfg.auth = c.auth_header();
|
||||
}
|
||||
// Multiple identities/roles → access-control testing (IDOR/BOLA/BFLA/privesc).
|
||||
if let Some(ri) = c.roles_instruction() {
|
||||
if cfg.auth.is_none() {
|
||||
cfg.auth = c.roles.iter().find_map(|r| r.header_line());
|
||||
}
|
||||
let base = cfg.instructions.clone().unwrap_or_default();
|
||||
cfg.instructions = Some(format!("{ri}\n{base}"));
|
||||
println!(" [*] {} identities loaded ({}) — access-control testing enabled",
|
||||
c.roles.len(), c.roles.iter().map(|r| r.name.clone()).collect::<Vec<_>>().join("/"));
|
||||
}
|
||||
// Host credentials (SSH / Windows-AD) → tell the agents how to authenticate
|
||||
// to the host so they can run on-host enumeration / privesc / AD checks.
|
||||
if let Some(hi) = c.host_instruction() {
|
||||
@@ -563,6 +573,13 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
|
||||
std::env::set_var("NEUROSPLOIT_PROXY", &p);
|
||||
println!(" │ proxy : {p} (traffic routed to Burp/ZAP for inspection)");
|
||||
}
|
||||
// Identifying User-Agent (attribution): cfg.user_agent overrides the default.
|
||||
let ua = cfg.user_agent.clone()
|
||||
.or_else(|| std::env::var("NEUROSPLOIT_UA").ok())
|
||||
.filter(|u| !u.trim().is_empty())
|
||||
.unwrap_or_else(harness::pipeline::default_user_agent);
|
||||
std::env::set_var("NEUROSPLOIT_UA", &ua);
|
||||
println!(" │ ua : {ua}");
|
||||
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
|
||||
|
||||
println!(" ┌─ NeuroSploit v3.5.5 · by Joas A Santos & Red Team Leaders");
|
||||
@@ -629,6 +646,7 @@ pub(crate) fn report_url(workdir: &Path) -> String {
|
||||
/// when the user chooses "report without validating" on /stop.
|
||||
pub(crate) fn report_raw(target: &str, findings: &[harness::types::Finding], workdir: &Path) {
|
||||
let mut fs = findings.to_vec();
|
||||
harness::pipeline::stamp_attribution(&mut fs); // provenance travels with raw reports too
|
||||
harness::attack_graph::enrich(&mut fs);
|
||||
std::fs::write(workdir.join("findings.json"), serde_json::to_string_pretty(&fs).unwrap_or_default()).ok();
|
||||
let _ = harness::report::typst_report(target, &fs, workdir);
|
||||
|
||||
@@ -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", "/timeout", "/proxy", "/burp", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
|
||||
"/votes", "/chain", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
|
||||
"/status", "/diff", "/retest", "/finding", "/expand", "/integrations", "/quit",
|
||||
];
|
||||
|
||||
@@ -219,6 +219,8 @@ struct Session {
|
||||
idle_secs: u64,
|
||||
/// Local intercepting proxy (Burp/ZAP), e.g. http://127.0.0.1:8080.
|
||||
proxy: Option<String>,
|
||||
/// Identifying User-Agent for NeuroSploit traffic (None = default UA).
|
||||
user_agent: Option<String>,
|
||||
offline: bool,
|
||||
target: Option<String>,
|
||||
repo: Option<String>,
|
||||
@@ -240,6 +242,7 @@ impl Default for Session {
|
||||
chain_depth: 2,
|
||||
idle_secs: 300, // 5-minute idle guardrail by default
|
||||
proxy: None,
|
||||
user_agent: None,
|
||||
offline: false,
|
||||
target: None,
|
||||
repo: None,
|
||||
@@ -441,6 +444,14 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
else { println!(" idle guardrail: stop if no new finding in {mins} min"); }
|
||||
}
|
||||
}
|
||||
"/ua" | "/useragent" => {
|
||||
match arg {
|
||||
"" => println!(" user-agent: {} \x1b[2m(identifies NeuroSploit traffic)\x1b[0m",
|
||||
s.user_agent.clone().unwrap_or_else(harness::pipeline::default_user_agent)),
|
||||
"default" | "reset" => { s.user_agent = None; println!(" user-agent reset to default (NeuroSploit)"); }
|
||||
u => { s.user_agent = Some(u.to_string()); println!(" user-agent: {u}"); }
|
||||
}
|
||||
}
|
||||
"/proxy" | "/burp" => {
|
||||
match arg {
|
||||
"" => println!(" proxy: {}", s.proxy.clone().unwrap_or_else(|| "(none) — route traffic to Burp/ZAP with /proxy <url>, e.g. /proxy http://127.0.0.1:8080".into())),
|
||||
@@ -755,6 +766,7 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
|
||||
cfg.vote_n = s.vote_n;
|
||||
cfg.chain_depth = s.chain_depth;
|
||||
cfg.proxy = s.proxy.clone();
|
||||
cfg.user_agent = s.user_agent.clone();
|
||||
cfg.max_agents = s.max_agents;
|
||||
cfg.verbose = true;
|
||||
cfg.offline = s.offline;
|
||||
@@ -809,6 +821,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
cfg.vote_n = s.vote_n;
|
||||
cfg.chain_depth = s.chain_depth;
|
||||
cfg.proxy = s.proxy.clone();
|
||||
cfg.user_agent = s.user_agent.clone();
|
||||
cfg.max_agents = s.max_agents;
|
||||
cfg.verbose = true;
|
||||
cfg.offline = s.offline;
|
||||
@@ -1243,6 +1256,7 @@ 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!(" │ proxy : {}", s.proxy.clone().unwrap_or_else(|| "(none — /proxy for Burp/ZAP)".into()));
|
||||
println!(" │ user-agent: {}", s.user_agent.clone().unwrap_or_else(|| "NeuroSploit (default)".into()));
|
||||
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
|
||||
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,
|
||||
@@ -1278,7 +1292,7 @@ fn help() {
|
||||
h("/target <url[,..]>", "black-box target URL (comma-separated = multi-target, sequential)");
|
||||
h("/repo <path|url>", "analyse a repo — path or GitHub URL (repo + target = greybox)");
|
||||
h("/auth <value>", "auth header, e.g. 'Authorization: Bearer <jwt>' (no arg = show)");
|
||||
h("/creds <file.yaml>", "credentials: jwt/header/cookie/login + ssh/windows + aws/gcp/azure");
|
||||
h("/creds <file.yaml>", "creds: jwt/header/cookie/login + ssh/windows + aws/gcp/azure + roles");
|
||||
h("/focus <text>", "steer the tests (or just type the instruction)");
|
||||
h("@path @dir @f:1-20", "attach a file/folder/line-range to context (Tab → menu)");
|
||||
h("/attach <path>", "attach a file/folder to context");
|
||||
@@ -1312,6 +1326,7 @@ fn help() {
|
||||
h("/chain <n>", "attack-chain depth (post-exploitation pivots; 0 = off)");
|
||||
h("/timeout <min>", "idle guardrail: stop if no new finding in <min> (0 = off)");
|
||||
h("/proxy <url>|off", "route agent HTTP through Burp/ZAP (/burp = default :8080)");
|
||||
h("/ua <string>", "identifying User-Agent for NeuroSploit traffic (default = NeuroSploit)");
|
||||
h("/agents <n>|list", "cap agents to run · `list` shows library counts");
|
||||
h("/theme color|mono", "toggle colored output");
|
||||
h("/show", "show the current session config");
|
||||
|
||||
@@ -80,6 +80,38 @@ impl Cloud {
|
||||
}
|
||||
}
|
||||
|
||||
/// A named identity/role for multi-user access-control testing (IDOR / BOLA /
|
||||
/// BFLA / privilege escalation). Each carries ONE way to authenticate.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Identity {
|
||||
pub name: String, // e.g. "admin", "user", "victim"
|
||||
pub jwt: String, // → Authorization: Bearer <jwt>
|
||||
pub header: String, // raw header, e.g. "X-Api-Key: abc"
|
||||
pub cookie: String, // → Cookie: <cookie>
|
||||
pub apikey: String, // → X-Api-Key: <apikey> (unless it contains ':')
|
||||
pub login_url: String, // login endpoint (agent authenticates itself)
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
/// The ready-to-send auth header for this identity, if it has direct material.
|
||||
pub fn header_line(&self) -> Option<String> {
|
||||
if !self.header.is_empty() { return Some(self.header.clone()); }
|
||||
if !self.jwt.is_empty() { return Some(format!("Authorization: Bearer {}", self.jwt)); }
|
||||
if !self.apikey.is_empty() {
|
||||
return Some(if self.apikey.contains(':') { self.apikey.clone() } else { format!("X-Api-Key: {}", self.apikey) });
|
||||
}
|
||||
if !self.cookie.is_empty() { return Some(format!("Cookie: {}", self.cookie)); }
|
||||
None
|
||||
}
|
||||
fn describe(&self) -> String {
|
||||
if let Some(h) = self.header_line() { format!("{} → send `{}`", self.name, h) }
|
||||
else if !self.login_url.is_empty() { format!("{} → log in at {} as {}:{} and reuse the session", self.name, self.login_url, self.username, self.password) }
|
||||
else { format!("{} → (no usable credential)", self.name) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Creds {
|
||||
pub jwt: Option<String>,
|
||||
@@ -89,6 +121,8 @@ pub struct Creds {
|
||||
pub ssh: Option<Ssh>,
|
||||
pub win: Option<Win>,
|
||||
pub cloud: Option<Cloud>,
|
||||
/// Named identities for multi-role access-control testing.
|
||||
pub roles: Vec<Identity>,
|
||||
}
|
||||
|
||||
impl Creds {
|
||||
@@ -100,7 +134,9 @@ impl Creds {
|
||||
let mut win = Win::default();
|
||||
let mut cloud = Cloud::default();
|
||||
let (mut have_login, mut have_ssh, mut have_win) = (false, false, false);
|
||||
let mut block = ""; // "", "login", "ssh", "windows", "aws", "gcp", "azure"
|
||||
let mut roles: Vec<Identity> = Vec::new();
|
||||
let mut cur_role = 0usize;
|
||||
let mut block = ""; // "", "login", "ssh", "windows", "aws", "gcp", "azure", "role"
|
||||
for raw in text.lines() {
|
||||
let line = raw.split('#').next().unwrap_or("");
|
||||
if line.trim().is_empty() {
|
||||
@@ -120,7 +156,9 @@ impl Creds {
|
||||
"aws" => "aws",
|
||||
"gcp" | "google" | "gcloud" => "gcp",
|
||||
"azure" | "az" => "azure",
|
||||
_ => "",
|
||||
"roles" | "identities" | "users" => "", // optional wrapper — ignore
|
||||
// Any other named block is a role/identity for access-control testing.
|
||||
other => { roles.push(Identity { name: other.to_string(), ..Default::default() }); cur_role = roles.len() - 1; "role" }
|
||||
};
|
||||
continue;
|
||||
}
|
||||
@@ -172,6 +210,18 @@ impl Creds {
|
||||
"subscription_id" | "subscription" => cloud.azure_subscription_id = v,
|
||||
_ => {}
|
||||
},
|
||||
"role" => if let Some(r) = roles.get_mut(cur_role) {
|
||||
match k.as_str() {
|
||||
"jwt" | "token" => r.jwt = v,
|
||||
"header" => r.header = v,
|
||||
"cookie" => r.cookie = v,
|
||||
"apikey" | "api_key" | "key" => r.apikey = v,
|
||||
"login" | "url" | "login_url" => r.login_url = v,
|
||||
"username" | "user" => r.username = v,
|
||||
"password" | "pass" => r.password = v,
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
@@ -188,13 +238,36 @@ impl Creds {
|
||||
if have_ssh && !ssh.host.is_empty() { c.ssh = Some(ssh); }
|
||||
if have_win && !win.host.is_empty() { c.win = Some(win); }
|
||||
if !cloud.is_empty() { c.cloud = Some(cloud); }
|
||||
roles.retain(|r| r.header_line().is_some() || !r.login_url.is_empty());
|
||||
c.roles = roles;
|
||||
if c.jwt.is_none() && c.header.is_none() && c.cookie.is_none()
|
||||
&& c.login.is_none() && c.ssh.is_none() && c.win.is_none() && c.cloud.is_none() {
|
||||
&& c.login.is_none() && c.ssh.is_none() && c.win.is_none() && c.cloud.is_none()
|
||||
&& c.roles.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// Multi-role access-control testing directive: lists every identity and
|
||||
/// instructs the agent to test cross-role access (IDOR/BOLA, BFLA, privesc)
|
||||
/// by acting as each role against the others' objects and functions.
|
||||
pub fn roles_instruction(&self) -> Option<String> {
|
||||
if self.roles.len() < 2 { return None; }
|
||||
let list = self.roles.iter().map(|r| format!(" - {}", r.describe())).collect::<Vec<_>>().join("\n");
|
||||
Some(format!(
|
||||
"MULTI-ROLE ACCESS CONTROL — you have {} identities:\n{list}\n\
|
||||
Authenticate as EACH identity (use its header on every request, or log in first for a login: role and \
|
||||
reuse the session). Then test broken access control across roles:\n\
|
||||
- BOLA/IDOR: as a low-privilege role, capture your own object IDs, then try to READ/UPDATE another \
|
||||
role's objects by their IDs; a low-priv role reaching a high-priv/other-user object is a finding.\n\
|
||||
- BFLA: call admin-only functions/endpoints/HTTP methods with a low-privilege role's session.\n\
|
||||
- Privilege escalation: mass-assignment of role/permission fields, or reaching admin routes.\n\
|
||||
Always compare against the control (the authorized role should succeed; the unauthorized role should be \
|
||||
denied). Prove each with the two requests (authorized vs unauthorized) and their responses. Respect data \
|
||||
safety — read-only proof, mask any PII.\n",
|
||||
self.roles.len()))
|
||||
}
|
||||
|
||||
/// Environment variables to export so the `aws`/`gcloud`/`az` CLIs the agents
|
||||
/// run pick up the cloud credentials automatically. For inline GCP JSON the
|
||||
/// content is written to a temp file and that path is returned.
|
||||
|
||||
@@ -85,13 +85,48 @@ fn tool_doctrine(mcp_on: bool) -> String {
|
||||
repo or download a tool (`git clone`, `wget`, `pip install`, `go install`, `cargo install`) — use pinned, \
|
||||
reputable sources; review before running; never run destructive payloads.\n\
|
||||
- {browser}\n\
|
||||
- {proxy}{pocs}\
|
||||
- {ua}{proxy}{pocs}\
|
||||
Use only what is installed; degrade gracefully. Never run destructive or DoS actions.\n\n",
|
||||
ua = ua_line(),
|
||||
proxy = proxy_line(),
|
||||
pocs = pocs_line(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Default identifying User-Agent so target owners (and the operator) can tell
|
||||
/// traffic came from NeuroSploit — and so authorship of a scan is unambiguous.
|
||||
pub fn default_user_agent() -> String {
|
||||
format!("NeuroSploit/{} (authorized security assessment; +https://github.com/JoasASantos/NeuroSploit)",
|
||||
env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
/// Identify NeuroSploit traffic at the request layer (User-Agent + a marker
|
||||
/// header). Overridable via `NEUROSPLOIT_UA`.
|
||||
fn ua_line() -> String {
|
||||
let ua = std::env::var("NEUROSPLOIT_UA").ok().filter(|v| !v.trim().is_empty())
|
||||
.unwrap_or_else(default_user_agent);
|
||||
format!(
|
||||
"IDENTIFY (attribution — do NOT strip): tag every HTTP request as NeuroSploit so the scan is \
|
||||
attributable — add `-A \"{ua}\"` (User-Agent) AND `-H \"X-NeuroSploit-Scan: {}\"` to curl. Only omit \
|
||||
when a specific test requires a different/absent User-Agent.\n ",
|
||||
env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
/// Attribution stamped into every finding's impact so the provenance travels
|
||||
/// with the finding across the report, findings.json and any copy — making it
|
||||
/// hard to silently re-badge NeuroSploit's output as someone else's work.
|
||||
const ATTRIBUTION: &str = "Identified and validated by NeuroSploit (multi-model adversarial validation) — https://github.com/JoasASantos/NeuroSploit · by Joas A Santos & Red Team Leaders.";
|
||||
|
||||
/// Append the NeuroSploit attribution to each finding's impact (idempotent).
|
||||
pub fn stamp_attribution(findings: &mut [Finding]) {
|
||||
for f in findings.iter_mut() {
|
||||
if !f.impact.contains("Identified and validated by NeuroSploit") {
|
||||
let sep = if f.impact.trim().is_empty() { "" } else { "\n\n" };
|
||||
f.impact = format!("{}{sep}{ATTRIBUTION}", f.impact.trim_end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If a local proxy is configured (Burp/ZAP), tell agents to route HTTP through
|
||||
/// it so the operator can inspect/replay traffic in Burp Suite.
|
||||
fn proxy_line() -> String {
|
||||
@@ -878,6 +913,8 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin
|
||||
}
|
||||
|
||||
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
|
||||
// Attribution: stamp provenance into each finding (report + json + copies).
|
||||
stamp_attribution(&mut findings);
|
||||
// Map findings to OWASP / MITRE / kill-chain stage for the attack graph.
|
||||
crate::attack_graph::enrich(&mut findings);
|
||||
|
||||
|
||||
@@ -133,6 +133,10 @@ pub struct RunConfig {
|
||||
/// traffic in Burp Suite.
|
||||
#[serde(default)]
|
||||
pub proxy: Option<String>,
|
||||
/// Custom User-Agent for identifying NeuroSploit traffic (attribution).
|
||||
/// Defaults to the NeuroSploit UA when unset.
|
||||
#[serde(default)]
|
||||
pub user_agent: Option<String>,
|
||||
}
|
||||
|
||||
fn default_vote() -> usize {
|
||||
@@ -165,6 +169,7 @@ impl RunConfig {
|
||||
pinned: Vec::new(),
|
||||
chain_depth: 2,
|
||||
proxy: None,
|
||||
user_agent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user