feat: PR security gate, @neurosploit bot, richer NL REPL (#39)

GitHub automation
- integrations: github_set_status (commit status), github_pr_review
  (REQUEST_CHANGES/APPROVE), github_pr_head_sha, and a shared severity
  gate (severity_rank / worst_confirmed_rank / gate_trips — confirmed
  findings only).
- `neurosploit pr --fail-on <critical|high|medium|low>`: on a confirmed
  finding at/above the threshold, sets a failing `neurosploit/security`
  commit status, posts a REQUEST_CHANGES review, and exits 2 so a CI
  check fails — branch protection then blocks the merge.
- Two ready GitHub Actions: neurosploit-pr-gate.yml (review + block every
  PR) and neurosploit-mention.yml (writers comment @neurosploit <text> to
  trigger a scan; any language; URL → black-box, else PR review).

Natural-language REPL
- Intent now also parses spoken toggles/knobs across PT/EN/ES: Burp/proxy,
  browser/MCP, subscription, "N votos/votes", recon depth (number or
  quick/deep/exhaustive), plus stop verbs. handle_nl returns the follow-up
  command (/run or /stop).

Docs: README trimmed to features (version changelog stays in RELEASE.md),
new automations documented in README + TUTORIAL-INTEGRATION.

Tests: gate (3), NL toggles/stop (added). All green.


Claude-Session: https://claude.ai/code/session_018BGLy4j5qsqqid6CoovowC

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Joas A Santos
2026-08-02 19:12:18 -03:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 21a62c95e5
commit 3786d7c559
8 changed files with 544 additions and 43 deletions
+54 -1
View File
@@ -258,6 +258,11 @@ enum Cmd {
/// Post a summary comment back on the PR (needs github integration on).
#[arg(long)]
comment: bool,
/// Block the PR when a confirmed finding is this severity or worse:
/// critical|high|medium|low. Sets a failing commit status + a
/// REQUEST_CHANGES review, and exits non-zero so CI fails the check.
#[arg(long)]
fail_on: Option<String>,
/// Open a Jira card per finding (needs jira integration on).
#[arg(long)]
jira: bool,
@@ -494,7 +499,7 @@ async fn main() -> anyhow::Result<()> {
let out = run_mode(&base, cfg, false, Mode::Skills).await?;
print_findings(&out);
}
Cmd::Pr { repo, number, models, vote_n, chain_depth, recon, subscription, comment, jira, verbose } => {
Cmd::Pr { repo, number, models, vote_n, chain_depth, recon, subscription, comment, fail_on, 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)?;
@@ -510,6 +515,16 @@ async fn main() -> anyhow::Result<()> {
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;
// Security gate: block the PR when a confirmed finding is >= threshold.
if let Some(thresh) = fail_on.as_deref() {
let blocked = gate_pr(&ig, &owner_repo, number, &out, thresh).await;
if blocked {
eprintln!(" \x1b[1;31m⛔ PR gate: confirmed finding ≥ {thresh} — blocking (exit 2)\x1b[0m");
std::process::exit(2);
} else {
println!(" \x1b[1;32m✓ PR gate: nothing ≥ {thresh} — clear\x1b[0m");
}
}
}
Cmd::Watch { repo, branch, interval, models, subscription, jira, verbose } => {
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
@@ -972,6 +987,44 @@ fn clone_pr(base: &Path, ig: &harness::integrations::Integrations, owner_repo: &
}
/// After a run, optionally open Jira cards and/or comment on a GitHub PR.
/// Enforce the PR security gate. Sets a GitHub commit status (success/failure)
/// on the PR head and, when it trips, submits a REQUEST_CHANGES review so branch
/// protection blocks the merge. Best-effort on the API calls (a token may be
/// absent locally); returns whether the gate tripped so the caller can exit 2.
async fn gate_pr(
ig: &harness::integrations::Integrations,
owner_repo: &str,
number: u64,
out: &RunOutput,
threshold: &str,
) -> bool {
use harness::integrations as gi;
let tripped = gi::gate_trips(&out.findings, threshold);
if ig.github.enabled {
let (state, desc) = if tripped {
("failure", format!("Confirmed finding ≥ {threshold} — merge blocked by NeuroSploit"))
} else {
("success", "No confirmed finding at/above the gate threshold".to_string())
};
// Attach the status to the PR head SHA (looked up from the API).
match ig.github_pr_head_sha(owner_repo, number).await {
Ok(sha) => {
if let Err(e) = ig.github_set_status(owner_repo, &sha, state, "neurosploit/security", &desc, None).await {
eprintln!(" github status: {e}");
}
}
Err(e) => eprintln!(" github PR head lookup: {e}"),
}
if tripped {
let body = format!("## ⛔ NeuroSploit security gate\n\nBlocking this PR: a **confirmed** finding is **{threshold}** or worse.\n\n{}", pr_comment_body(out));
if let Err(e) = ig.github_pr_review(owner_repo, number, "REQUEST_CHANGES", &body).await {
eprintln!(" github review: {e}");
}
}
}
tripped
}
async fn post_integrations(
ig: &harness::integrations::Integrations,
target: &str,