Skip to content
BreachPilot

Tool Family: agent-modules

Agent-planning MCP adapters split out of the attack-modules god file into tools/mcp_tools/modules/. Each file defines one register_*_tools(mcp, *, ctx) registrar; tools/mcp_tools/registry.py auto-discovers every register_*_tools in the package including subpackages (modules/, terminal/) via _discover_tool_registrars, and collect_tools() AST-validates that every @mcp.tool also carries @audit_tool or @require_allowlist (CI fails otherwise). No edit to mcp_exploit_server.py is needed for a new module file.

  • Registration sources: register_planning_tools (modules/planning.py:18), register_campaign_tools (modules/campaign.py:32), register_adaptive_tools (modules/adaptive.py:17), register_synthesis_tools (modules/synthesis.py:16), register_hash_tools (modules/hash.py:128), register_web_tools (modules/web.py:170).
  • Context: ToolContext (tools/mcp_tools/registry.py:45-61) — workspace, config, search (ExploitSearch), nvd (NVDClient), researcher (WebResearcher), audit_tool, require_allowlist, plus sandbox / sandbox_notice for the execution plane.
  • Gate pattern: target-touching tools use @require_allowlist(); advisory/local-only tools use @audit_tool. run_campaign_step is @audit_tool + a manual check_targets_allowlist on the target read back from state.json (LLM-writable path).
  • Shared helper: _identify_hash_modes(h) (modules/hash.py:11) — single source for the hash-type → hashcat-mode mapping, re-exported via tools/mcp_tools/attack_modules.py for cracking.py and modules/synthesis.py.

Architecture

mcp_exploit_server.create_mcp_server
  └─ collect_tools() ─► _discover_tool_registrars()  (package walk incl. modules/)
       ├─ register_planning_tools  → create_attack_plan / get_current_plan / replan
       ├─ register_campaign_tools  → start_autonomous_campaign / get_campaign_status /
       │                              run_campaign_step / stop_campaign
       ├─ register_adaptive_tools  → craft_exploit / mutate_exploit
       ├─ register_synthesis_tools → cve_to_exploit_synth (+ 18 _render_* templates)
       ├─ register_hash_tools      → hash_crack_identify (+ _identify_hash_modes)
       └─ register_web_tools       → jwt_tamper / ssti_probe / graphql_introspect /
                                      race_request / timing_oracle /
                                      request_smuggling_probe / password_spray

Backing engines live outside mcp_tools: tools/attack_planner.py (plan model + prompts), tools/autonomous_orchestrator.py (campaign runner), tools/exploit_mutator.py + tools/payload_crafter.py (adaptive generation), tools/attack_modules/ (pre-packaged module registry), tools/cve_lookup.py + ExploitSearch.cve_to_poc (synth evidence), tools/recon_pipeline.py (campaign step recon).

Planning (modules/planning.py)

def register_planning_tools(mcp: Any, *, ctx: ToolContext) -> None
def create_attack_plan(target_ip: str, target_os: str = "", known_cves: str = "") -> str
def get_current_plan(target_ip: str) -> str
def replan(target_ip: str, failure_reason: str) -> str
ToolGateResult shapeNotes
create_attack_plan@require_allowlist()ATTACK_PLAN_CREATED: <ip>\nPLAN_ID: ...\nPHASES: RECON → …\nCURRENT_PHASE: ...\nTOTAL_STEPS: N\nSAVED_TO: plans/<ip>_plan.jsonAttackPlanner(workspace).create_plan(target_ip, target_os, known_cves.split(","), attack_mode=True); when _get_model_client(config) returns a client, builds build_planning_prompt(...) and appends parse_plan_json(content) steps; always planner.save_plan(plan). Invalid target → ERROR: Invalid target (IP or domain).
get_current_plan@require_allowlist()ATTACK_PLAN: ...\nCURRENT_PHASE / PHASES / TOTAL_STEPS / COMPLETED / SUCCESSFUL / FAILED / IS_COMPLETE + plan.generate_battle_log()planner.load_plan(target_ip); missing → NO_PLAN_FOUND: ... Use create_attack_plan first.
replan@require_allowlist()REPLAN_RESULT: ...\nFAILURE_REASON: <first 200 chars>\nNEW_PHASE: ...\nTOTAL_STEPS: ... + battle logLoads plan, builds build_replanning_prompt(plan, failure_reason, attacker_os); parse_replan_json yields (action, step, explanation)next_phase / done / add_step; model failure falls back to plan.next_phase(). Always re-saves.

