diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 1fc57a6..c5b1bbf 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -604,7 +604,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "3.4.1" +version = "3.5.0" dependencies = [ "anyhow", "clap", @@ -617,7 +617,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "3.4.1" +version = "3.5.0" dependencies = [ "anyhow", "futures", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index 7ccf9dc..82a1a77 100644 --- a/neurosploit-rs/Cargo.toml +++ b/neurosploit-rs/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/harness", "app"] resolver = "2" [workspace.package] -version = "3.4.1" +version = "3.5.0" edition = "2021" license = "MIT" repository = "https://github.com/JoasASantos/NeuroSploit" diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index 5efa2aa..ae12d3e 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1,4 +1,6 @@ -//! NeuroSploit v3.4.1 — CLI: `run` (black-box) / `whitebox` (source) / `agents` / `models`. +//! NeuroSploit v3.5.0 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). + +mod repl; use clap::{Parser, Subcommand}; use harness::{agents, models::ModelRef, pool::ModelPool, types::RunConfig, RunOutput}; @@ -8,8 +10,8 @@ use std::path::{Path, PathBuf}; #[command( name = "neurosploit", version, - about = "NeuroSploit v3.4.1 — multi-model autonomous pentest harness", - long_about = "NeuroSploit v3.4.1 — a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v3.5.0 — multi-model autonomous pentest harness", + long_about = "NeuroSploit v3.5.0 — a Rust multi-model harness that drives a pool of LLMs \ (API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \ After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \ them in parallel, then validates every finding by cross-model voting before reporting.\n\n\ @@ -107,9 +109,13 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let base = find_base(); + // No subcommand → launch the Claude-Code-style interactive session. let cmd = match cli.cmd { Some(c) => c, - None => interactive(&base).await?, // no args → wizard + None => { + repl::repl(&base).await?; + return Ok(()); + } }; match cmd { @@ -159,8 +165,8 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -/// Shared engagement runner for `run` / `whitebox`. -async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bool) -> anyhow::Result { +/// Shared engagement runner for `run` / `whitebox` / the interactive session. +pub(crate) async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bool) -> anyhow::Result { let lib = agents::load(base); // Unique, sortable run id → runs// @@ -171,7 +177,7 @@ async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bo cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string()); write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target)); - println!(" ┌─ NeuroSploit v3.4.1 · by Joas A Santos & Red Team Leaders"); + println!(" ┌─ NeuroSploit v3.5.0 · by Joas A Santos & Red Team Leaders"); println!(" │ run id : {run_id}"); println!(" │ target : {}", cfg.target); println!(" │ models : {}", cfg.models.join(", ")); @@ -235,7 +241,7 @@ async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bo Ok(out) } -fn print_findings(out: &RunOutput) { +pub(crate) fn print_findings(out: &RunOutput) { println!("\n=== {} validated finding(s) ===", out.findings.len()); println!("{}", serde_json::to_string_pretty(&out.findings).unwrap_or_default()); if !out.artifacts.is_empty() { @@ -262,46 +268,3 @@ fn write_status(workdir: &Path, state: &str, extra: &str) { if extra.is_empty() { String::new() } else { format!(",{extra}") })); } -fn prompt(q: &str, default: &str) -> String { - use std::io::Write; - print!(" {q}{}: ", if default.is_empty() { String::new() } else { format!(" [{default}]") }); - std::io::stdout().flush().ok(); - let mut s = String::new(); - std::io::stdin().read_line(&mut s).ok(); - let s = s.trim().to_string(); - if s.is_empty() { default.to_string() } else { s } -} - -/// Interactive wizard launched when `neurosploit` is run with no subcommand. -async fn interactive(base: &Path) -> anyhow::Result { - let lib = agents::load(base); - let backends = harness::installed_cli_backends(); - println!("\n ┌────────────────────────────────────────────┐"); - println!(" │ NeuroSploit v3.4.1 — interactive │"); - println!(" │ by Joas A Santos & Red Team Leaders │"); - println!(" └────────────────────────────────────────────┘"); - println!(" agents: {} · detected CLI logins: {}\n", - lib.total(), if backends.is_empty() { "none".into() } else { backends.join(", ") }); - - let mode = prompt("Mode — (b)lack-box URL or (w)hite-box repo?", "b").to_lowercase(); - let whitebox = mode.starts_with('w'); - let target = if whitebox { - prompt("Repository path", "/tmp/DVWA") - } else { - prompt("Target URL", "http://testphp.vulnweb.com/") - }; - let model = prompt("Model (provider:model)", "anthropic:claude-opus-4-8"); - let sub = prompt("Use subscription login (no API key)? (y/n)", "y").to_lowercase().starts_with('y'); - let mcp = if whitebox { false } else { - prompt("Use Playwright MCP browser if available? (y/n)", "y").to_lowercase().starts_with('y') - }; - let max_agents: usize = prompt("Max agents (0 = all matching)", "5").parse().unwrap_or(5); - let vote_n: usize = prompt("Validator votes (N)", "3").parse().unwrap_or(3); - - let models = vec![model]; - Ok(if whitebox { - Cmd::Whitebox { path: target, models, max_agents, vote_n, offline: false, subscription: sub, verbose: true } - } else { - Cmd::Run { url: target, models, max_agents, vote_n, offline: false, subscription: sub, mcp, verbose: true } - }) -} diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs new file mode 100644 index 0000000..aa28ac0 --- /dev/null +++ b/neurosploit-rs/app/src/repl.rs @@ -0,0 +1,218 @@ +//! NeuroSploit v3.5.0 — interactive session (Claude-Code / Codex / Cursor-CLI style). +//! +//! Launched when `neurosploit` runs with no subcommand. A persistent REPL where +//! you pick models, set an API key (or use a subscription login), point at a URL +//! or a repo, configure authentication, and write free-text instructions that +//! steer which agents run and how — e.g. "find injection and broken access +//! control". `/run` then executes the engagement with that configuration. + +use harness::{agents, types::RunConfig}; +use std::io::Write; +use std::path::Path; + +/// Mutable session state edited via slash-commands and consumed by `/run`. +struct Session { + models: Vec, + subscription: bool, + mcp: bool, + vote_n: usize, + max_agents: usize, + target: Option, + repo: Option, + auth: Option, + instructions: Option, +} + +impl Default for Session { + fn default() -> Self { + Session { + models: vec!["anthropic:claude-opus-4-8".into()], + subscription: harness::installed_cli_backends().contains(&"claude"), + mcp: false, + vote_n: 3, + max_agents: 0, + target: None, + repo: None, + auth: None, + instructions: None, + } + } +} + +const PROMPT: &str = "\x1b[35mneurosploit›\x1b[0m "; + +pub async fn repl(base: &Path) -> anyhow::Result<()> { + let lib = agents::load(base); + let backends = harness::installed_cli_backends(); + println!("\x1b[1m"); + println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗"); + println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.5.0"); + println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness"); + println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos"); + println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders"); + println!(" ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝\x1b[0m"); + println!(" {} agents loaded · detected logins: {}", lib.total(), + if backends.is_empty() { "none (use API keys)".into() } else { backends.join(", ") }); + println!(" Type \x1b[36m/help\x1b[0m to get started, \x1b[36m/run\x1b[0m to launch, \x1b[36m/quit\x1b[0m to exit.\n"); + + let mut s = Session::default(); + show(&s); + + let stdin = std::io::stdin(); + loop { + print!("{PROMPT}"); + std::io::stdout().flush().ok(); + let mut line = String::new(); + if stdin.read_line(&mut line).unwrap_or(0) == 0 { + println!(); + break; // EOF (Ctrl-D) + } + let line = line.trim(); + if line.is_empty() { + continue; + } + // A bare line that isn't a command is treated as test instructions. + if !line.starts_with('/') { + s.instructions = Some(line.to_string()); + println!(" focus set: {line}"); + continue; + } + let mut parts = line.splitn(2, char::is_whitespace); + let cmd = parts.next().unwrap_or(""); + let arg = parts.next().unwrap_or("").trim(); + match cmd { + "/help" | "/?" => help(), + "/show" | "/config" => show(&s), + "/providers" => { + for p in harness::providers() { + println!(" [{}] {:<14} {}", p.kind, p.key, + p.models.iter().map(|m| format!("{}:{}", p.key, m)).collect::>().join(" ")); + } + } + "/model" | "/models" => { + if arg.is_empty() { + println!(" current: {}", s.models.join(", ")); + } else { + s.models = arg.split([',', ' ']).filter(|x| !x.is_empty()).map(String::from).collect(); + println!(" models: {}", s.models.join(", ")); + } + } + "/key" => { + // /key → sets the provider's env var for this session + let mut kp = arg.splitn(2, char::is_whitespace); + match (kp.next(), kp.next()) { + (Some(prov), Some(key)) if !key.trim().is_empty() => { + match harness::provider_for(prov) { + Some(p) => { + std::env::set_var(p.env_key, key.trim()); + s.subscription = false; + println!(" set {} (API mode)", p.env_key); + } + None => println!(" unknown provider '{prov}' (see /providers)"), + } + } + _ => println!(" usage: /key e.g. /key anthropic sk-ant-..."), + } + } + "/sub" | "/subscription" => { + s.subscription = !matches!(arg, "off" | "false" | "0" | "no"); + println!(" subscription: {}", onoff(s.subscription)); + } + "/target" | "/url" => { + let t = if arg.starts_with("http") || arg.is_empty() { arg.to_string() } else { format!("https://{arg}") }; + s.target = if t.is_empty() { None } else { Some(t) }; + s.repo = None; + println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none)".into())); + } + "/repo" => { + s.repo = if arg.is_empty() { None } else { Some(arg.to_string()) }; + s.target = None; + println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none)".into())); + } + "/auth" => { + s.auth = if arg.is_empty() { None } else { Some(arg.to_string()) }; + println!(" auth: {}", s.auth.clone().unwrap_or_else(|| "(none)".into())); + } + "/focus" | "/instructions" => { + s.instructions = if arg.is_empty() { None } else { Some(arg.to_string()) }; + println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into())); + } + "/mcp" => { + s.mcp = !matches!(arg, "off" | "false" | "0" | "no"); + println!(" Playwright MCP: {}", onoff(s.mcp)); + } + "/votes" => { s.vote_n = arg.parse().unwrap_or(s.vote_n); println!(" votes: {}", s.vote_n); } + "/agents" => { s.max_agents = arg.parse().unwrap_or(s.max_agents); println!(" max agents: {} ", s.max_agents); } + "/clear" => { print!("\x1b[2J\x1b[H"); } + "/run" | "/go" => run(base, &s).await, + "/quit" | "/exit" | "/q" => { println!(" bye."); break; } + other => println!(" unknown command '{other}' — try /help"), + } + } + Ok(()) +} + +async fn run(base: &Path, s: &Session) { + let (target, whitebox) = match (&s.repo, &s.target) { + (Some(r), _) => (r.clone(), true), + (_, Some(t)) => (t.clone(), false), + _ => { + println!(" \x1b[31m✗ set a /target or /repo first.\x1b[0m"); + return; + } + }; + let mut cfg = RunConfig::new(&target); + cfg.models = s.models.clone(); + cfg.subscription = s.subscription; + cfg.vote_n = s.vote_n; + cfg.max_agents = s.max_agents; + cfg.verbose = true; + cfg.instructions = s.instructions.clone(); + cfg.auth = s.auth.clone(); + + match crate::run_engagement(base, cfg, s.mcp && !whitebox, whitebox).await { + Ok(out) => crate::print_findings(&out), + Err(e) => println!(" \x1b[31m✗ run failed: {e}\x1b[0m"), + } +} + +fn show(s: &Session) { + println!(" ┌─ session"); + println!(" │ models : {}", s.models.join(", ")); + println!(" │ auth mode: {}", if s.subscription { "subscription (CLI login)" } else { "API key" }); + println!(" │ target : {}", s.target.clone().unwrap_or_else(|| "(none)".into())); + println!(" │ repo : {}", s.repo.clone().unwrap_or_else(|| "(none)".into())); + println!(" │ auth : {}", s.auth.clone().unwrap_or_else(|| "(none)".into())); + println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into())); + println!(" │ mcp : {} votes: {} max-agents: {}", onoff(s.mcp), s.vote_n, s.max_agents); + println!(" └─ /run to launch"); +} + +fn help() { + println!(" Commands:"); + println!(" /model a:b[,c:d] set model panel (1st primary; rest fail over + vote)"); + println!(" /providers list providers & models"); + println!(" /key set a provider API key (switches to API mode)"); + println!(" /sub on|off use local subscription login instead of API key"); + println!(" /target black-box target URL"); + println!(" /repo white-box: analyse a local repository"); + println!(" /auth auth to send (e.g. 'Authorization: Bearer ' or 'Cookie: s=..')"); + println!(" /focus steer the tests, e.g. 'injection and broken access control'"); + println!(" (or just type the instruction with no slash)"); + println!(" /mcp on|off enable Playwright MCP browser (subscription path)"); + println!(" /votes validator votes per finding"); + println!(" /agents cap agents (0 = all matching)"); + println!(" /show show current session config"); + println!(" /run launch the engagement"); + println!(" /quit exit"); + println!(); + println!(" Example:"); + println!(" /model anthropic:claude-opus-4-8"); + println!(" /target http://testphp.vulnweb.com/"); + println!(" find injection and broken access control"); + println!(" /run"); +} + +fn onoff(b: bool) -> &'static str { + if b { "on" } else { "off" } +} diff --git a/neurosploit-rs/crates/harness/src/lib.rs b/neurosploit-rs/crates/harness/src/lib.rs index 3dc0609..d3c21f3 100644 --- a/neurosploit-rs/crates/harness/src/lib.rs +++ b/neurosploit-rs/crates/harness/src/lib.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.4.1 harness — a robust multi-model runtime for the +//! NeuroSploit v3.5.0 harness — a robust multi-model runtime for the //! markdown-driven autonomous pentest engine. //! //! The harness loads the `agents_md/` library, drives a *pool* of LLM models diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 7565646..9995ef7 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -22,6 +22,22 @@ pub struct RunOutput { 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."; +/// Operator directives (focus instructions + auth material) prepended to +/// recon/exploit prompts so the engagement is steered as the user asked. +fn operator_directives(cfg: &RunConfig) -> String { + let mut s = String::new(); + if let Some(focus) = cfg.instructions.as_deref().filter(|x| !x.trim().is_empty()) { + s.push_str(&format!("OPERATOR FOCUS — prioritise this: {focus}\n")); + } + if let Some(auth) = cfg.auth.as_deref().filter(|x| !x.trim().is_empty()) { + s.push_str(&format!("AUTHENTICATION — test as an authenticated user; send this with each request: {auth}\n")); + } + if !s.is_empty() { + s.push('\n'); + } + s +} + /// Tool-usage doctrine prepended to recon/exploit prompts so the agent knows /// exactly what it may use. Best run on Kali Linux (or the Kali Docker image), /// where these tools are preinstalled. @@ -68,7 +84,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender { let _ = tx.send(format!("recon complete via {}", m.label())).await; @@ -101,19 +117,20 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender = if !chosen.is_empty() { let sel: Vec = ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect(); if sel.is_empty() { - heuristic_select(&ranked, &recon, cap) + heuristic_select(&ranked, &recon, &focus, cap) } else { sel.into_iter().take(cap).collect() } } else { - // LLM selection failed/empty → recon-keyword heuristic, not a blind flat list. + // LLM selection failed/empty → recon+focus keyword heuristic, not a blind flat list. let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await; - heuristic_select(&ranked, &recon, cap) + heuristic_select(&ranked, &recon, &focus, cap) }; // Dedup: never run the same agent twice in one engagement. let selected: Vec = { @@ -129,12 +146,14 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender)> = stream::iter(selected.iter().cloned()) .map(|ag| { let target = target.clone(); let recon = recon_ctx.clone(); + let directives = directives.clone(); let txc = tx.clone(); async move { if verbose { @@ -143,10 +162,11 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender { let names = parse_string_array(&text); @@ -280,7 +305,7 @@ fn parse_string_array(text: &str) -> Vec { /// Fallback agent selection when the LLM selector fails: score each agent by /// keyword overlap between its name/title and the recon text, always seed a /// black-box baseline of high-yield web classes, and take the top `cap`. -fn heuristic_select(ranked: &[Agent], recon: &str, cap: usize) -> Vec { +fn heuristic_select(ranked: &[Agent], recon: &str, focus: &str, cap: usize) -> Vec { const BASELINE: &[&str] = &[ "sqli_error", "sqli_blind", "sqli_union", "xss_reflected", "xss_stored", "xss_dom", "command_injection", "lfi", "path_traversal", "ssrf", "idor", "open_redirect", @@ -288,6 +313,7 @@ fn heuristic_select(ranked: &[Agent], recon: &str, cap: usize) -> Vec { "security_headers", "cors_misconfig", ]; let r = recon.to_lowercase(); + let f = focus.to_lowercase(); // Recon signal → agent-name substrings. Only agents whose surface the recon // actually identified get the signal boost; the rest rely on the baseline. let signals: &[(&str, &[&str])] = &[ @@ -335,6 +361,18 @@ fn heuristic_select(ranked: &[Agent], recon: &str, cap: usize) -> Vec { score += 2; } } + // operator focus: strongly boost agents matching the requested classes + if !f.is_empty() { + let blob = format!("{} {}", a.name, a.title).to_lowercase(); + let hit = ["inject", "sqli", "xss", "ssrf", "ssti", "rce", "command", "lfi", "rfi", + "idor", "bola", "bfla", "access", "auth", "privilege", "csrf", "redirect", + "deserial", "xxe", "traversal", "upload", "jwt", "secret", "crypto"] + .iter() + .any(|kw| f.contains(kw) && blob.contains(kw)); + if hit { + score += 10; + } + } (score, a) }) .collect(); diff --git a/neurosploit-rs/crates/harness/src/report.rs b/neurosploit-rs/crates/harness/src/report.rs index 98e5056..9ac787f 100644 --- a/neurosploit-rs/crates/harness/src/report.rs +++ b/neurosploit-rs/crates/harness/src/report.rs @@ -79,9 +79,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String { h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\ .b{{color:#8b5cf6;font-weight:800}}\

NeuroSploit Penetration Test Report

\ -
Target: {t} · v3.4.1 Rust harness · multi-model validated
\ +
Target: {t} · v3.5.0 Rust harness · multi-model validated
\
{chips}

Findings ({n})

{body}\ -

Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.4.1 · by Joas A Santos & Red Team Leaders

", +

Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.5.0 · by Joas A Santos & Red Team Leaders

", t = esc(target), chips = chips, n = sorted.len(), body = body, ) } @@ -117,7 +117,7 @@ pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io:: let mut data = String::new(); data.push_str(&format!( "#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n", - tq(target), tq(&run_id), tq("NeuroSploit v3.4.1"), tq("multi-model") + tq(target), tq(&run_id), tq("NeuroSploit v3.5.0"), tq("multi-model") )); data.push_str("#let findings = (\n"); for f in sorted_findings(findings) { diff --git a/neurosploit-rs/crates/harness/src/types.rs b/neurosploit-rs/crates/harness/src/types.rs index d9cc0ad..3b09080 100644 --- a/neurosploit-rs/crates/harness/src/types.rs +++ b/neurosploit-rs/crates/harness/src/types.rs @@ -83,6 +83,14 @@ pub struct RunConfig { /// Verbose: log each agent as it launches, recon snippet, and votes. #[serde(default)] pub verbose: bool, + /// Free-text instructions from the operator that steer agent selection and + /// execution (e.g. "focus on injection and broken access control"). + #[serde(default)] + pub instructions: Option, + /// Authentication material to use against the target so agents test as an + /// authenticated user (e.g. "Authorization: Bearer " or "Cookie: session=..."). + #[serde(default)] + pub auth: Option, } fn default_vote() -> usize { @@ -105,6 +113,8 @@ impl RunConfig { workdir: None, rl_path: None, verbose: false, + instructions: None, + auth: None, } } } diff --git a/neurosploit-rs/templates/report.typ b/neurosploit-rs/templates/report.typ index b3c6bab..29aa75b 100644 --- a/neurosploit-rs/templates/report.typ +++ b/neurosploit-rs/templates/report.typ @@ -1,4 +1,4 @@ -// NeuroSploit v3.4.1 — Typst report template (blank, structured). +// NeuroSploit v3.5.0 — Typst report template (blank, structured). // // The harness generates `report.typ` per run by prepending a `findings` array // and a `meta` dict, then including this template's rendering logic. This file @@ -24,7 +24,7 @@ #set page(margin: 2cm, numbering: "1", footer: context [ #set text(size: 8pt, fill: gray) - NeuroSploit v3.4.1 · #meta.target · confidential + NeuroSploit v3.5.0 · #meta.target · confidential #h(1fr) #counter(page).display() ]) #set text(font: ("Helvetica Neue", "Helvetica", "Arial"), size: 10pt)