Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-bug-hunter",
"description": "AI-powered bug bounty toolkit \u2014 recon, vulnerability hunting, validation gates, and submission-ready reports for HackerOne, Bugcrowd, Intigriti, and Immunefi.",
"version": "4.3.2",
"description": "AI-powered bug bounty toolkit — recon, vulnerability hunting, validation gates, and submission-ready reports for HackerOne, Bugcrowd, Intigriti, and Immunefi.",
"version": "6.0.1",
"author": {
"name": "AwareXone",
"url": "https://awarexone.com"
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ api_keys.txt
# Nested local clones / sub-repos (not tracked)
Agentic-Bug-Hunter/

# Local control sockets (short paths for AF_UNIX)
.ctl/

# Relocatable engine assembled by scripts/bundle-engine.sh
desktop/src-tauri/resources/engine/**
!desktop/src-tauri/resources/engine/README.md

# Python
__pycache__/
*.py[cod]
.venv/
.venv-*/
venv/
*.egg-info/

# macOS
.DS_Store
.AppleDouble
*.p12
*.cer
*.p8
AuthKey_*.p8

# Logs
*.log
Expand All @@ -36,5 +48,8 @@ venv/
nmap-output/
.atlas/
.forge/

# Runtime lead-board ledgers (per-target, regenerated on each hunt)
memory/leads/
dist/
build/
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ This repo is an agent-portable bug bounty plugin for professional hunting across
| `skills/mobile-pentest/` | Android/iOS app pentest — runtime-first proxy workflow, APK/IPA decompile, deeplink injection, WebView bridge |
| `skills/cicd-security/` | CI/CD pipeline hunting — GitHub Actions injection, secret exfil, self-hosted runner poisoning |
| `skills/graphql-audit/` | GraphQL hunting — introspection, field suggestions, batching DoS, IDOR via aliasing, injection |
| `skills/mcp-server-audit/` | MCP server audit — tool poisoning, param→sink injection, missing approval gates, secret leaks, rug-pull/confused-deputy, transport config |
| `skills/cloud-pentest/` | Post-access cloud exploitation (AWS/GCP/Azure) — IAM enum + privesc, IMDS metadata creds, impersonation, bucket takeover, secrets harvest, impact proof |

### Commands (slash commands)

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ This repo is a Claude Code plugin for professional bug bounty hunting across Hac
| `skills/cicd-security/` | CI/CD pipeline hunting — GitHub Actions injection, secret exfil, self-hosted runner poisoning, OIDC abuse, supply chain attacks |
| `skills/graphql-audit/` | GraphQL hunting — introspection, field suggestions (clairvoyance), batching DoS, IDOR via aliasing, injection, auth bypass, depth bombs |
| `skills/argus/` | **Argus** (all-seeing scanner suite) — CORS, CRLF/host-header, NoSQL injection, JWT (alg:none/confusion/crack), OOB blind-bug confirmation (interactsh), LLM red-team corpus |
| `skills/mcp-server-audit/` | MCP server security — tool poisoning, param→sink injection (path/cmd/SSRF/SQL), missing approval gates, result-channel secret leaks, rug-pull/confused-deputy, transport config |
| `skills/cloud-pentest/` | Post-access cloud exploitation (AWS/GCP/Azure) — IAM enum + privesc, IMDS metadata creds, STS/impersonation, bucket takeover, secrets harvest, proving impact |

### Commands (33 slash commands)

Expand Down
477 changes: 373 additions & 104 deletions bughunter/agent.py

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions bughunter/brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1703,13 +1703,68 @@ def build_chains(self, findings_dir: str) -> str:
# ─────────────────────────────────────────────────────────────────────────
# Phase 4 — Report Writer
# ─────────────────────────────────────────────────────────────────────────
def _report_gate(self, findings_dir: str) -> tuple[bool, str]:
"""Return (ready, note). A report is allowed only when at least one
validation.json under findings_dir is report-ready (deterministically
verified + evidence linked). Set BBHUNT_ALLOW_UNVALIDATED_REPORT=1 to
bypass when no validations exist yet (legacy flows / manual review)."""
import glob as _glob
import json as _json

try:
from tools.validate_core import is_report_ready
except Exception: # noqa: BLE001 - if the gate can't load, fail open loudly
return True, ""

val_files = _glob.glob(os.path.join(findings_dir, "**", "validation.json"),
recursive=True)
if not val_files:
if os.environ.get("BBHUNT_ALLOW_UNVALIDATED_REPORT") == "1":
return True, ""
return False, (
"NO_REPORTS\nReport gate blocked: no validation.json found. Run the "
"deterministic verifier first (python3 -m tools.verifiers / "
"tools/validate.py --auto <finding.json>). "
"Set BBHUNT_ALLOW_UNVALIDATED_REPORT=1 to override."
)

ready, blocked = [], []
for vf in val_files:
try:
v = _json.loads(open(vf, encoding="utf-8").read())
except (OSError, ValueError):
continue
(ready if is_report_ready(v) else blocked).append(
(vf, v.get("status"), v.get("rejection_reasons") or []))

if ready:
return True, ""
reasons = "; ".join(
f"{os.path.basename(os.path.dirname(p))}: {s} ({', '.join(r) or 'unconfirmed'})"
for p, s, r in blocked[:5]
)
return False, (
"NO_REPORTS\nReport gate blocked: no finding passed deterministic "
f"verification. {reasons}"
)

def write_report(self, findings_dir: str, recon_dir: str = "") -> str:
if not self.enabled:
return ""

findings_path = Path(findings_dir)
target = self._target_from_artifact_dir(findings_dir)

# HARD GATE — a report may only be written for findings that a
# deterministic verifier confirmed (validation.json status ==
# validated_finding with a linked verifier trace). Discovery never
# confirms its own bug; this is the last checkpoint before a report.
ready, gate_note = self._report_gate(findings_dir)
if not ready:
print(f"{YELLOW}[!] Report gate: {gate_note}{NC}")
self._save_analysis(findings_dir, "04_h1_reports.md", gate_note)
return gate_note

evidence = self._build_report_evidence(findings_dir, recon_dir)
if not evidence.strip():
note = "NO_REPORTS\nNo grounded report candidates were found in the validated scan artifacts."
Expand Down
39 changes: 29 additions & 10 deletions bughunter/mcp/bughunter-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,41 @@ This does **not** replace:
- `caido-mcp-client/`
- `hackerone-mcp/`

## Run
## Install (any MCP client)

```bash
pip install 'mcp>=2.2.0'
python3 mcp/bughunter-mcp/server.py
# or
./install.sh --agent standalone
bughunter mcp serve
bughunter mcp doctor
bughunter mcp tools
pip install agentic-bug-hunter
```

That installs the `bughunter` command. The MCP server runs as `bughunter mcp serve`,
so the same config works from any directory in any MCP-compatible agent.

```bash
bughunter mcp doctor # verify SDK, tools, and paths
bughunter mcp tools # list the exposed tool catalog
```

## Clients

- Claude Code: merge `claude-config.json` into `mcpServers`
- OpenCode: see `opencode-config.json`
Add this to your client's MCP config (works after `pip install`, no repo checkout needed):

```json
{
"mcpServers": {
"bughunter": {
"command": "bughunter",
"args": ["mcp", "serve"],
"env": { "BBHUNT_MCP_APPROVE": "0" }
}
}
}
```

- **Claude Desktop / Claude Code**: merge `claude-config.json` into `mcpServers`
- **Cursor / Cline / Windsurf / Zed**: same `command` + `args` in their MCP settings
- **OpenCode**: see `opencode-config.json`

Running from a cloned repo instead of pip? Use `python3 mcp/bughunter-mcp/server.py`.

## Safety

Expand Down
98 changes: 91 additions & 7 deletions bughunter/mcp/bughunter-mcp/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,55 @@ def _sys_path() -> None:
sys.path.insert(0, root)


def _build_checker(domains: list[str] | None, excluded: list[str] | None = None):
"""Build the ONE authoritative ScopeChecker (same class the agent path uses),
or None if no domains / a dangerous config. Never a second implementation."""
if not domains:
return None
_sys_path()
from tools.scope_checker import ScopeChecker, ScopeError

try:
return ScopeChecker(domains=domains, excluded_domains=excluded)
except ScopeError as exc:
import sys as _sys
print(f"WARNING: rejected unsafe scope configuration: {exc}", file=_sys.stderr)
return None


# Recon-output files that later scanners read. Mirrors
# agent.ToolDispatcher._filter_recon_urls_to_scope so the MCP path filters
# discovered (possibly out-of-scope) hosts before any active scan reads them.
_RECON_URL_FILES = (
("urls", "all.txt"),
("urls", "with_params.txt"),
("urls", "js_files.txt"),
("urls", "api_endpoints.txt"),
("urls", "graphql.txt"),
("live", "urls.txt"),
)


def _filter_recon_to_scope(target: str, checker) -> dict[str, int]:
"""Drop out-of-scope URLs from recon files in place. Returns per-file counts.
No-op when checker is None (caller decides fail-closed behavior separately)."""
dropped: dict[str, int] = {}
if checker is None:
return dropped
recon_dir = REPO / "recon" / target
for parts in _RECON_URL_FILES:
f = recon_dir.joinpath(*parts)
if not f.is_file():
continue
try:
_kept, out = checker.filter_file(str(f))
if out:
dropped["/".join(parts)] = out
except OSError:
continue
return dropped


class UnsafeTargetError(ValueError):
"""Raised when a target/relative path would escape the repo sandbox."""

Expand Down Expand Up @@ -52,8 +101,12 @@ def scope_check(target: str, domains: list[str], excluded: list[str] | None = No
}


def run_recon(target: str, *, timeout: int = 600) -> dict[str, Any]:
"""Invoke tools/recon_engine.sh (existing recon)."""
def run_recon(target: str, *, timeout: int = 600, scope_checker=None) -> dict[str, Any]:
"""Invoke tools/recon_engine.sh (existing recon).

If a scope_checker is supplied, recon-discovered URL files are filtered to
scope in place before any downstream scanner reads them.
"""
script = REPO / "tools" / "recon_engine.sh"
if not script.exists():
return {"status": "failed", "error": "RESEARCH_FAILED", "reason": "recon_engine.sh missing"}
Expand Down Expand Up @@ -87,6 +140,11 @@ def run_recon(target: str, *, timeout: int = 600) -> dict[str, Any]:
"stderr_tail": (proc.stderr or "")[-1000:],
"next_action": "review_attack_surface",
}
# Scope-filter discovered URLs in place before anything reads them.
if recon_dir.is_dir() and scope_checker is not None:
dropped = _filter_recon_to_scope(target, scope_checker)
if dropped:
summary["scope_filtered_out_of_scope"] = dropped
# ingest leads if recon produced output
if recon_dir.is_dir():
try:
Expand Down Expand Up @@ -153,16 +211,41 @@ def attack_surface(target: str) -> dict[str, Any]:
}