Lifecycle: create_attack_planworkspace/plans/<ip_underscored>_plan.jsonget_current_plan reads → replan mutates + re-saves. Kill chain phases follow RECON → ENUMERATE → EXPLOIT → ESCALATE → LOOT → PIVOT → DONE (per the tool docstring; phase enum lives in tools/attack_planner.py).

Campaign (modules/campaign.py)

_running_campaign_tasks: set = set()
_campaign_orchestrators: dict[str, Any] = {}
def register_campaign_tools(mcp: Any, *, ctx: ToolContext) -> None
async def start_autonomous_campaign(target_ip: str, goal: str = "initial_access", aggression_level: str = "normal") -> str
def get_campaign_status(campaign_id: str) -> str
async def run_campaign_step(campaign_id: str) -> str
def stop_campaign(campaign_id: str) -> str
ToolGateResult shapeNotes
start_autonomous_campaign@require_allowlist()CAMPAIGN_STARTED: <id>\nTARGET / GOAL / AGGRESSION\nSTATUS: started\nCAMPAIGN_DIR / STATE_FILEMaps stealth/normal/aggressive/maximumAggressionLevel (unknown → NORMAL). campaign_id = campaign-<UTC ts>-<sha256(target)[:8]>; workspace/campaigns/<id>/ + initial state.json (status: started). Merges the autonomous config block plus opsec, exploit.msf.auto_local_exploit_suggester, orchestrator.semantic_memory + ollama/memory.embedding_model, agent, killchain, snapshots, replay_simulator, then overrides target/goal/aggression/max_cycles (= exploit.max_rounds)/workspace. Domain targets thread original_target + resolved_ip (is_fqdn / resolve_target_to_ip). Launches asyncio.create_task(orchestrator.run_autonomous_campaign(...)), held in _running_campaign_tasks (strong ref — the event loop only weak-refs tasks) with a done-callback that drops the ref and pops _campaign_orchestrators[campaign_id]. Completion writes status: completed (+ task counts from orchestrator._tasks by TaskStatus); exception writes status: error + last_error. swarm.enabled: false in config → BLOCKED: swarm is disabled in config.yaml.
get_campaign_status@audit_toolCAMPAIGN_STATUS / TARGET / GOAL / STATUS / AGGRESSION / CURRENT_PHASE / STARTED_AT / COMPLETED_AT\nTASKS: Completed/Failed/Pending\nCOMPROMISED: ...\nLAST_ERROR?Pure read of workspace/campaigns/<id>/state.json; missing → ERROR: Campaign '<id>' not found.
run_campaign_step@audit_tool + manual check_targets_allowlist([target_ip], config)CAMPAIGN_STEP_RESULT: recon_completed / executed / no_applicable_modules / blockedRebuilds a single-step AutonomousOrchestrator (max_cycles=1, same config merge as start). If state.recon_result is None, runs ReconPipeline(ReconConfig()).recon_host(target_ip) → sets ENUMERATION, writes state, returns open ports/services. Else builds ModuleContext(target_ip, target_os, services) and runs the top find_modules(ctx) hit, bumping tasks.completed/failed and compromised_hosts in state.json. Off-allowlist target from state → CAMPAIGN_STEP_RESULT: blocked.
stop_campaign@audit_toolSTOPPED: Campaign '<id>' stop signal sent. / ... is not running (already finished). / ERROR: Campaign '<id>' not found.orchestrator.stop() on the live entry in _campaign_orchestrators; state.json left intact for get_campaign_status.

Adaptive (modules/adaptive.py)

