mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-07 03:47:56 +02:00
v3.5.3 — Integrations (GitHub · GitLab · Jira)
New harness module `integrations` (+ app commands) wiring NeuroSploit into the SDLC. Config persists per-project to .neurosploit/integrations.json; secrets are NEVER stored — only the env-var name is saved, values read from the environment. GitHub: - private-repo clone (token injected into the clone URL for whitebox/greybox/tui) - `neurosploit pr <owner/repo> <n>`: clone the PR head (refs/pull/N/head), white-box review, optional `--comment` (PR summary) and `--jira` (cards) - `neurosploit watch <owner/repo> --branch --interval`: re-review on each new commit GitLab: - private-repo clone (oauth2 token) for whitebox/greybox (gitlab.com or self-hosted) Jira: - `--jira` on any engagement opens one card per finding (REST /issue, basic auth) Control: - `/integrations` (REPL): show · enable/disable · setup jira|gitlab|github - `neurosploit integrations [show|enable|disable] [github|gitlab|jira]` (CLI) Docs: README "Integrations" section + new TUTORIAL-INTEGRATION.md (per-tool setup, scopes, recipes, troubleshooting). Version bumped 3.5.2 → 3.5.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 /
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `<project>/.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<String> {
|
||||
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<String> { env(&self.github.token_env) }
|
||||
pub fn gitlab_token(&self) -> Option<String> { 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<String> {
|
||||
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<String> {
|
||||
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<String>, Vec<String>) {
|
||||
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<String> {
|
||||
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 }),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}</style></head><body>\
|
||||
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
|
||||
<div class=meta>Target: <b>{t}</b> · v3.5.2 Rust harness · multi-model validated</div>\
|
||||
<div class=meta>Target: <b>{t}</b> · v3.5.3 Rust harness · multi-model validated</div>\
|
||||
<div>{chips}</div>{graph_block}<h2>Findings ({n})</h2>{body}\
|
||||
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.2 · by <b>Joas A Santos</b> & <b>Red Team Leaders</b></p></body></html>",
|
||||
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.3 · by <b>Joas A Santos</b> & <b>Red Team Leaders</b></p></body></html>",
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user