def run_hunt(target: str, *, quick: bool = False, timeout: int = 900) -> dict[str, Any]:
def run_hunt(target: str, *, quick: bool = False, timeout: int = 900, scope_checker=None) -> dict[str, Any]:
_sys_path()
# Hard block: never launch active testing against an out-of-scope base
# target, and filter recon-discovered hosts to scope before the scanner
# (vuln_scanner.sh) reads them. Uses the one authoritative ScopeChecker.
if scope_checker is not None:
if not scope_checker.is_in_scope(target):
return {
"status": "denied",
"error": "OUT_OF_SCOPE",
"target": target,
"reason": f"{target} is out of scope — refusing to hunt",
"next_action": "Choose an in-scope target",
}
_filter_recon_to_scope(target, scope_checker)
try:
from tools import hunt as hunt_mod
except Exception:
hunt_mod = None
if hunt_mod and hasattr(hunt_mod, "hunt_target"):
try:
# Many hunt_target signatures take target string
result = hunt_mod.hunt_target(target) if not quick else hunt_mod.hunt_target(target)
# hunt_target re-runs recon (which can repopulate URL files with
# out-of-scope hosts) and then the vuln scanner. Install the SAME
# ScopeChecker process-wide for the duration so hunt.py's own gate
# (block out-of-scope target, filter recon files before scanning)
# applies inside hunt_target too. Restore afterward to avoid leaking
# scope across MCP calls.
_prev = getattr(hunt_mod, "_SCOPE_CHECKER", None)
if scope_checker is not None and hasattr(hunt_mod, "set_scope_checker"):
hunt_mod.set_scope_checker(scope_checker)
try:
result = hunt_mod.hunt_target(target)
finally:
if scope_checker is not None and hasattr(hunt_mod, "set_scope_checker"):
hunt_mod.set_scope_checker(_prev)
return {
"status": "completed",
"target": target,
Expand Down Expand Up @@ -411,6 +494,7 @@ def research(
) -> dict[str, Any]:
"""High-level workflow stages — does not auto-run everything."""
mode = (mode or "RECON").upper()
checker = _build_checker(domains)
stages: list[dict[str, Any]] = []
out: dict[str, Any] = {
"status": "completed",
Expand All @@ -424,10 +508,10 @@ def research(
"next_action": "continue",
}
if mode in {"RECON", "FULL"}:
stages.append(run_recon(target))
stages.append(run_recon(target, scope_checker=checker))
out["research"]["stage"] = "recon"
if mode in {"HUNT", "FULL"}:
stages.append(run_hunt(target))
stages.append(run_hunt(target, scope_checker=checker))
out["research"]["stage"] = "hunt"
out["findings"] = list_findings(target).get("findings", [])
if mode == "VALIDATE":
Expand Down
4 changes: 2 additions & 2 deletions bughunter/mcp/bughunter-mcp/claude-config.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"mcpServers": {
"bughunter": {
"command": "python3",
"args": ["mcp/bughunter-mcp/server.py"],
"command": "bughunter",
"args": ["mcp", "serve"],
"env": {
"BBHUNT_MCP_APPROVE": "0"
}
Expand Down
2 changes: 1 addition & 1 deletion bughunter/mcp/bughunter-mcp/opencode-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"bughunter": {
"type": "local",
"enabled": true,
"command": ["python3", "mcp/bughunter-mcp/server.py"],
"command": ["bughunter", "mcp", "serve"],
"environment": {
"BBHUNT_MCP_APPROVE": "0"
}
Expand Down
Loading
Loading