def register_adaptive_tools(mcp: Any, *, ctx: ToolContext) -> None
def craft_exploit(target_ip: str, service_name: str, version: str = "", os_hint: str = "", module_name: str = "") -> str
def mutate_exploit(script_id: str, failure_output: str) -> str
ToolGateResult shapeNotes
craft_exploit@require_allowlist()CRAFT_EXPLOIT_RESULT: generated\nGENERATION_ID / SCRIPT_PATH / SIDECAR_PATH / CONFIDENCE / MUTATION_STRATEGY / TARGET / SERVICE / OS_HINT / MODULE + SCRIPT_PREVIEW (first 500 chars)ExploitMutator(workspace=exploits_dir, experience_store, client, model, max_mutations).craft_initial(...). ExperienceStore(get_default_db()) falls back to None when the DB is unavailable. Saves workspace/exploits/<generation_id>.py + sidecar <generation_id>.json (generation_id, parent_id, mutation_strategy, confidence, metadata, created_at). Empty service → ERROR; adaptive_exploits.enabled: falseBLOCKED.
mutate_exploit@audit_tool (no target touch — operates on workspace files by id)MUTATE_EXPLOIT_RESULT: mutated\nGENERATION_ID / PARENT_ID / SCRIPT_PATH / SIDECAR_PATH / CONFIDENCE / MUTATION_STRATEGY / ATTEMPT_NUMBER + preview; or max_mutations_reached with ATTEMPT / MAX_MUTATIONSLoads <script_id>.py + <script_id>.json, reconstructs CraftedPayload, walks parent sidecars to derive attempt_number, calls mutator.mutate_on_failure(payload, failure_output, attempt_number) (None → max-reached block). Missing script/sidecar → ERROR. Same adaptive_exploits.enabled gate.

Lifecycle: craft_exploit (generation 1, parent_id=None) → run → mutate_exploit with failure output → child generation with parent_id linkage, up to adaptive_exploits.max_mutations.

Synthesis (modules/synthesis.py)

def register_synthesis_tools(mcp: Any, *, ctx: ToolContext) -> None
def cve_to_exploit_synth(target_ip: str, cve_id: str, service_name: str = "", version: str = "") -> str
def _render_log4j_exploit(target_ip: str, port: int, svc: str, ver: str, cve: str) -> str
# + 17 more _render_*_template with the same signature (see table)
def _render_generic_probe(target_ip: str, port: int, svc: str, ver: str, cve: str) -> str

Dispatch table (18 branches; TEMPLATE_DISPATCHED: <name> is echoed so the agent can see which branch fired):

BranchTrigger (CVE id or service_name hint)Renderer
log4jname contains log4j/log4shell, CVE-2021-44228, CVE-2021-45046_render_log4j_exploit
eternalbluename contains eternalblue, CVE-2017-0143…0146_render_eternalblue_template
smbghostname contains smbghost, CVE-2020-0796_render_smbghost_template
bluekeepname contains bluekeep, CVE-2019-0708_render_bluekeep_template
regresshionCVE-2024-6387, svc openssh_render_regresshion_template
xz_backdoorCVE-2024-3094, name contains xz_render_xz_backdoor_template
activemqCVE-2023-46604, svc activemq_render_activemq_rce_template
confluenceCVE-2023-22515, svc confluence_render_confluence_template
ivantiCVE-2024-21887, CVE-2023-46805, svc ivanti_render_ivanti_template
panosCVE-2024-3400, svc panos_render_panos_template
citrixCVE-2023-3519, svc citrix_render_citrix_template
connectwiseCVE-2024-0204, svc connectwise_render_connectwise_template
jenkinsCVE-2024-23897, svc jenkins_render_jenkins_template
joomlaCVE-2023-23752, svc joomla_render_joomla_template
text4shellCVE-2022-42889, svc commons_text_render_text4shell_template
php_cgiCVE-2024-4577, svc php_cgi_render_php_cgi_template
http2_rapid_resetCVE-2023-44487_render_http2_rapid_reset_template
genericfallback_render_generic_probe

cve_to_exploit_synth pipeline: validate target (validate_target_or_ip) + CVE format (CVE-\d{4}-\d{4,}) + reject newline/quote/backslash in service_name/version (they are interpolated into generated-source comments) → nvd.search_sync(cve) + format_cve_results (capped at 2000 chars) → verified PoC lookup via search.cve_to_poc(cve, nvd_refs) with search_web_exploit fallback (capped at 1500 chars) → port hint from service_name (http 80, https 443, smb 445, rdp 3389, ssh 22, activemq 61616, confluence 8090, jenkins 8080, …) → renderer → optional inline syntax gate (poc_verification.enabledsyntax_check(exploit_body)SYNTAX_OK: true/false + fix-and-verify_poc instructions) → INSTRUCTIONS: Use write_python_file ... then run_python_file.

