diff --git a/README.md b/README.md index b409f43..86873bd 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

๐Ÿง  NeuroSploit v3.5.2

+

๐Ÿง  NeuroSploit v3.5.3

Stars @@ -8,7 +8,7 @@

- + @@ -24,12 +24,12 @@ > > ๐Ÿ“– **New here? Read the [full Tutorial & User Guide โ†’](TUTORIAL.md)** โ€” every mode, flag, config and example explained. -> ๐Ÿ†• **New in v3.5.2 โ€” Exploitation Depth & Report Hygiene:** a **DEPTH doctrine** -> makes the engine *use* what it finds (exposed โ†’ exploited), **chain** findings -> across modules, decode/fingerprint artifacts โ†’ CVEs, and **audit tokens** (JWT -> alg-confusion / weak HS256 secrets). A deterministic post-pass **calibrates -> severity to proven impact** and **consolidates duplicated hygiene** findings. -> See [RELEASE.md](RELEASE.md). +> ๐Ÿ†• **New in v3.5.3 โ€” Integrations:** connect **GitHub / GitLab** (clone private +> repos, review a **Pull Request's** code, **watch** a branch and re-review on +> every commit) and **Jira** (open a vulnerability **card per finding**). Toggle +> them with **`/integrations`** in the REPL or `neurosploit integrations`. Full +> setup in **[TUTORIAL-INTEGRATION.md](TUTORIAL-INTEGRATION.md)**. +> *(v3.5.2 added the DEPTH doctrine + report-hygiene pass โ€” see [RELEASE.md](RELEASE.md).)* --- @@ -149,6 +149,41 @@ No login? Use an **API key** instead โ€” see [Authentication](#authentication--r --- +## ๐Ÿ”Œ Integrations (GitHub ยท GitLab ยท Jira) + +Wire NeuroSploit into your SDLC. Toggle from the REPL (`/integrations`) or the CLI +(`neurosploit integrations enable github|gitlab|jira`). **Tokens are never stored** +โ€” only the *name* of the env var is saved; the value is read from your environment. + +```bash +export GITHUB_TOKEN=ghp_... # PAT with `repo` scope (private repos) +neurosploit integrations enable github + +# Review a Pull Request's code (clones the PR head, white-box) and comment back: +neurosploit pr digininja/DVWA 42 --subscription --model anthropic:claude-opus-4-8 --comment + +# Watch a branch and re-review on every new commit: +neurosploit watch myorg/private-app --branch main --subscription --model anthropic:claude-opus-4-8 + +# Private GitLab repo (token-injected clone) โ€” works in whitebox/greybox: +export GITLAB_TOKEN=glpat-... ; neurosploit integrations enable gitlab +neurosploit whitebox https://gitlab.com/myorg/private-svc --subscription --model anthropic:claude-opus-4-8 + +# Open a Jira card per finding (any engagement): +export JIRA_EMAIL=you@org.com JIRA_API_TOKEN=... # set base/project once: /integrations setup jira +neurosploit whitebox https://github.com/myorg/app --jira --subscription --model anthropic:claude-opus-4-8 +``` + +| Integration | What you get | Env vars | +|-------------|--------------|----------| +| **GitHub** | private clone ยท `pr` review + comment ยท `watch` branch | `GITHUB_TOKEN` | +| **GitLab** | private clone for whitebox/greybox | `GITLAB_TOKEN` | +| **Jira** | one card per finding (`--jira`) | `JIRA_EMAIL`, `JIRA_API_TOKEN` | + +๐Ÿ“– Step-by-step setup for each tool: **[TUTORIAL-INTEGRATION.md](TUTORIAL-INTEGRATION.md)**. + +--- + ## Build ```bash diff --git a/RELEASE.md b/RELEASE.md index 4370c4f..e7f37df 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,3 +1,55 @@ +# NeuroSploit v3.5.3 โ€” Release Notes + +**Release Date:** June 2026 +**Codename:** Integrations (GitHub ยท GitLab ยท Jira) +**License:** MIT +**Credits:** Joas A Santos & Red Team Leaders + +--- + +## TL;DR + +v3.5.3 plugs NeuroSploit into your SDLC: review **private** GitHub/GitLab repos +and **Pull Requests**, **watch** a branch and re-review on every commit, and open +a **Jira card per finding** โ€” all toggleable via a new `/integrations` command. + +## Highlights + +- **GitHub integration** + - **Private repos**: when enabled, `whitebox` / `greybox --repo` / `tui --repo` + inject your `GITHUB_TOKEN` into the clone URL (token never printed/stored). + - **`neurosploit pr `** โ€” clones the **PR head** + (`refs/pull/N/head`), runs a white-box review, optionally **posts a summary + comment** back on the PR (`--comment`) and/or **opens Jira cards** (`--jira`). + - **`neurosploit watch --branch --interval `** โ€” polls the + branch and runs a white-box review **each time a new commit lands**. +- **GitLab integration** โ€” private clone (token-injected) for `whitebox`/`greybox` + against `gitlab.com` or a self-hosted base. +- **Jira integration** โ€” `--jira` on any engagement (or `pr`/`watch`) opens **one + card per finding** (summary, severity, CVSS, CWE, location, PoC, evidence, + remediation) in your project via the Jira REST API. +- **`/integrations` (REPL) + `neurosploit integrations` (CLI)** โ€” `show`, + `enable`/`disable `, and `setup ` + (interactive). Config persists to `/.neurosploit/integrations.json`. + **Secrets are never stored** โ€” only the env-var *name* is saved; values come + from the environment at use time. +- New harness module `integrations` + app commands `pr` / `watch` / + `integrations`, plus a `--jira` flag on `run` / `whitebox`. + +## Setup + +Step-by-step for tokens, scopes and configuration is in +**[TUTORIAL-INTEGRATION.md](TUTORIAL-INTEGRATION.md)** and summarized in the README. + +## Notes + +- Additive and back-compatible: all existing modes/flags are unchanged; if no + integration is enabled the behavior is identical to v3.5.2. +- Tokens use env vars: `GITHUB_TOKEN`, `GITLAB_TOKEN`, `JIRA_EMAIL` + + `JIRA_API_TOKEN` (names configurable per integration). + +--- + # NeuroSploit v3.5.2 โ€” Release Notes **Release Date:** June 2026 diff --git a/TUTORIAL-INTEGRATION.md b/TUTORIAL-INTEGRATION.md new file mode 100644 index 0000000..c31d372 --- /dev/null +++ b/TUTORIAL-INTEGRATION.md @@ -0,0 +1,210 @@ +# NeuroSploit โ€” Integrations Setup Guide (v3.5.3) + +Connect NeuroSploit to **GitHub**, **GitLab** and **Jira** so it can review private +repositories and Pull Requests, watch branches for new code, and file a Jira +**card per vulnerability**. + +> โš ๏ธ **Authorized testing only.** Use integrations against code/projects you own or +> are explicitly permitted to test. + +--- + +## Table of contents +1. [How it works (config & secrets)](#1-how-it-works) +2. [The `/integrations` command](#2-the-integrations-command) +3. [GitHub](#3-github) +4. [GitLab](#4-gitlab) +5. [Jira](#5-jira) +6. [Recipes](#6-recipes) +7. [Troubleshooting](#7-troubleshooting) + +--- + +## 1. How it works + +- Integration config is **per project**, stored at + `/.neurosploit/integrations.json`. +- **Secrets are never written to disk.** The config only stores the **name** of + the environment variable that holds each token (e.g. `GITHUB_TOKEN`). The real + value is read from your environment at use time. Keep tokens in your shell / + secret manager, not in the repo. +- Enable/disable per integration; each is independent. + +Default env-var names (configurable): + +| Integration | Token env var(s) | +|-------------|------------------| +| GitHub | `GITHUB_TOKEN` | +| GitLab | `GITLAB_TOKEN` | +| Jira | `JIRA_EMAIL` + `JIRA_API_TOKEN` | + +--- + +## 2. The `/integrations` command + +In the **REPL** (`neurosploit` with no args): + +``` +/integrations # show status of all three +/integrations enable github # toggle on (also: gitlab | jira) +/integrations disable jira # toggle off +/integrations setup jira # interactive: base URL, project key, issue type +/integrations setup gitlab # set the GitLab base (gitlab.com or self-hosted) +/integrations setup github # set the API base (change only for GitHub Enterprise) +``` + +From the **CLI**: + +```bash +neurosploit integrations # show status +neurosploit integrations enable github # enable / disable +``` + +`show` prints whether each is on and whether the token env var is currently set +(`โœ“ token` / `โš  token env not set`). + +--- + +## 3. GitHub + +**a. Create a token.** GitHub โ†’ *Settings โ†’ Developer settings โ†’ Personal access +tokens*. A classic PAT with the **`repo`** scope (read access to the private repos +you'll test) is enough. Fine-grained tokens also work (grant *Contents: Read* and, +for PR comments, *Pull requests: Read & write*). + +**b. Export it and enable:** +```bash +export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx +neurosploit integrations enable github +``` + +**c. What you can now do:** + +- **Clone & review a private repo** (token is injected into the clone URL, + never printed): + ```bash + neurosploit whitebox https://github.com/myorg/private-app \ + --subscription --model anthropic:claude-opus-4-8 -v + ``` +- **Review a Pull Request's code** โ€” clones the PR head (`refs/pull/N/head`): + ```bash + neurosploit pr myorg/private-app 128 \ + --subscription --model anthropic:claude-opus-4-8 --comment + ``` + - `--comment` posts a Markdown findings summary back on the PR. + - `--jira` also opens a card per finding (needs Jira configured). +- **Watch a branch** and re-review on every new commit: + ```bash + neurosploit watch myorg/private-app --branch main --interval 300 \ + --subscription --model anthropic:claude-opus-4-8 + ``` + It polls the branch tip via the GitHub API and runs a white-box review whenever + the SHA changes (Ctrl-C to stop). + +**GitHub Enterprise:** `/integrations setup github` and set the API base to your +GHE URL (e.g. `https://ghe.mycorp.com/api/v3`). + +--- + +## 4. GitLab + +**a. Create a token.** GitLab โ†’ *Preferences โ†’ Access Tokens* (or a project/group +token) with the **`read_repository`** scope (add `api` if you want more later). + +**b. Export it and enable:** +```bash +export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx +neurosploit integrations enable gitlab +# self-hosted? set the base: +# /integrations setup gitlab โ†’ https://gitlab.mycorp.com +``` + +**c. Review a private GitLab repo** (token-injected clone, works in whitebox & +greybox): +```bash +neurosploit whitebox https://gitlab.com/myorg/private-svc \ + --subscription --model anthropic:claude-opus-4-8 -v +``` + +> To review a specific Merge Request, check out its source branch and point +> `whitebox` at that clone, or pass the MR source branch URL. + +--- + +## 5. Jira + +**a. Create an API token.** https://id.atlassian.com/manage-profile/security/api-tokens +โ†’ *Create API token*. Note the email of the Atlassian account that owns it. + +**b. Export credentials:** +```bash +export JIRA_EMAIL=you@yourorg.com +export JIRA_API_TOKEN=xxxxxxxxxxxxxxxxxxxx +``` + +**c. Configure base URL + project (once):** +``` +# in the REPL: +/integrations setup jira + Jira base URL (https://your-org.atlassian.net): https://yourorg.atlassian.net + Jira project key (e.g. SEC): SEC + Issue type [Bug]: Bug +``` +This enables Jira and saves the base URL / project key / issue type to +`.neurosploit/integrations.json` (no secrets). + +**d. Open cards.** Add `--jira` to any engagement (or `pr` / `watch`). One card is +created per **validated** finding, with severity, CVSS, CWE, location, PoC, +evidence and remediation: +```bash +neurosploit whitebox https://github.com/myorg/app --jira \ + --subscription --model anthropic:claude-opus-4-8 -v +``` +The created issue keys are printed (e.g. `๐Ÿชช Jira cards opened: SEC-481, SEC-482`). + +> Uses the Jira REST API (`POST /rest/api/2/issue`) with Basic auth +> (`JIRA_EMAIL` : `JIRA_API_TOKEN`). The `issuetype` must exist in your project +> (use `Vulnerability` if your project defines it). + +--- + +## 6. Recipes + +**PR gate in CI** (block a PR if Critical/High findings appear): +```bash +export GITHUB_TOKEN=... # CI secret +neurosploit integrations enable github +neurosploit pr "$REPO" "$PR_NUMBER" --model anthropic:claude-opus-4-8 --comment --jira +``` + +**Nightly drift review** of a private app, filing Jira cards: +```bash +neurosploit integrations enable github +neurosploit integrations enable jira +neurosploit watch myorg/app --branch main --interval 3600 --jira \ + --model anthropic:claude-opus-4-8 +``` + +**Local private-repo audit** (no PR), cards to Jira: +```bash +neurosploit whitebox https://github.com/myorg/app --jira \ + --subscription --model anthropic:claude-opus-4-8 -v +``` + +--- + +## 7. Troubleshooting + +- **`โš  token env not set`** โ€” the integration is enabled but the env var isn't + exported in this shell. Export it (`export GITHUB_TOKEN=...`) and re-run. +- **`git clone failed` on a private repo** โ€” confirm the token scope (`repo` / + `read_repository`) and that the integration is enabled (`neurosploit + integrations`). The token is only injected when the matching integration is on. +- **`jira create failed: 400`** โ€” the `issuetype` name doesn't exist in the + project, or a required field is enforced. Try `Bug`, or set your project's type + via `/integrations setup jira`. +- **`jira ... not set`** โ€” export `JIRA_EMAIL` and `JIRA_API_TOKEN`. +- **GitHub comment fails (403/404)** โ€” the token needs *Pull requests: write* + (fine-grained) or `repo` (classic), and you must have access to the repo. +- **Tokens in CI** โ€” pass them as masked secrets; NeuroSploit never logs or + stores token values. diff --git a/TUTORIAL.md b/TUTORIAL.md index 1e9e758..e7ca7ee 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit โ€” Tutorial & User Guide (v3.5.2) +# NeuroSploit โ€” Tutorial & User Guide (v3.5.3) A complete, hands-on guide to installing, configuring and running NeuroSploit โ€” the autonomous, multi-model penetration-testing harness. @@ -98,7 +98,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 3.5.2 +neurosploit --version # neurosploit 3.5.3 neurosploit agents # {"vulns":196,...,"chains":12,"total":329} neurosploit models # all providers & models ``` diff --git a/install.ps1 b/install.ps1 index df20fb4..984b340 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ function Ok ($m) { Write-Host " + $m" -ForegroundColor Green } function Warn($m){ Write-Host " ! $m" -ForegroundColor Yellow } Write-Host "" -Write-Host " NeuroSploit installer (Windows) โ€” v3.5.2" -ForegroundColor Cyan +Write-Host " NeuroSploit installer (Windows) โ€” v3.5.3" -ForegroundColor Cyan $arch = $env:PROCESSOR_ARCHITECTURE Say "Platform: Windows / $arch" diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 540372c..52c74de 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "3.5.2" +version = "3.5.3" dependencies = [ "anyhow", "clap", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "3.5.2" +version = "3.5.3" dependencies = [ "anyhow", "futures", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index 26e6873..13bb0ae 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.5.2" +version = "3.5.3" 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 3ef96a6..1517844 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.5.2 โ€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). +//! NeuroSploit v3.5.3 โ€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). mod repl; mod tui; @@ -11,8 +11,8 @@ use std::path::{Path, PathBuf}; #[command( name = "neurosploit", version, - about = "NeuroSploit v3.5.2 โ€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v3.5.2 โ€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v3.5.3 โ€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v3.5.3 โ€” 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\ @@ -61,6 +61,9 @@ enum Cmd { /// Free-text focus, e.g. "injection and broken access control". #[arg(long)] focus: Option, + /// Open a Jira card per finding (needs the jira integration enabled). + #[arg(long)] + jira: bool, /// Verbose: log each agent as it launches, recon, and votes. #[arg(short, long)] verbose: bool, @@ -80,6 +83,9 @@ enum Cmd { offline: bool, #[arg(long)] subscription: bool, + /// Open a Jira card per finding (needs the jira integration enabled). + #[arg(long)] + jira: bool, #[arg(short, long)] verbose: bool, }, @@ -155,6 +161,52 @@ enum Cmd { #[arg(short, long)] verbose: bool, }, + /// Review a GitHub Pull Request's code (clones the PR head, white-box). + /// Optionally comments back on the PR and/or opens Jira cards per finding. + Pr { + /// `owner/repo` or a GitHub URL. + repo: String, + /// Pull request number. + number: u64, + #[arg(long = "model")] + models: Vec, + #[arg(long, default_value_t = 2)] + vote_n: usize, + #[arg(long)] + subscription: bool, + /// Post a summary comment back on the PR (needs github integration on). + #[arg(long)] + comment: bool, + /// Open a Jira card per finding (needs jira integration on). + #[arg(long)] + jira: bool, + #[arg(short, long)] + verbose: bool, + }, + /// Watch a GitHub repo branch; white-box review each time a new commit lands. + Watch { + /// `owner/repo` or a GitHub URL. + repo: String, + #[arg(long, default_value = "main")] + branch: String, + /// Poll interval in seconds. + #[arg(long, default_value_t = 300)] + interval: u64, + #[arg(long = "model")] + models: Vec, + #[arg(long)] + subscription: bool, + #[arg(long)] + jira: bool, + #[arg(short, long)] + verbose: bool, + }, + /// Manage integrations: `integrations [show|enable|disable] [github|gitlab|jira]`. + Integrations { + #[arg(default_value = "show")] + action: String, + name: Option, + }, /// Show agent library counts. Agents, /// List providers and models. @@ -215,7 +267,7 @@ async fn main() -> anyhow::Result<()> { } } } - Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, creds, focus, verbose } => { + Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, creds, focus, jira, verbose } => { let url = if url.starts_with("http") { url } else { format!("https://{url}") }; let mut cfg = RunConfig::new(&url); cfg.max_agents = max_agents; @@ -230,8 +282,10 @@ async fn main() -> anyhow::Result<()> { apply_creds(&mut cfg, creds.as_deref()).await; let out = run_engagement(&base, cfg, mcp, false).await?; print_findings(&out); + let ig = harness::integrations::Integrations::load(&repl::proj_dir()); + post_integrations(&ig, &url, &out, jira, false, None).await; } - Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription, verbose } => { + Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription, jira, verbose } => { let path = resolve_source(&base, &path)?; // local path OR github URL/owner/repo let mut cfg = RunConfig::new(&path); cfg.max_agents = max_agents; @@ -244,6 +298,8 @@ async fn main() -> anyhow::Result<()> { } let out = run_engagement(&base, cfg, false, true).await?; print_findings(&out); + let ig = harness::integrations::Integrations::load(&repl::proj_dir()); + post_integrations(&ig, &path, &out, jira, false, None).await; } Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, offline, subscription, mcp, verbose } => { let repo = resolve_source(&base, &repo)?; // local path OR github URL/owner/repo @@ -294,6 +350,76 @@ async fn main() -> anyhow::Result<()> { let out = run_mode(&base, cfg, false, Mode::Host).await?; print_findings(&out); } + Cmd::Pr { repo, number, models, vote_n, subscription, comment, jira, verbose } => { + let ig = harness::integrations::Integrations::load(&repl::proj_dir()); + let owner_repo = normalize_repo(&repo); + let path = clone_pr(&base, &ig, &owner_repo, number)?; + println!(" ๐Ÿ” white-box review of {owner_repo} PR #{number}"); + let mut cfg = RunConfig::new(&path); + cfg.vote_n = vote_n; + cfg.subscription = subscription; + cfg.verbose = verbose; + cfg.instructions = Some(format!("This is the code of pull request #{number} of {owner_repo}. Focus on vulnerabilities introduced or touched by this change.")); + if !models.is_empty() { cfg.models = models; } + let out = run_engagement(&base, cfg, false, true).await?; + print_findings(&out); + post_integrations(&ig, &format!("{owner_repo}#{number}"), &out, jira, comment, Some((&owner_repo, number))).await; + } + Cmd::Watch { repo, branch, interval, models, subscription, jira, verbose } => { + let ig = harness::integrations::Integrations::load(&repl::proj_dir()); + let owner_repo = normalize_repo(&repo); + println!(" ๐Ÿ‘€ watching {owner_repo}@{branch} every {interval}s โ€” Ctrl-C to stop"); + let mut last = String::new(); + loop { + match ig.github_latest_sha(&owner_repo, &branch).await { + Ok(sha) if sha != last => { + let short = &sha[..7.min(sha.len())]; + println!("\n ๐Ÿ”” {} commit {short} on {owner_repo}@{branch} โ€” reviewing", + if last.is_empty() { "current" } else { "new" }); + // fresh clone of the branch tip + let dest = base.join("repos").join(sanitize(&format!("{owner_repo}-{branch}"))); + std::fs::remove_dir_all(&dest).ok(); + let url = ig.authed_clone_url(&format!("https://github.com/{owner_repo}")); + if run_git(&["clone", "--depth", "1", "--branch", &branch, &url, &dest.display().to_string()]).is_ok() { + let mut cfg = RunConfig::new(&dest.display().to_string()); + cfg.subscription = subscription; + cfg.verbose = verbose; + if !models.is_empty() { cfg.models = models.clone(); } + if let Ok(out) = run_engagement(&base, cfg, false, true).await { + print_findings(&out); + post_integrations(&ig, &format!("{owner_repo}@{short}"), &out, jira, false, None).await; + } + } + last = sha; + } + Ok(_) => {} + Err(e) => eprintln!(" watch: {e}"), + } + tokio::time::sleep(std::time::Duration::from_secs(interval.max(15))).await; + } + } + Cmd::Integrations { action, name } => { + let dir = repl::proj_dir(); + let mut ig = harness::integrations::Integrations::load(&dir); + match action.as_str() { + "enable" | "disable" => { + let on = action == "enable"; + match name.as_deref() { + Some("github") => ig.github.enabled = on, + Some("gitlab") => ig.gitlab.enabled = on, + Some("jira") => ig.jira.enabled = on, + _ => { eprintln!(" usage: integrations {action} "); return Ok(()); } + } + ig.save(&dir)?; + println!(" {} {}", name.unwrap_or_default(), if on { "enabled โœ“" } else { "disabled" }); + } + _ => { + println!(" integrations ยท {}", dir.display()); + for l in ig.status_lines() { println!(" {l}"); } + println!(" toggle: `neurosploit integrations enable github|gitlab|jira` ยท full setup in the REPL: /integrations"); + } + } + } } Ok(()) } @@ -384,7 +510,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: 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.5.2 ยท by Joas A Santos & Red Team Leaders"); + println!(" โ”Œโ”€ NeuroSploit v3.5.3 ยท by Joas A Santos & Red Team Leaders"); println!(" โ”‚ run id : {run_id}"); println!(" โ”‚ target : {}", cfg.target); println!(" โ”‚ models : {}", cfg.models.join(", ")); @@ -564,9 +690,14 @@ pub(crate) fn resolve_source(base: &Path, arg: &str) -> anyhow::Result { println!(" [*] repo cache hit โ†’ {} (delete it to re-clone)", dest.display()); return Ok(dest.display().to_string()); } - println!(" [*] cloning {url} โ†’ {}", dest.display()); + // If a GitHub/GitLab integration is enabled, inject its token so PRIVATE + // repos clone without an interactive prompt (token never printed). + let ig = harness::integrations::Integrations::load(&repl::proj_dir()); + let clone_url = ig.authed_clone_url(&url); + let private = clone_url != url; + println!(" [*] cloning {url}{} โ†’ {}", if private { " (private, via token)" } else { "" }, dest.display()); let status = std::process::Command::new("git") - .args(["clone", "--depth", "1", &url, &dest.display().to_string()]) + .args(["clone", "--depth", "1", &clone_url, &dest.display().to_string()]) .status() .map_err(|e| anyhow::anyhow!("could not start `git clone` (is git installed?): {e}"))?; if !status.success() { @@ -576,6 +707,87 @@ pub(crate) fn resolve_source(base: &Path, arg: &str) -> anyhow::Result { Ok(dest.display().to_string()) } +/// Normalize a GitHub repo reference to `owner/name`. +fn normalize_repo(s: &str) -> String { + s.trim() + .trim_end_matches('/') + .trim_end_matches(".git") + .replace("https://github.com/", "") + .replace("http://github.com/", "") + .replace("git@github.com:", "") +} + +/// Run a git command, returning Ok(()) on success. +fn run_git(args: &[&str]) -> anyhow::Result<()> { + let status = std::process::Command::new("git").args(args).status() + .map_err(|e| anyhow::anyhow!("could not run git (is it installed?): {e}"))?; + if !status.success() { anyhow::bail!("git {:?} failed", args.first().unwrap_or(&"")); } + Ok(()) +} + +/// Clone a repo and check out a Pull Request's HEAD (`refs/pull/N/head`). +fn clone_pr(base: &Path, ig: &harness::integrations::Integrations, owner_repo: &str, number: u64) -> anyhow::Result { + let dest = base.join("repos").join(sanitize(&format!("{owner_repo}-pr{number}"))); + std::fs::create_dir_all(base.join("repos")).ok(); + std::fs::remove_dir_all(&dest).ok(); // always fresh โ€” PR code changes + let url = ig.authed_clone_url(&format!("https://github.com/{owner_repo}")); + let private = url.contains('@'); + println!(" [*] cloning {owner_repo}{} + PR #{number} head โ†’ {}", if private { " (private)" } else { "" }, dest.display()); + let d = dest.display().to_string(); + run_git(&["clone", "--depth", "1", &url, &d])?; + run_git(&["-C", &d, "fetch", "--depth", "1", "origin", &format!("pull/{number}/head:pr-{number}")])?; + run_git(&["-C", &d, "checkout", &format!("pr-{number}")])?; + Ok(d) +} + +/// After a run, optionally open Jira cards and/or comment on a GitHub PR. +async fn post_integrations( + ig: &harness::integrations::Integrations, + target: &str, + out: &RunOutput, + jira: bool, + comment: bool, + gh_pr: Option<(&str, u64)>, +) { + if jira && ig.jira.enabled && !out.findings.is_empty() { + let (keys, errs) = ig.jira_cards_for(target, &out.findings).await; + if !keys.is_empty() { println!(" ๐Ÿชช Jira cards opened: {}", keys.join(", ")); } + for e in errs { eprintln!(" jira: {e}"); } + } + if comment && ig.github.enabled { + if let Some((repo, number)) = gh_pr { + match ig.github_comment(repo, number, &pr_comment_body(out)).await { + Ok(()) => println!(" ๐Ÿ’ฌ commented results on {repo}#{number}"), + Err(e) => eprintln!(" github comment: {e}"), + } + } + } +} + +/// Markdown summary of a run, for a PR comment. +fn pr_comment_body(out: &RunOutput) -> String { + let mut by = std::collections::BTreeMap::new(); + for f in &out.findings { *by.entry(f.severity.as_str()).or_insert(0) += 1; } + let chips: Vec = by.iter().map(|(k, v)| format!("{k}: {v}")).collect(); + let mut s = format!( + "### ๐Ÿง  NeuroSploit white-box review\n\n**{} validated finding(s)** โ€” {}\n\n", + out.findings.len(), + if chips.is_empty() { "none".into() } else { chips.join(" ยท ") } + ); + if out.findings.is_empty() { + s.push_str("_No vulnerabilities confirmed in the reviewed code._\n"); + } else { + s.push_str("| Severity | Finding | CWE | Location |\n|---|---|---|---|\n"); + for f in &out.findings { + s.push_str(&format!("| {} | {} | {} | {} |\n", + f.severity, f.title.replace('|', "\\|"), f.cwe, + f.endpoint.replace('|', "\\|"))); + } + s.push_str("\n_Findings validated by multi-model voting. Authorized testing only._\n"); + } + s +} + /// Blocking yes/no prompt (default yes). Used after a graceful Ctrl-C. fn ask_yes_no(q: &str) -> bool { use std::io::Write; diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index b50d152..c5662a1 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.5.2 โ€” interactive session (Claude-Code / Codex / Cursor-CLI style). +//! NeuroSploit v3.5.3 โ€” interactive session (Claude-Code / Codex / Cursor-CLI style). //! //! Launched when `neurosploit` runs with no subcommand. A persistent REPL with //! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model @@ -120,7 +120,7 @@ const COMMANDS: &[&str] = &[ "/help", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target", "/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline", "/votes", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report", - "/status", "/diff", "/retest", "/quit", + "/status", "/diff", "/retest", "/integrations", "/quit", ]; /// rustyline helper: Tab-completes `/commands` and `@filesystem-paths`, @@ -299,7 +299,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { let backends = harness::installed_cli_backends(); println!("\x1b[1m"); println!(" โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—"); - println!(" โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•— NeuroSploit v3.5.2"); + println!(" โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•— NeuroSploit v3.5.3"); println!(" โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ interactive harness"); println!(" โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ by Joas A Santos"); println!(" โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• & Red Team Leaders"); @@ -430,6 +430,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { } "/mcp" => { s.mcp = !matches!(arg, "off" | "false" | "0" | "no"); println!(" Playwright MCP: {}", onoff(s.mcp)); } "/offline" => { s.offline = !matches!(arg, "off" | "false" | "0" | "no"); println!(" offline: {}", onoff(s.offline)); } + "/integrations" | "/integration" => integrations_cmd(arg), "/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"); } @@ -939,6 +940,64 @@ fn sev_rank(s: &str) -> u8 { } /// Read one line synchronously (for the /stop choice prompt). +/// `/integrations` โ€” show / enable / disable / setup GitHub, GitLab, Jira. +fn integrations_cmd(arg: &str) { + let dir = proj_dir(); + let mut ig = harness::integrations::Integrations::load(&dir); + let mut parts = arg.splitn(2, char::is_whitespace); + let sub = parts.next().unwrap_or("").trim(); + let name = parts.next().unwrap_or("").trim(); + match sub { + "" | "show" | "status" => { + println!(" \x1b[1mintegrations\x1b[0m ยท {}", dir.display()); + for l in ig.status_lines() { println!(" {l}"); } + println!(" \x1b[2m/integrations enable|disable ยท /integrations setup \x1b[0m"); + println!(" \x1b[2mtokens come from env vars (never stored): GITHUB_TOKEN ยท GITLAB_TOKEN ยท JIRA_EMAIL + JIRA_API_TOKEN\x1b[0m"); + } + "enable" | "disable" => { + let on = sub == "enable"; + match name { + "github" => ig.github.enabled = on, + "gitlab" => ig.gitlab.enabled = on, + "jira" => ig.jira.enabled = on, + _ => { println!(" usage: /integrations {sub} "); return; } + } + let _ = ig.save(&dir); + println!(" {name} {}", if on { "enabled โœ“" } else { "disabled" }); + } + "setup" => match name { + "jira" => { + let base = ask_line(" Jira base URL (https://your-org.atlassian.net):"); + if !base.trim().is_empty() { ig.jira.base_url = base.trim().trim_end_matches('/').to_string(); } + let proj = ask_line(" Jira project key (e.g. SEC):"); + if !proj.trim().is_empty() { ig.jira.project_key = proj.trim().to_string(); } + let it = ask_line(" Issue type [Bug]:"); + if !it.trim().is_empty() { ig.jira.issue_type = it.trim().to_string(); } + ig.jira.enabled = true; + let _ = ig.save(&dir); + println!(" โœ“ jira configured (project {}, {}). Now export {} and {} in your shell.", + ig.jira.project_key, ig.jira.base_url, ig.jira.email_env, ig.jira.token_env); + } + "gitlab" => { + let b = ask_line(" GitLab base [https://gitlab.com]:"); + if !b.trim().is_empty() { ig.gitlab.base = b.trim().trim_end_matches('/').to_string(); } + ig.gitlab.enabled = true; + let _ = ig.save(&dir); + println!(" โœ“ gitlab enabled (base {}). Export {} (PAT with read_repository).", ig.gitlab.base, ig.gitlab.token_env); + } + "github" => { + let a = ask_line(" GitHub API base [https://api.github.com] (change for GHE):"); + if !a.trim().is_empty() { ig.github.api = a.trim().trim_end_matches('/').to_string(); } + ig.github.enabled = true; + let _ = ig.save(&dir); + println!(" โœ“ github enabled (api {}). Export {} (PAT with repo scope).", ig.github.api, ig.github.token_env); + } + _ => println!(" usage: /integrations setup "), + }, + _ => println!(" usage: /integrations [show | enable | disable | setup ]"), + } +} + fn ask_line(prompt: &str) -> String { use std::io::Write; print!("{prompt} "); @@ -1047,6 +1106,9 @@ fn help() { h("/runs", "list runs ยท /results [n] ยท /report [n]"); h("/diff /retest [n]", "what changed vs last run ยท re-verify a past run"); + println!("\n \x1b[2mINTEGRATIONS\x1b[0m"); + h("/integrations", "show ยท enable/disable github|gitlab|jira ยท setup "); + println!("\n \x1b[2mOPTIONS\x1b[0m"); h("/mcp on|off", "Playwright MCP browser /offline on|off self-test"); h("/votes ", "validator votes /agents cap agents"); diff --git a/neurosploit-rs/app/src/tui.rs b/neurosploit-rs/app/src/tui.rs index e297d58..19828df 100644 --- a/neurosploit-rs/app/src/tui.rs +++ b/neurosploit-rs/app/src/tui.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.5.2 โ€” TUI "Mission Control" mode. +//! NeuroSploit v3.5.3 โ€” TUI "Mission Control" mode. //! //! Concurrent panels that update live while the engagement runs in the //! background, with a composer input that stays active during execution: diff --git a/neurosploit-rs/crates/harness/src/belief.rs b/neurosploit-rs/crates/harness/src/belief.rs index af12d31..4ed9b1f 100644 --- a/neurosploit-rs/crates/harness/src/belief.rs +++ b/neurosploit-rs/crates/harness/src/belief.rs @@ -1,4 +1,4 @@ -//! POMDP belief-state world model (v3.5.2). +//! POMDP belief-state world model (v3.5.3). //! //! The target is only partially observable, so we don't track booleans โ€” we //! track a **belief**: a property graph whose nodes (host / service / vuln / diff --git a/neurosploit-rs/crates/harness/src/grounding.rs b/neurosploit-rs/crates/harness/src/grounding.rs index c607f55..4a3bd32 100644 --- a/neurosploit-rs/crates/harness/src/grounding.rs +++ b/neurosploit-rs/crates/harness/src/grounding.rs @@ -1,4 +1,4 @@ -//! Verification / grounding engine (v3.5.2). +//! Verification / grounding engine (v3.5.3). //! //! Hard rule: **no claim enters the world model without a tool receipt** โ€” raw //! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination diff --git a/neurosploit-rs/crates/harness/src/integrations.rs b/neurosploit-rs/crates/harness/src/integrations.rs new file mode 100644 index 0000000..6a01713 --- /dev/null +++ b/neurosploit-rs/crates/harness/src/integrations.rs @@ -0,0 +1,199 @@ +//! External integrations (v3.5.3): GitHub / GitLab (private repos, PR/MR code +//! review, commit watching) and Jira (open one vulnerability card per finding). +//! +//! Config persists to `/.neurosploit/integrations.json`. **Secrets are +//! never stored** โ€” only the *name* of the env var holding each token is saved; +//! the value is read from the environment at use time. +use crate::types::Finding; +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +#[derive(Serialize, Deserialize, Clone)] +pub struct GithubCfg { + pub enabled: bool, + pub token_env: String, // e.g. GITHUB_TOKEN (a PAT with `repo` scope for private repos) + pub api: String, // https://api.github.com (or GHE base) +} +impl Default for GithubCfg { + fn default() -> Self { Self { enabled: false, token_env: "GITHUB_TOKEN".into(), api: "https://api.github.com".into() } } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct GitlabCfg { + pub enabled: bool, + pub token_env: String, // GITLAB_TOKEN + pub base: String, // https://gitlab.com (or self-hosted) +} +impl Default for GitlabCfg { + fn default() -> Self { Self { enabled: false, token_env: "GITLAB_TOKEN".into(), base: "https://gitlab.com".into() } } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct JiraCfg { + pub enabled: bool, + pub base_url: String, // https://your-org.atlassian.net + pub email_env: String, // JIRA_EMAIL + pub token_env: String, // JIRA_API_TOKEN + pub project_key: String, + pub issue_type: String, // Bug / Vulnerability / Task +} +impl Default for JiraCfg { + fn default() -> Self { + Self { enabled: false, base_url: String::new(), email_env: "JIRA_EMAIL".into(), + token_env: "JIRA_API_TOKEN".into(), project_key: String::new(), issue_type: "Bug".into() } + } +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct Integrations { + pub github: GithubCfg, + pub gitlab: GitlabCfg, + pub jira: JiraCfg, +} + +fn env(name: &str) -> Option { + std::env::var(name).ok().filter(|v| !v.trim().is_empty()) +} + +fn client() -> reqwest::Client { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap_or_default() +} + +impl Integrations { + pub fn path(dir: &Path) -> std::path::PathBuf { dir.join("integrations.json") } + + pub fn load(dir: &Path) -> Self { + std::fs::read_to_string(Self::path(dir)) + .ok() + .and_then(|t| serde_json::from_str(&t).ok()) + .unwrap_or_default() + } + + pub fn save(&self, dir: &Path) -> Result<()> { + std::fs::create_dir_all(dir).ok(); + std::fs::write(Self::path(dir), serde_json::to_string_pretty(self)?)?; + Ok(()) + } + + pub fn github_token(&self) -> Option { env(&self.github.token_env) } + pub fn gitlab_token(&self) -> Option { env(&self.gitlab.token_env) } + + /// Inject a token into an https git URL so private repos can be cloned. + /// No-op if the matching integration is off, the token env is unset, or the + /// URL doesn't match the configured host. + pub fn authed_clone_url(&self, url: &str) -> String { + if self.github.enabled { + if let Some(rest) = url.strip_prefix("https://github.com/") { + if let Some(tok) = self.github_token() { + return format!("https://x-access-token:{tok}@github.com/{rest}"); + } + } + } + if self.gitlab.enabled { + let host = self.gitlab.base.trim_start_matches("https://").trim_start_matches("http://").trim_end_matches('/'); + let prefix = format!("https://{host}/"); + if let Some(rest) = url.strip_prefix(&prefix) { + if let Some(tok) = self.gitlab_token() { + return format!("https://oauth2:{tok}@{host}/{rest}"); + } + } + } + url.to_string() + } + + /// Post a comment on a GitHub PR/issue (`repo` = `owner/name`). + pub async fn github_comment(&self, repo: &str, number: u64, body: &str) -> Result<()> { + let tok = self.github_token().ok_or_else(|| anyhow!("{} not set", self.github.token_env))?; + let url = format!("{}/repos/{}/issues/{}/comments", self.github.api.trim_end_matches('/'), repo, number); + let resp = client().post(&url) + .header("User-Agent", "NeuroSploit") + .header("Accept", "application/vnd.github+json") + .bearer_auth(tok) + .json(&serde_json::json!({ "body": body })) + .send().await?; + if !resp.status().is_success() { + return Err(anyhow!("github comment failed: {} {}", resp.status(), resp.text().await.unwrap_or_default())); + } + Ok(()) + } + + /// Latest commit SHA of a branch via the GitHub API (for `watch`). + pub async fn github_latest_sha(&self, repo: &str, branch: &str) -> Result { + let url = format!("{}/repos/{}/commits/{}", self.github.api.trim_end_matches('/'), repo, branch); + let mut req = client().get(&url) + .header("User-Agent", "NeuroSploit") + .header("Accept", "application/vnd.github+json"); + if let Some(t) = self.github_token() { req = req.bearer_auth(t); } + let resp = req.send().await?; + if !resp.status().is_success() { + return Err(anyhow!("github commits API {}: {}", resp.status(), resp.text().await.unwrap_or_default())); + } + let v: serde_json::Value = resp.json().await?; + v["sha"].as_str().map(|s| s.to_string()).ok_or_else(|| anyhow!("no sha in response")) + } + + /// Create one Jira issue. Returns the issue key (e.g. SEC-123). + pub async fn jira_card(&self, summary: &str, description: &str) -> Result { + let email = env(&self.jira.email_env).ok_or_else(|| anyhow!("{} not set", self.jira.email_env))?; + let token = env(&self.jira.token_env).ok_or_else(|| anyhow!("{} not set", self.jira.token_env))?; + if self.jira.base_url.is_empty() || self.jira.project_key.is_empty() { + return Err(anyhow!("jira base_url/project_key not configured (run /integrations setup jira)")); + } + let url = format!("{}/rest/api/2/issue", self.jira.base_url.trim_end_matches('/')); + let payload = serde_json::json!({ + "fields": { + "project": { "key": self.jira.project_key }, + "summary": summary, + "description": description, + "issuetype": { "name": self.jira.issue_type }, + } + }); + let resp = client().post(&url) + .basic_auth(email, Some(token)) + .header("Accept", "application/json") + .json(&payload) + .send().await?; + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + return Err(anyhow!("jira create failed: {} {}", status, text)); + } + let v: serde_json::Value = serde_json::from_str(&text)?; + Ok(v["key"].as_str().unwrap_or("?").to_string()) + } + + /// Open one Jira card per finding. Returns (created keys, errors). + pub async fn jira_cards_for(&self, target: &str, findings: &[Finding]) -> (Vec, Vec) { + let (mut keys, mut errs) = (Vec::new(), Vec::new()); + for f in findings { + let summary = format!("[{}] {} โ€” {}", f.severity, f.title, target); + let description = format!( + "*Target:* {target}\n*Severity:* {} | *CVSS:* {} | *CWE:* {}\n*Location:* {}\n\n*Impact:*\n{}\n\n*PoC / payload:*\n{{code}}{}{{code}}\n\n*Evidence:*\n{{code}}{}{{code}}\n\n*Remediation:*\n{}\n\n_Filed automatically by NeuroSploit._", + f.severity, f.cvss, f.cwe, f.endpoint, f.impact, f.payload, f.evidence, f.remediation + ); + match self.jira_card(&summary, &description).await { + Ok(k) => keys.push(k), + Err(e) => errs.push(format!("{}: {e}", f.title)), + } + } + (keys, errs) + } + + /// Human-readable status (for `/integrations` and the CLI). + pub fn status_lines(&self) -> Vec { + let badge = |on: bool, tok: bool| if !on { "off".to_string() } + else if tok { "on โœ“ token".to_string() } else { "on โš  token env not set".to_string() }; + vec![ + format!("github : {:<18} (clone private repos ยท PR review ยท watch) env={}", badge(self.github.enabled, self.github_token().is_some()), self.github.token_env), + format!("gitlab : {:<18} (clone private repos ยท MR review) env={}", badge(self.gitlab.enabled, self.gitlab_token().is_some()), self.gitlab.token_env), + format!("jira : {:<18} (open a card per finding) project={} base={}", + badge(self.jira.enabled, env(&self.jira.token_env).is_some()), + if self.jira.project_key.is_empty() { "-" } else { &self.jira.project_key }, + if self.jira.base_url.is_empty() { "-" } else { &self.jira.base_url }), + ] + } +} diff --git a/neurosploit-rs/crates/harness/src/lib.rs b/neurosploit-rs/crates/harness/src/lib.rs index 9debcba..01a3a7c 100644 --- a/neurosploit-rs/crates/harness/src/lib.rs +++ b/neurosploit-rs/crates/harness/src/lib.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.5.2 harness โ€” a robust multi-model runtime for the +//! NeuroSploit v3.5.3 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 @@ -12,6 +12,7 @@ pub mod belief; pub mod creds; pub mod grounding; pub mod hygiene; +pub mod integrations; pub mod pomdp; pub mod models; pub mod pipeline; diff --git a/neurosploit-rs/crates/harness/src/pomdp.rs b/neurosploit-rs/crates/harness/src/pomdp.rs index 6e77d3b..b94a78f 100644 --- a/neurosploit-rs/crates/harness/src/pomdp.rs +++ b/neurosploit-rs/crates/harness/src/pomdp.rs @@ -1,4 +1,4 @@ -//! POMDP decision layer (v3.5.2): value-of-information planning + the +//! POMDP decision layer (v3.5.3): value-of-information planning + the //! anti-hallucination gate. //! //! The choice "scan more vs exploit now" is **not** a heuristic here โ€” it falls diff --git a/neurosploit-rs/crates/harness/src/report.rs b/neurosploit-rs/crates/harness/src/report.rs index 145f2ae..e9ea22e 100644 --- a/neurosploit-rs/crates/harness/src/report.rs +++ b/neurosploit-rs/crates/harness/src/report.rs @@ -97,9 +97,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.5.2 Rust harness ยท multi-model validated
\ +
Target: {t} ยท v3.5.3 Rust harness ยท multi-model validated
\
{chips}
{graph_block}

Findings ({n})

{body}\ -

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

", +

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

", t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block, ) } @@ -135,7 +135,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.5.2"), tq("multi-model") + tq(target), tq(&run_id), tq("NeuroSploit v3.5.3"), tq("multi-model") )); data.push_str("#let findings = (\n"); for f in sorted_findings(findings) { diff --git a/setup.sh b/setup.sh index 2857376..181a3fc 100755 --- a/setup.sh +++ b/setup.sh @@ -25,7 +25,7 @@ cat <<'BANNER' โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•— NeuroSploit installer - โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ v3.5.2 โ€” Rust harness + โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ v3.5.3 โ€” Rust harness โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ by Joas A Santos โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• & Red Team Leaders โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•