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
@@ -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