All templates are detection-only probes (banner/version checks, safe payloads, non-routable default callbacks) meant for the LLM to refine — not weaponized exploits.

Implementation note: CVEToExploit (tools.attack_modules.modules.synthesis) and _identify_hash_modes are imported at the top of synthesis.py as the single-source reuse seam, but the tool body builds its output from the local _render_* templates plus nvd/search evidence rather than calling the module class directly.

Hash (modules/hash.py)

def _identify_hash_modes(h: str) -> list[tuple[str, str, str]]
def register_hash_tools(mcp: Any, *, ctx: ToolContext) -> None
def hash_crack_identify(hash_value: str) -> str

_identify_hash_modes returns [(name, hashcat_mode, sample_cmd), ...]. Order matters: a 32-hex string reports both NTLM (mode 1000) and MD5 (mode 0) since they are format-indistinguishable (context decides — NTLM from SMB, MD5 from a web app). LM (16 hex, mode 3000) only fires when nothing else matched, so fragments of longer hashes do not false-positive.

PatternNameMode
32 hexNTLM + MD5 (also possible)1000 / 0
user::domain:challenge:… (≥5 : parts)NetNTLMv25600
$krb5tgs$18$… / $krb5tgs$…Kerberos 5 TGS-REP etype 18 / TGS-REP19900 / 13100
$krb5asrep$18$… / $krb5asrep$…Kerberos 5 AS-REP etype 18 / AS-REP19900 / 18200
40 / 64 / 128 hexSHA1 / SHA2-256 / SHA2-512100 / 1400 / 1700
$2a$/$2b$/$2y$/$2$/$2x$bcrypt3200
$6$…$… / $1$…$…sha512crypt / md5crypt + Cisco type 51800 / 500
$9$ / $4$Cisco type 9 (scrypt) / type 4 (PBKDF2-SHA256)22321 / 2400
0x0100… (≥54 chars) / 0x0200… (≥70)MSSQL 2005 / 2012-2014132 / 1731
$argon2…Argon2 (john only)N/A (john --format=argon2)
$scrypt$… / SCRYPT:…scrypt8900
pbkdf2_sha256$/sha1$Django PBKDF212100
$pdf$… / $office$…PDF / MS Office10400 / 9400
64-hex:SSIDWPA-PBKDF2 (possible)22000
16 hex (nothing else matched)LM3000

hash_crack_identify (@audit_tool, local-only) prints each (Type, mode, Command) plus a rule-based-attack hint (-r best64.rule / OneRuleToRuleThemAll); unknown formats suggest hashid/hash-identifier with an 80-char preview.

Web probes (modules/web.py)

_MIN_PORT = 1; _MAX_PORT = 65535
_MIN_CONCURRENT = 2; _MAX_CONCURRENT = 100
_MIN_TOOL_TIMEOUT = 5; _MAX_TOOL_TIMEOUT = 300; _DEFAULT_TOOL_TIMEOUT = 90
_MAX_OUTPUT_CHARS = 20000; _MAX_ENDPOINT_CHARS = 2048
def register_web_tools(mcp: Any, *, ctx: ToolContext) -> None
def jwt_tamper(target_ip: str, jwt_token: str = "", timeout: int = 90) -> str
def ssti_probe(target_ip: str, port: int = 80, timeout: int = 90) -> str
def graphql_introspect(target_ip: str, port: int = 80, timeout: int = 90) -> str
def race_request(target_ip: str, port: int = 80, endpoint: str = "/api/redeem", concurrent: int = 20, timeout: int = 90) -> str
def timing_oracle(target_ip: str, port: int = 80, timeout: int = 90) -> str
def request_smuggling_probe(target_ip: str, port: int = 80, timeout: int = 90) -> str
def password_spray(target_ip: str, port: int = 80, password: str = "Password1", timeout: int = 90) -> str

Shared pre-gates (web.py:43-167): _check_target (non-empty, no CR/LF, validate_target_or_ip), _check_port (1-65535, numeric strings accepted), _check_concurrent (2-100), _check_endpoint (absolute path, no CR/LF or whitespace, ≤2048 chars), _clamp_timeout (5-300 s, default 90), _http_host (brackets IPv6), _open_connection (socket.create_connection, always used as a context manager), _sock_budget (per-connection budget honoring the tool deadline), _finish (caps display at 20000 chars with a [truncated] marker).

ToolGateWhat it does
jwt_tamper@require_allowlist() + syntax/CR-LF pre-gatesEmpty jwt_token → probes 15 discovery paths (/api/auth/login, /oauth/token, Keycloak, wp-json/jwt-auth, openid-configuration, …) on port 80 for a header.payload.signature regex hit. Then: alg:none token material + curl tester; weak-HMAC brute-force over a 34-secret list when alg starts with HS; key-confusion note when alg starts with RS. Returns JWT_TAMPER_RESULTS. Discovery socket reads only.
ssti_probe@require_allowlist() + target/port pre-gates10 math payloads ({{7*7}}→49 Jinja2/Twig … {{this.constructor…}}→7 Handlebars) × 18 endpoints × 21 params via raw-socket GETs; stops at the tool deadline or first expected in resp hit. Returns SSTI_PROBE_RESULTS + engine or No SSTI detected. Read-only GETs.
graphql_introspect@require_allowlist() + target/port pre-gatesPOSTs the __schema introspection query to 14 endpoints (/graphql, /gql, /api/v1/graphql, /__graphql, …); on a hit prints up to 20 type names, then a 5-query batching test (__typename × 5 → batching verdict). Returns GRAPHQL_INTROSPECT_RESULTS.
race_request@require_allowlist() + target/port/endpoint/concurrent pre-gatesFires concurrent POSTs ({"code":"TEST100","user":"attacker"}) via ThreadPoolExecutor(max 50); counts 200/201 vs other; mixed status codes or >1 success → possible TOCTOU. State-changing POSTs by design. Returns RACE_REQUEST_RESULTS.
timing_oracle@require_allowlist() + target/port pre-gates8 samples each (0.15 s pacing): valid vs invalid user on /api/login, existing vs non-existing email on /api/reset-password; >50 ms mean diff → oracle verdict. Early deadline expiry → Insufficient samples. Returns TIMING_ORACLE_RESULTS.
request_smuggling_probe@require_allowlist() + target/port pre-gatesBaseline Content-Length: 0 POST, then CL.TE / TE.CL / TE.TE (obfuscated TE) malformed probes; flags baseline deltas >200 B or a leaked GPOST/Unrecognized method. Sends intentionally malformed requests (detection only). Returns REQUEST_SMUGGLING_RESULTS.
password_spray@require_allowlist() + target/port pre-gatesSprays one password (never echoed — output shows [redacted]) across 47 usernames (POST /api/login, ~1.5 s pacing to avoid lockout); 200/302 → per-user SUCCESS. Stops at the deadline with a Stopped at tool deadline after N/47 note. Returns PASSWORD_SPRAY_RESULTS (usernames only, no password material).

Implementation note: web.py imports PasswordSpray, JWTTamper, GraphQLIntrospect, RaceRequest, RequestSmuggling, SSTIProbe, TimingOracle from tools.attack_modules.modules.* as the single-source seam, but the registered probe bodies are inline raw-socket implementations in web.py itself.

Config keys

KeyDefaultEffect
adaptive_exploits.enabledtrue (config.yaml:381)false blocks craft_exploit/mutate_exploit with BLOCKED: adaptive_exploits is disabled
adaptive_exploits.max_mutations5 (config.yaml:383)Lineage cap; mutate_on_failure returns None past it → max_mutations_reached block
swarm.enabledtrue (config.yaml:175)false blocks start_autonomous_campaign with BLOCKED: swarm is disabled
autonomous.*merged first into campaign mission_configOpt-in Phase 2 flags (persistence_phase, checkpoint_every, adaptive_replan, max_pivot_depth) reach the orchestrator
opsec.*merged into campaign mission_configBuilds the OpsecManager; absent → disabled profile → pacing no-op
exploit.msf.auto_local_exploit_suggestermerged as msf_auto_lesLets the campaign privesc phase dispatch the advisory follow-up
orchestrator.semantic_memory + ollama + memory.embedding_modelfalse / nomic-embed-textBuilds the orchestrator's SemanticMemoryManager when opted in
agent.* / killchain.* / snapshots.* / replay_simulator.*merged into campaign mission_configRetry caps, kill-chain branch, snapshot-before-destructive, counterfactual toggle reach the campaign
exploit.max_roundsread as campaign max_cyclesBounds the background campaign loop (run_campaign_step forces max_cycles=1)
poc_verification.enabledtrue (config.yaml:350)Runs the inline syntax_check gate inside cve_to_exploit_synth; opt-in — failures never break the synth path
exploit.require_explicit_allowlist / exploit.allowed_targetstrue / [127.0.0.1]Target-IP lock for every @require_allowlist() tool in this family
model routing (models.*, provider blocks)via _get_model_client(config)Planning/adaptive LLM enrichment; None client degrades gracefully (plan saved without AI steps)

Examples

# 1. Plan, inspect, adapt.
create_attack_plan("192.168.1.100", "linux", "CVE-2024-6387,CVE-2021-44228")
get_current_plan("192.168.1.100")
replan("192.168.1.100", "Log4j probe returned no response — service may be patched")

# 2. Autonomous campaign with monitoring.
start_autonomous_campaign("192.168.1.100", "full_compromise", "aggressive")
get_campaign_status("campaign-20260504_120000-abc12345")
run_campaign_step("campaign-20260504_120000-abc12345")  # single-step/debug pacing
stop_campaign("campaign-20260504_120000-abc12345")

# 3. Adaptive exploit generation loop.
craft_exploit("192.168.1.100", "ssh", "OpenSSH 8.9p1", "linux", "RegreSSHion")
mutate_exploit("gen-1712345678-abc12345", "ConnectionResetError: target closed connection")

# 4. CVE synthesis → save → run.
cve_to_exploit_synth("192.168.1.100", "CVE-2021-44228", "http", "")
# write_python_file the TEMPLATE_DISPATCHED script, then run_python_file.

# 5. Identify a hash, then probe the web surface.
hash_crack_identify("5d41402abc4b2a76b9719d911017c592")
jwt_tamper("192.168.1.100")
ssti_probe("192.168.1.100", 80)
graphql_introspect("192.168.1.100", 80)
race_request("192.168.1.100", 80, "/api/redeem", 20)
timing_oracle("192.168.1.100", 80)
request_smuggling_probe("192.168.1.100", 80)
password_spray("192.168.1.100", 80, "Password1")

Source map

  • tools/mcp_tools/modules/planning.pyregister_planning_tools, create_attack_plan, get_current_plan, replan
  • tools/mcp_tools/modules/campaign.pyregister_campaign_tools, start_autonomous_campaign, get_campaign_status, run_campaign_step, stop_campaign, _running_campaign_tasks, _campaign_orchestrators
  • tools/mcp_tools/modules/adaptive.pyregister_adaptive_tools, craft_exploit, mutate_exploit
  • tools/mcp_tools/modules/synthesis.pyregister_synthesis_tools, cve_to_exploit_synth, 18 _render_* CVE-family templates
  • tools/mcp_tools/modules/hash.pyregister_hash_tools, hash_crack_identify, _identify_hash_modes
  • tools/mcp_tools/modules/web.pyregister_web_tools, jwt_tamper, ssti_probe, graphql_introspect, race_request, timing_oracle, request_smuggling_probe, password_spray, shared _check_*/_clamp_timeout/_finish helpers
  • tools/mcp_tools/attack_modules.pyregister_attack_module_tools aggregate, _identify_hash_modes re-export
  • tools/mcp_tools/registry.pyToolContext, _discover_tool_registrars, collect_tools, decorator validation
  • tools/attack_planner.py — plan model, planning/replanning prompts and parsers
  • tools/autonomous_orchestrator.py — campaign runner, AggressionLevel, TaskStatus
  • tools/exploit_mutator.py + tools/payload_crafter.py — adaptive generation and mutation
  • tools/attack_modules/ — pre-packaged module registry stepped by campaigns
  • config.yamladaptive_exploits, swarm, autonomous, opsec, poc_verification, exploit
source: repo docs (build sync)Edit this page on GitHub →