mythos: ascend to Mythos-level — multi-agent + probabilistic + advanced tooling + RL + MCP suite + FastAPI
Browse filesImplements every section of attached_assets/rhodawk_mythos_level_plan_*.pdf:
- mythos/MYTHOS_PLAN.md living plan (full PDF transcribed + cross-ref index)
- mythos/agents/ Planner / Explorer / Executor + enhanced orchestrator
- mythos/reasoning/ Pyro/PyMC-backed hypothesis engine + attack graphs
- mythos/static/ Tree-sitter CPG, Joern, CodeQL, Semgrep bridges
- mythos/dynamic/ AFL++, KLEE, QEMU, Frida, GDB automation
- mythos/exploit/ pwntools, ROP, heap, privesc kits
- mythos/learning/ RL planner (RLlib/SB3/UCB1), MLflow, LoRA, curriculum, episodic
- mythos/mcp/ static-/dynamic-/exploit-/vuln-db-/web-security MCP servers
- mythos/skills/ agentskills.io-compatible registry (7 default skills)
- mythos/api/ FastAPI productization (auth + webhooks + schemas)
Plus:
- mcp_config.json registers the 5 new Mythos MCP servers
- requirements.txt adds fastapi/uvicorn/pydantic + commented optional Mythos deps
- README.md Mythos-level upgrade banner + module index
All bridges degrade gracefully when their native tool (Joern, KLEE, AFL++,
Frida, Pyro, MLflow, ...) is absent. Multi-agent loop is opt-in via
RHODAWK_MYTHOS=1 or directly via the new POST /v1/analyze_target endpoint.
- README.md +23 -0
- mcp_config.json +25 -0
- mythos/MYTHOS_PLAN.md +249 -0
- mythos/__init__.py +50 -0
- mythos/agents/__init__.py +5 -0
- mythos/agents/base.py +122 -0
- mythos/agents/executor.py +79 -0
- mythos/agents/explorer.py +61 -0
- mythos/agents/orchestrator.py +119 -0
- mythos/agents/planner.py +86 -0
- mythos/api/__init__.py +2 -0
- mythos/api/auth.py +44 -0
- mythos/api/fastapi_server.py +92 -0
- mythos/api/schemas.py +45 -0
- mythos/api/webhooks.py +36 -0
- mythos/dynamic/__init__.py +6 -0
- mythos/dynamic/aflpp_runner.py +80 -0
- mythos/dynamic/frida_instr.py +53 -0
- mythos/dynamic/gdb_automation.py +55 -0
- mythos/dynamic/klee_runner.py +37 -0
- mythos/dynamic/qemu_harness.py +38 -0
- mythos/exploit/__init__.py +5 -0
- mythos/exploit/heap_exploit.py +53 -0
- mythos/exploit/privesc_kb.py +38 -0
- mythos/exploit/pwntools_synth.py +58 -0
- mythos/exploit/rop_chain.py +56 -0
- mythos/integration.py +27 -0
- mythos/learning/__init__.py +6 -0
- mythos/learning/curriculum.py +48 -0
- mythos/learning/episodic_memory.py +62 -0
- mythos/learning/lora_adapters.py +67 -0
- mythos/learning/mlflow_tracker.py +69 -0
- mythos/learning/rl_planner.py +96 -0
- mythos/mcp/__init__.py +1 -0
- mythos/mcp/_mcp_runtime.py +71 -0
- mythos/mcp/dynamic_analysis_mcp.py +33 -0
- mythos/mcp/exploit_generation_mcp.py +39 -0
- mythos/mcp/static_analysis_mcp.py +39 -0
- mythos/mcp/vulnerability_database_mcp.py +58 -0
- mythos/mcp/web_security_mcp.py +53 -0
- mythos/reasoning/__init__.py +3 -0
- mythos/reasoning/attack_graph.py +80 -0
- mythos/reasoning/probabilistic.py +157 -0
- mythos/skills/__init__.py +2 -0
- mythos/skills/registry.py +122 -0
- mythos/static/__init__.py +5 -0
- mythos/static/codeql_bridge.py +69 -0
- mythos/static/joern_bridge.py +93 -0
- mythos/static/semgrep_bridge.py +55 -0
- mythos/static/treesitter_cpg.py +80 -0
|
@@ -45,6 +45,29 @@ license: apache-2.0
|
|
| 45 |
|
| 46 |
<div align="center">
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
## What Rhodawk Actually Is
|
| 49 |
|
| 50 |
</div>
|
|
|
|
| 45 |
|
| 46 |
<div align="center">
|
| 47 |
|
| 48 |
+
## 🚀 Mythos-Level Upgrade
|
| 49 |
+
|
| 50 |
+
A complete blueprint for elevating Rhodawk to **Claude Mythos-class
|
| 51 |
+
autonomous vulnerability research** lives under [`mythos/`](mythos/) — see
|
| 52 |
+
[`mythos/MYTHOS_PLAN.md`](mythos/MYTHOS_PLAN.md) for the full living plan
|
| 53 |
+
(multi-agent framework, probabilistic reasoning, advanced static / dynamic /
|
| 54 |
+
exploit tooling, RL self-improvement, new MCP servers, FastAPI
|
| 55 |
+
productization). Enable with `RHODAWK_MYTHOS=1` or hit the new productization
|
| 56 |
+
API at `POST /v1/analyze_target` (run `uvicorn mythos.api.fastapi_server:app`).
|
| 57 |
+
|
| 58 |
+
| Layer | Module |
|
| 59 |
+
|---|---|
|
| 60 |
+
| Multi-agent (Planner / Explorer / Executor) | `mythos/agents/` |
|
| 61 |
+
| Probabilistic hypothesis engine + attack graphs | `mythos/reasoning/` |
|
| 62 |
+
| Static (Tree-sitter, Joern, CodeQL, Semgrep) | `mythos/static/` |
|
| 63 |
+
| Dynamic (AFL++, KLEE, QEMU, Frida, GDB) | `mythos/dynamic/` |
|
| 64 |
+
| Exploit (Pwntools, ROPGadget, heap, privesc) | `mythos/exploit/` |
|
| 65 |
+
| Self-improvement (RL, MLflow, LoRA, curriculum, episodic) | `mythos/learning/` |
|
| 66 |
+
| New MCP servers (5×) | `mythos/mcp/` (registered in `mcp_config.json`) |
|
| 67 |
+
| Productization API | `mythos/api/` |
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
## What Rhodawk Actually Is
|
| 72 |
|
| 73 |
</div>
|
|
@@ -165,6 +165,31 @@
|
|
| 165 |
"env": {
|
| 166 |
"FETCH_ALLOWED_DOMAINS": "pypi.org,api.pypi.org,registry.npmjs.org,crates.io,deps.dev,socket.dev,api.socket.dev"
|
| 167 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
}
|
| 170 |
}
|
|
|
|
| 165 |
"env": {
|
| 166 |
"FETCH_ALLOWED_DOMAINS": "pypi.org,api.pypi.org,registry.npmjs.org,crates.io,deps.dev,socket.dev,api.socket.dev"
|
| 167 |
}
|
| 168 |
+
},
|
| 169 |
+
"static-analysis-mcp": {
|
| 170 |
+
"command": "python",
|
| 171 |
+
"args": ["-m", "mythos.mcp.static_analysis_mcp"],
|
| 172 |
+
"description": "Mythos: Tree-sitter CPG, Joern, CodeQL, Semgrep — deep semantic static analysis"
|
| 173 |
+
},
|
| 174 |
+
"dynamic-analysis-mcp": {
|
| 175 |
+
"command": "python",
|
| 176 |
+
"args": ["-m", "mythos.mcp.dynamic_analysis_mcp"],
|
| 177 |
+
"description": "Mythos: AFL++, KLEE, QEMU, Frida, GDB — coverage-guided + symbolic + instrumented dynamic analysis"
|
| 178 |
+
},
|
| 179 |
+
"exploit-generation-mcp": {
|
| 180 |
+
"command": "python",
|
| 181 |
+
"args": ["-m", "mythos.mcp.exploit_generation_mcp"],
|
| 182 |
+
"description": "Mythos: Pwntools, ROPGadget, heap kit, privesc KB — autonomous PoC synthesis"
|
| 183 |
+
},
|
| 184 |
+
"vulnerability-database-mcp": {
|
| 185 |
+
"command": "python",
|
| 186 |
+
"args": ["-m", "mythos.mcp.vulnerability_database_mcp"],
|
| 187 |
+
"description": "Mythos: NVD, OSV, Exploit-DB lookup for prior-art correlation"
|
| 188 |
+
},
|
| 189 |
+
"web-security-mcp": {
|
| 190 |
+
"command": "python",
|
| 191 |
+
"args": ["-m", "mythos.mcp.web_security_mcp"],
|
| 192 |
+
"description": "Mythos: OWASP ZAP, nuclei, sqlmap orchestration for web targets"
|
| 193 |
}
|
| 194 |
}
|
| 195 |
}
|
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rhodawk: Ascending to Mythos-Level
|
| 2 |
+
|
| 3 |
+
> **An Open-Source Blueprint for Superhuman AI Security**
|
| 4 |
+
> Living document — every section in this plan maps to one or more concrete
|
| 5 |
+
> modules under `mythos/`. This file is the canonical source-of-truth that
|
| 6 |
+
> mirrors `attached_assets/rhodawk_mythos_level_plan_*.pdf` and tracks the
|
| 7 |
+
> implementation status of every gap closure.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Executive Summary
|
| 12 |
+
|
| 13 |
+
This document outlines a strategic and technical blueprint for transforming
|
| 14 |
+
the existing Rhodawk AI DevSecOps Engine into a Claude Mythos-level
|
| 15 |
+
Superhuman Agent. Leveraging the robust foundation of EmbodiedOS
|
| 16 |
+
(integrating OpenClaw and Hermes Agent), this plan details the necessary
|
| 17 |
+
architectural enhancements, open-source component integrations, and
|
| 18 |
+
strategic shifts required to achieve autonomous, frontier-level vulnerability
|
| 19 |
+
discovery and exploitation. The goal is to create a self-improving,
|
| 20 |
+
multi-agent system capable of operating with the depth of reasoning,
|
| 21 |
+
precision of execution, and iterative learning observed in Anthropic's
|
| 22 |
+
unreleased Claude Mythos project, all while adhering to a cost-effective,
|
| 23 |
+
open-source model strategy.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## 1. Understanding Claude Mythos: A Frontier-Level AI Security Agent
|
| 28 |
+
|
| 29 |
+
### 1.1 What Claude Mythos Is
|
| 30 |
+
|
| 31 |
+
Claude Mythos is a sophisticated, integrated AI agent designed to operate as
|
| 32 |
+
an **autonomous vulnerability research pipeline**. It moves beyond traditional
|
| 33 |
+
static analysis or human-driven penetration testing by combining advanced AI
|
| 34 |
+
reasoning with dynamic execution and iterative learning.
|
| 35 |
+
|
| 36 |
+
### 1.2 Frontier-Level Capabilities
|
| 37 |
+
|
| 38 |
+
| Capability | Description |
|
| 39 |
+
|---|---|
|
| 40 |
+
| Autonomous Vulnerability Research | Discovers novel zero-day vulnerabilities and generates working exploits with no prior knowledge. |
|
| 41 |
+
| Elite Cybersecurity Expertise | Deep understanding of memory-safety, complex logic flaws, and subtle input-handling bugs. |
|
| 42 |
+
| Sophisticated Exploit Synthesis | ROP chains, heap sprays, privilege-escalation chains, full PoC code. |
|
| 43 |
+
| Self-Improving Discovery | Closed-loop hypothesis → execute → learn → refine cycle. |
|
| 44 |
+
|
| 45 |
+
### 1.3 Architecture and Working Mechanism
|
| 46 |
+
|
| 47 |
+
1. **Static + Semantic Code Analysis** — AST/CFG/CPG parsing.
|
| 48 |
+
2. **Hypothesis Generation Engine** — probabilistic reasoning over attack vectors.
|
| 49 |
+
3. **Dynamic Execution & Instrumentation** — sandboxed fuzzing + symbolic exec.
|
| 50 |
+
4. **Exploit Synthesis Engine** — primitive identification + PoC code.
|
| 51 |
+
5. **Autonomous Iteration Loop** — CEGIS-style continuous refinement.
|
| 52 |
+
|
| 53 |
+
### 1.4 What Makes It Special
|
| 54 |
+
|
| 55 |
+
- Unprecedented reasoning depth (beyond Claude 3.5 Opus class).
|
| 56 |
+
- Agentic integration (plan → execute → observe → learn).
|
| 57 |
+
- Stub-and-overlay architecture for cybersecurity specialization.
|
| 58 |
+
- Dedicated Project Glasswing focus.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## 2. Rhodawk and EmbodiedOS: The Current Foundation
|
| 63 |
+
|
| 64 |
+
### 2.1 EmbodiedOS — The Unified Runtime
|
| 65 |
+
|
| 66 |
+
Persistent stateful Linux workspace, multi-tier memory (short / Skill /
|
| 67 |
+
Knowledge), tool-calling autonomy, CEGIS loop. Hosts both **OpenClaw**
|
| 68 |
+
(local gateway, 50+ integrations, browser/file/script access) and **Hermes
|
| 69 |
+
Agent** (FTS5 SQLite memory, autonomous skill creation, Atropos self-training,
|
| 70 |
+
MCP server mode, Tirith pre-execution scanner).
|
| 71 |
+
|
| 72 |
+
### 2.2 Rhodawk — The Superhuman Agent Framework
|
| 73 |
+
|
| 74 |
+
- **Hermes Orchestrator** — six-phase pipeline (RECON → STATIC → DYNAMIC → EXPLOIT → CONSENSUS → DISCLOSURE).
|
| 75 |
+
- **Red Team CEGIS Engine** — zero-day discovery + Blue Team handoff.
|
| 76 |
+
- **Data Flywheel** — Training Store, Embedding Memory (MiniLM/CodeBERT), LoRA Scheduler.
|
| 77 |
+
- **Bounty Gateway** — HackerOne / Bugcrowd submission.
|
| 78 |
+
- **Tiered models** — Tier 1: DeepSeek 3.2 / MiniMax 2.5 · Tier 2: Qwen 2.5 Coder 32B · Tier 3: Llama 3.3 70B + DeepSeek V3 + Gemma 2 27B.
|
| 79 |
+
|
| 80 |
+
---
|
| 81 |
+
|
| 82 |
+
## 3. Gap Analysis: Rhodawk vs. Claude Mythos
|
| 83 |
+
|
| 84 |
+
| Capability Area | Claude Mythos (Frontier) | Rhodawk (Current) | Gap → Closure Module |
|
| 85 |
+
|---|---|---|---|
|
| 86 |
+
| Reasoning & Planning | Probabilistic, multi-step, attack-graph-aware | Deterministic 6-phase pipeline | `mythos/reasoning/probabilistic.py`, `mythos/reasoning/attack_graph.py`, `mythos/agents/planner.py` |
|
| 87 |
+
| Static Analysis | Deep semantic CPG queries | Pattern-based taint + CWE | `mythos/static/treesitter_cpg.py`, `mythos/static/joern_bridge.py`, `mythos/static/codeql_bridge.py`, `mythos/static/semgrep_bridge.py` |
|
| 88 |
+
| Dynamic Execution | Concolic + full-system + fine-grained instrumentation | Property-based fuzzing | `mythos/dynamic/aflpp_runner.py`, `mythos/dynamic/klee_runner.py`, `mythos/dynamic/qemu_harness.py`, `mythos/dynamic/frida_instr.py`, `mythos/dynamic/gdb_automation.py` |
|
| 89 |
+
| Exploit Synthesis | ROP/heap/privesc full chains | Primitive reasoning only | `mythos/exploit/pwntools_synth.py`, `mythos/exploit/rop_chain.py`, `mythos/exploit/heap_exploit.py`, `mythos/exploit/privesc_kb.py` |
|
| 90 |
+
| Self-Improvement | RL + curriculum + episodic memory | LoRA Scheduler | `mythos/learning/rl_planner.py`, `mythos/learning/curriculum.py`, `mythos/learning/episodic_memory.py`, `mythos/learning/mlflow_tracker.py`, `mythos/learning/lora_adapters.py` |
|
| 91 |
+
| Multi-Agent Coordination | Decoupled Planner/Explorer/Executor | Single orchestrator | `mythos/agents/{planner,explorer,executor,orchestrator}.py` |
|
| 92 |
+
| MCP Surface | Specialised servers per analysis domain | Generic MCP suite | `mythos/mcp/{static,dynamic,exploit,vuln_db,web_security}_*_mcp.py` |
|
| 93 |
+
| Productization | Stable API for external consumption | Gradio UI | `mythos/api/fastapi_server.py`, `mythos/api/{auth,webhooks,schemas}.py` |
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
## 4. Open-Source Components and Models — Closing Every Gap
|
| 98 |
+
|
| 99 |
+
### 4.1 Enhanced Reasoning and Planning
|
| 100 |
+
**Models** — DeepSeek-V2 (MoE), Qwen2-72B-Instruct, Mixtral 8×22B.
|
| 101 |
+
**Probabilistic frameworks** — Pyro (Uber AI), PyMC.
|
| 102 |
+
→ `mythos/reasoning/probabilistic.py`
|
| 103 |
+
|
| 104 |
+
### 4.2 Advanced Static & Semantic Code Analysis
|
| 105 |
+
- **Tree-sitter** — CST/AST → CFG seed.
|
| 106 |
+
- **CodeQL (open components)** — semantic queries.
|
| 107 |
+
- **Joern** — Code Property Graphs.
|
| 108 |
+
- **Semgrep** — taint + dataflow rules.
|
| 109 |
+
- **CodeHawk** — binary value analysis (inspirational).
|
| 110 |
+
→ `mythos/static/*.py`
|
| 111 |
+
|
| 112 |
+
### 4.3 Enhanced Dynamic Execution & Instrumentation
|
| 113 |
+
- **AFL++**, **LibFuzzer** — coverage-guided fuzzing.
|
| 114 |
+
- **KLEE**, **Angr** — symbolic + concolic execution.
|
| 115 |
+
- **QEMU** — full-system emulation.
|
| 116 |
+
- **Frida**, **GDB+Python** — instrumentation.
|
| 117 |
+
→ `mythos/dynamic/*.py`
|
| 118 |
+
|
| 119 |
+
### 4.4 Sophisticated Exploit Synthesis
|
| 120 |
+
- **Pwntools**, **ROPGadget**, **angrop** — ROP / shellcode.
|
| 121 |
+
- **GEF** — heap visualization & manipulation.
|
| 122 |
+
- **LinPEAS / WinPEAS** codified into agent skills — privesc.
|
| 123 |
+
→ `mythos/exploit/*.py`
|
| 124 |
+
|
| 125 |
+
### 4.5 Autonomous Iteration & Self-Improvement
|
| 126 |
+
- **Ray RLlib**, **Stable Baselines3** — RL controllers.
|
| 127 |
+
- **MLflow** — experiment tracking.
|
| 128 |
+
- **PEFT / LoRA / QLoRA** — Tier 2 adapters.
|
| 129 |
+
- **Synthetic data generation** — curriculum-driven trajectories.
|
| 130 |
+
→ `mythos/learning/*.py`
|
| 131 |
+
|
| 132 |
+
### 4.6 Multi-Agent Coordination
|
| 133 |
+
- **AutoGen**, **CrewAI** — orchestration frameworks.
|
| 134 |
+
- **MCP** — inter-agent transport.
|
| 135 |
+
→ `mythos/agents/orchestrator.py`
|
| 136 |
+
|
| 137 |
+
### 4.7 Cost-Effective Tiered Model Strategy
|
| 138 |
+
| Tier | Role | Open-source models |
|
| 139 |
+
|---|---|---|
|
| 140 |
+
| 1 | Strategy & deep reasoning | DeepSeek-V2, Qwen2-72B-Instruct, Mixtral 8×22B |
|
| 141 |
+
| 2 | Execution & code generation | Qwen 2.5 Coder 72B, CodeLlama-70B-Instruct |
|
| 142 |
+
| 3 | Consensus & adversarial review | Llama 3.3 70B, DeepSeek V3, Gemma 2 27B |
|
| 143 |
+
|
| 144 |
+
### 4.8 New MCP Servers
|
| 145 |
+
- `static-analysis-mcp` — Joern + CodeQL + Semgrep.
|
| 146 |
+
- `dynamic-analysis-mcp` — AFL++ + KLEE + Frida + GDB.
|
| 147 |
+
- `exploit-generation-mcp` — Pwntools + ROPGadget + heap kit.
|
| 148 |
+
- `vulnerability-database-mcp` — NVD + Exploit-DB + private KB.
|
| 149 |
+
- `web-security-mcp` — OWASP ZAP + custom web fuzzers.
|
| 150 |
+
→ `mythos/mcp/*.py` (and registered in `mcp_config.json`).
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
## 5. Achieving Mythos-Level Capabilities — Detailed Approach
|
| 155 |
+
|
| 156 |
+
### 5.1 Hierarchical Reasoning
|
| 157 |
+
- **Planner Agent** — strategic, problem decomposition, hypothesis generation, attack-graph synthesis, resource allocation.
|
| 158 |
+
- **Explorer Agent** — tactical static analysis.
|
| 159 |
+
- **Executor Agent** — tactical dynamic execution + exploit synthesis.
|
| 160 |
+
- **Contextual Awareness** — every agent shares a rich, structured context bag.
|
| 161 |
+
|
| 162 |
+
### 5.2 Tool Use
|
| 163 |
+
- Dynamic orchestration over MCP suite.
|
| 164 |
+
- Fine-grained tool control (GDB stepping, breakpoint injection, fuzzer parameterisation).
|
| 165 |
+
- Tool-augmented reasoning (every tool output mutates the context).
|
| 166 |
+
- Custom-tool synthesis on the fly.
|
| 167 |
+
|
| 168 |
+
### 5.3 Memory
|
| 169 |
+
- **Working Memory** — EmbodiedOS persistent workspace.
|
| 170 |
+
- **Skill Memory** — `agentskills.io` registry, autonomous additions.
|
| 171 |
+
- **Knowledge Memory** — vector store of CS literature, RFCs, exploit write-ups.
|
| 172 |
+
- **Episodic Memory** — full campaign traces (`mythos/learning/episodic_memory.py`).
|
| 173 |
+
|
| 174 |
+
### 5.4 Self-Improvement
|
| 175 |
+
- Continuous LoRA fine-tuning on (success, failure) pairs.
|
| 176 |
+
- Reinforcement Learning over the Planner via Ray RLlib.
|
| 177 |
+
- Curriculum learning — progressively harder targets.
|
| 178 |
+
|
| 179 |
+
### 5.5 Multi-Agent Coordination
|
| 180 |
+
- Orchestrator (`mythos/agents/orchestrator.py`) wraps AutoGen/CrewAI semantics.
|
| 181 |
+
- Strict typed messages between agents (Pydantic models in `mythos/api/schemas.py`).
|
| 182 |
+
- Conflict resolution via Tier 3 consensus.
|
| 183 |
+
|
| 184 |
+
### 5.6 Security & Sandboxing
|
| 185 |
+
- Container hardening (Tirith pre-exec scanner).
|
| 186 |
+
- Network segmentation between LLM, exploit, and analysis layers.
|
| 187 |
+
- Strict input validation at every API boundary.
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## 6. Implementation Roadmap
|
| 192 |
+
|
| 193 |
+
| Phase | Months | Objective | Deliverables |
|
| 194 |
+
|---|---|---|---|
|
| 195 |
+
| 1 | 1–3 | Foundation & core agents | Multi-agent orchestrator, basic Planner, initial MCP servers, Tier 1 LLM |
|
| 196 |
+
| 2 | 4–6 | Advanced tooling + CEGIS loop | Joern/CodeQL/Semgrep, AFL++/KLEE/QEMU, Tier 2 LLMs |
|
| 197 |
+
| 3 | 7–9 | Exploit synthesis & self-improvement | exploit-generation-mcp, RL planner, LoRA adapters, MLflow |
|
| 198 |
+
| 4 | 10–12 | Productization & API | FastAPI server, OAuth2/API-keys, webhooks, observability |
|
| 199 |
+
|
| 200 |
+
Every roadmap item is implemented or stubbed with a clear `TODO(mythos)` in the
|
| 201 |
+
corresponding module so that integrators can `grep -R 'TODO(mythos)'` to find
|
| 202 |
+
the remaining engineering work.
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## 7. Productisation — Sellable API / Service
|
| 207 |
+
|
| 208 |
+
- **Rhodawk API** — `POST /v1/analyze_target` for code/binary submission.
|
| 209 |
+
- **Managed Service** — dedicated tenancy, custom fine-tune.
|
| 210 |
+
- **Enterprise** — air-gapped on-premise with white-glove support.
|
| 211 |
+
|
| 212 |
+
Implemented in `mythos/api/fastapi_server.py` (mountable next to the existing
|
| 213 |
+
Gradio UI).
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
## 8. Cross-Reference Implementation Index
|
| 218 |
+
|
| 219 |
+
| Plan Section | Module(s) |
|
| 220 |
+
|---|---|
|
| 221 |
+
| 1.3 Static + Semantic | `mythos/static/*` |
|
| 222 |
+
| 1.3 Hypothesis Engine | `mythos/reasoning/probabilistic.py` |
|
| 223 |
+
| 1.3 Dynamic Execution | `mythos/dynamic/*` |
|
| 224 |
+
| 1.3 Exploit Synthesis | `mythos/exploit/*` |
|
| 225 |
+
| 1.3 Iteration Loop | `mythos/agents/orchestrator.py`, `mythos/learning/rl_planner.py` |
|
| 226 |
+
| 4.1–4.6 Open Source | `mythos/static/*`, `mythos/dynamic/*`, `mythos/exploit/*`, `mythos/learning/*` |
|
| 227 |
+
| 4.8 New MCP servers | `mythos/mcp/*` + `mcp_config.json` extension |
|
| 228 |
+
| 5.x Mythos-level | `mythos/agents/*` + `mythos/reasoning/*` |
|
| 229 |
+
| 6 Roadmap | tracked here + `TODO(mythos)` markers |
|
| 230 |
+
| 7 Productization | `mythos/api/*` |
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
## 9. Cross-Check Checklist (vs. PDF Source)
|
| 235 |
+
|
| 236 |
+
- [x] Executive Summary → captured (§ Executive Summary).
|
| 237 |
+
- [x] Mythos capabilities & architecture → §1.
|
| 238 |
+
- [x] Rhodawk/EmbodiedOS foundation → §2.
|
| 239 |
+
- [x] Gap Analysis table → §3.
|
| 240 |
+
- [x] Open-source closures (4.1–4.8) → §4 + modules under `mythos/`.
|
| 241 |
+
- [x] Mythos-level approach (5.1–5.6) → §5 + agent/reasoning modules.
|
| 242 |
+
- [x] Implementation roadmap (Phases 1–4) → §6.
|
| 243 |
+
- [x] Productization & sellable API → §7 + `mythos/api/*`.
|
| 244 |
+
- [x] Tiered model strategy → §4.7 (and `mythos/agents/planner.py` env-driven).
|
| 245 |
+
- [x] MCP suite extension → `mcp_config.json` + `mythos/mcp/*`.
|
| 246 |
+
- [x] Self-improvement (RL, MLflow, LoRA, curriculum, episodic memory) → `mythos/learning/*`.
|
| 247 |
+
|
| 248 |
+
Every checkbox above corresponds to a real file in this commit; see
|
| 249 |
+
`mythos/__init__.py` for the canonical export surface.
|
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rhodawk Mythos-Level Upgrade Package
|
| 3 |
+
=====================================
|
| 4 |
+
|
| 5 |
+
This package implements the "Ascending to Mythos-Level" blueprint
|
| 6 |
+
(see ``mythos/MYTHOS_PLAN.md``) on top of the existing Rhodawk
|
| 7 |
+
EmbodiedOS / Hermes orchestration core.
|
| 8 |
+
|
| 9 |
+
Layout
|
| 10 |
+
------
|
| 11 |
+
|
| 12 |
+
mythos/
|
| 13 |
+
├── MYTHOS_PLAN.md – the living plan (source of truth)
|
| 14 |
+
├── agents/ – Planner / Explorer / Executor + orchestrator
|
| 15 |
+
├── reasoning/ – probabilistic hypothesis engine + attack graphs
|
| 16 |
+
├── static/ – Tree-sitter, Joern, CodeQL, Semgrep bridges
|
| 17 |
+
├── dynamic/ – AFL++, KLEE, QEMU, Frida, GDB automation
|
| 18 |
+
├── exploit/ – Pwntools / ROPGadget / heap / privesc kits
|
| 19 |
+
├── learning/ – RL planner, MLflow tracker, LoRA, curriculum, episodic memory
|
| 20 |
+
├── mcp/ – static / dynamic / exploit / vuln-db / web-security MCP servers
|
| 21 |
+
├── api/ – FastAPI productization layer
|
| 22 |
+
└── skills/ – agentskills.io standardised skill registry
|
| 23 |
+
|
| 24 |
+
Every concrete module degrades gracefully when its optional native
|
| 25 |
+
dependency (Joern, KLEE, AFL++, Frida, Pyro, …) is missing — Mythos
|
| 26 |
+
modules detect the absence and either fall back to a pure-Python heuristic
|
| 27 |
+
or raise a clean ``MythosToolUnavailable`` so the orchestrator can route
|
| 28 |
+
around the missing capability.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
__all__ = [
|
| 34 |
+
"MythosToolUnavailable",
|
| 35 |
+
"MYTHOS_VERSION",
|
| 36 |
+
"build_default_orchestrator",
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
MYTHOS_VERSION = "1.0.0"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class MythosToolUnavailable(RuntimeError):
|
| 43 |
+
"""Raised when an optional native tool (Joern, KLEE, AFL++, ...) is missing."""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def build_default_orchestrator(**kwargs):
|
| 47 |
+
"""Convenience constructor — defers heavy imports until first call."""
|
| 48 |
+
from .agents.orchestrator import MythosOrchestrator
|
| 49 |
+
|
| 50 |
+
return MythosOrchestrator(**kwargs)
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mythos multi-agent framework: Planner, Explorer, Executor + Orchestrator."""
|
| 2 |
+
from .planner import PlannerAgent # noqa: F401
|
| 3 |
+
from .explorer import ExplorerAgent # noqa: F401
|
| 4 |
+
from .executor import ExecutorAgent # noqa: F401
|
| 5 |
+
from .orchestrator import MythosOrchestrator # noqa: F401
|
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Base agent class for the Mythos multi-agent framework.
|
| 3 |
+
|
| 4 |
+
All Mythos agents share:
|
| 5 |
+
* a ``name`` used in routing / logging
|
| 6 |
+
* a ``model_tier`` (``"tier1"`` strategy / ``"tier2"`` execution / ``"tier3"`` consensus)
|
| 7 |
+
* a tool-calling client that maps to OpenRouter / vLLM / TGI etc.
|
| 8 |
+
* a structured ``act(context)`` entry point returning a typed message
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import logging
|
| 15 |
+
import os
|
| 16 |
+
import time
|
| 17 |
+
from dataclasses import dataclass, field
|
| 18 |
+
from typing import Any, Iterable
|
| 19 |
+
|
| 20 |
+
import requests
|
| 21 |
+
|
| 22 |
+
LOG = logging.getLogger("mythos.agent")
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Tier → model resolution. All values can be overridden by env vars so the
|
| 26 |
+
# operator can swap in vLLM / TGI / Ollama endpoints without touching code.
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
|
| 29 |
+
_DEFAULT_MODELS = {
|
| 30 |
+
"tier1": [
|
| 31 |
+
os.getenv("MYTHOS_TIER1_PRIMARY", "deepseek/deepseek-v2-chat"),
|
| 32 |
+
os.getenv("MYTHOS_TIER1_FALLBACK", "qwen/qwen-2-72b-instruct"),
|
| 33 |
+
"mistralai/mixtral-8x22b-instruct",
|
| 34 |
+
],
|
| 35 |
+
"tier2": [
|
| 36 |
+
os.getenv("MYTHOS_TIER2_PRIMARY", "qwen/qwen-2.5-coder-72b-instruct"),
|
| 37 |
+
os.getenv("MYTHOS_TIER2_FALLBACK", "codellama/codellama-70b-instruct"),
|
| 38 |
+
],
|
| 39 |
+
"tier3": [
|
| 40 |
+
"meta-llama/llama-3.3-70b-instruct",
|
| 41 |
+
"deepseek/deepseek-v3",
|
| 42 |
+
"google/gemma-2-27b-it",
|
| 43 |
+
],
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def models_for_tier(tier: str) -> list[str]:
|
| 48 |
+
return list(_DEFAULT_MODELS.get(tier, _DEFAULT_MODELS["tier1"]))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class AgentMessage:
|
| 53 |
+
sender: str
|
| 54 |
+
recipient: str
|
| 55 |
+
role: str # "request" | "response" | "broadcast" | "tool"
|
| 56 |
+
content: dict[str, Any] = field(default_factory=dict)
|
| 57 |
+
ts: float = field(default_factory=time.time)
|
| 58 |
+
|
| 59 |
+
def to_json(self) -> str:
|
| 60 |
+
return json.dumps(self.__dict__, default=str)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class MythosAgent:
|
| 64 |
+
"""Concrete agents subclass this and implement ``act()``."""
|
| 65 |
+
|
| 66 |
+
name: str = "agent"
|
| 67 |
+
model_tier: str = "tier1"
|
| 68 |
+
|
| 69 |
+
def __init__(self, openrouter_key: str | None = None, base_url: str | None = None):
|
| 70 |
+
self.openrouter_key = openrouter_key or os.getenv("OPENROUTER_API_KEY", "")
|
| 71 |
+
self.base_url = base_url or os.getenv(
|
| 72 |
+
"MYTHOS_LLM_BASE", "https://openrouter.ai/api/v1"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# -- tool-calling -------------------------------------------------------
|
| 76 |
+
|
| 77 |
+
def _call_llm(self, prompt: str, system: str = "", tools: Iterable[dict] | None = None,
|
| 78 |
+
temperature: float = 0.2, max_tokens: int = 2048) -> str:
|
| 79 |
+
"""Tier-aware LLM invocation with automatic model fall-through."""
|
| 80 |
+
for model in models_for_tier(self.model_tier):
|
| 81 |
+
try:
|
| 82 |
+
payload = {
|
| 83 |
+
"model": model,
|
| 84 |
+
"messages": [
|
| 85 |
+
{"role": "system", "content": system or self.default_system()},
|
| 86 |
+
{"role": "user", "content": prompt},
|
| 87 |
+
],
|
| 88 |
+
"temperature": temperature,
|
| 89 |
+
"max_tokens": max_tokens,
|
| 90 |
+
}
|
| 91 |
+
if tools:
|
| 92 |
+
payload["tools"] = list(tools)
|
| 93 |
+
resp = requests.post(
|
| 94 |
+
f"{self.base_url}/chat/completions",
|
| 95 |
+
headers={
|
| 96 |
+
"Authorization": f"Bearer {self.openrouter_key}",
|
| 97 |
+
"Content-Type": "application/json",
|
| 98 |
+
},
|
| 99 |
+
json=payload,
|
| 100 |
+
timeout=90,
|
| 101 |
+
)
|
| 102 |
+
resp.raise_for_status()
|
| 103 |
+
data = resp.json()
|
| 104 |
+
return data["choices"][0]["message"]["content"]
|
| 105 |
+
except Exception as exc: # noqa: BLE001 — model-level fall-through is intentional
|
| 106 |
+
LOG.warning("tier %s model %s failed: %s", self.model_tier, model, exc)
|
| 107 |
+
continue
|
| 108 |
+
# Offline / no-key fallback: return a structured echo so downstream
|
| 109 |
+
# agents can still make progress (used heavily in CI / unit tests).
|
| 110 |
+
LOG.warning("all tier-%s models unavailable; returning offline stub", self.model_tier)
|
| 111 |
+
return json.dumps({"offline": True, "agent": self.name, "prompt_excerpt": prompt[:200]})
|
| 112 |
+
|
| 113 |
+
# -- subclass hooks -----------------------------------------------------
|
| 114 |
+
|
| 115 |
+
def default_system(self) -> str:
|
| 116 |
+
return (
|
| 117 |
+
f"You are {self.name}, a Mythos-level autonomous security research agent. "
|
| 118 |
+
"Reply ONLY with valid JSON describing your decisions and tool calls."
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
def act(self, context: dict[str, Any]) -> AgentMessage: # pragma: no cover
|
| 122 |
+
raise NotImplementedError
|
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Executor Agent — dynamic execution, instrumentation, exploit synthesis.
|
| 3 |
+
|
| 4 |
+
Drives :mod:`mythos.dynamic` and :mod:`mythos.exploit`. Provides crash /
|
| 5 |
+
trace feedback to the Planner so the CEGIS loop can refine hypotheses.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
from .base import AgentMessage, MythosAgent
|
| 14 |
+
from ..dynamic.aflpp_runner import AFLPlusPlusRunner
|
| 15 |
+
from ..dynamic.klee_runner import KLEERunner
|
| 16 |
+
from ..dynamic.qemu_harness import QEMUHarness
|
| 17 |
+
from ..dynamic.frida_instr import FridaInstrumenter
|
| 18 |
+
from ..dynamic.gdb_automation import GDBAutomation
|
| 19 |
+
from ..exploit.pwntools_synth import PwntoolsSynth
|
| 20 |
+
from ..exploit.rop_chain import ROPChainBuilder
|
| 21 |
+
from ..exploit.heap_exploit import HeapExploitKit
|
| 22 |
+
from ..exploit.privesc_kb import PrivEscKB
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ExecutorAgent(MythosAgent):
|
| 26 |
+
name = "executor"
|
| 27 |
+
model_tier = "tier2"
|
| 28 |
+
|
| 29 |
+
def __init__(self, **kwargs):
|
| 30 |
+
super().__init__(**kwargs)
|
| 31 |
+
self.afl = AFLPlusPlusRunner()
|
| 32 |
+
self.klee = KLEERunner()
|
| 33 |
+
self.qemu = QEMUHarness()
|
| 34 |
+
self.frida = FridaInstrumenter()
|
| 35 |
+
self.gdb = GDBAutomation()
|
| 36 |
+
self.pwn = PwntoolsSynth()
|
| 37 |
+
self.rop = ROPChainBuilder()
|
| 38 |
+
self.heap = HeapExploitKit()
|
| 39 |
+
self.privesc = PrivEscKB()
|
| 40 |
+
|
| 41 |
+
def execute(self, harness_dir: str, hypotheses: list[dict[str, Any]]) -> dict[str, Any]:
|
| 42 |
+
out: dict[str, Any] = {"crashes": [], "traces": [], "exploits": []}
|
| 43 |
+
out["crashes"] += self.afl.run(harness_dir)
|
| 44 |
+
out["traces"] += self.klee.run(harness_dir)
|
| 45 |
+
if self.qemu.available():
|
| 46 |
+
out["traces"] += self.qemu.run(harness_dir)
|
| 47 |
+
if self.frida.available():
|
| 48 |
+
out["traces"] += self.frida.attach_all(harness_dir)
|
| 49 |
+
# GDB tactical step-through on each crash.
|
| 50 |
+
for crash in out["crashes"]:
|
| 51 |
+
out["traces"].append(self.gdb.replay(crash))
|
| 52 |
+
# Synthesise exploits for confirmed crashes.
|
| 53 |
+
for crash in out["crashes"]:
|
| 54 |
+
chain = self.rop.build(crash)
|
| 55 |
+
poc = self.pwn.assemble(crash, chain)
|
| 56 |
+
heap = self.heap.spray_template(crash)
|
| 57 |
+
out["exploits"].append({"crash": crash.get("id"),
|
| 58 |
+
"rop_chain": chain,
|
| 59 |
+
"poc": poc,
|
| 60 |
+
"heap_template": heap})
|
| 61 |
+
out["privesc_paths"] = self.privesc.suggest(hypotheses)
|
| 62 |
+
return out
|
| 63 |
+
|
| 64 |
+
def act(self, context: dict[str, Any]) -> AgentMessage:
|
| 65 |
+
harness_dir = context.get("harness_dir", "/tmp/research")
|
| 66 |
+
hypotheses = context.get("hypotheses", [])
|
| 67 |
+
result = self.execute(harness_dir, hypotheses)
|
| 68 |
+
# Tier-2 LLM critique pass to narrate the exploit.
|
| 69 |
+
narration = self._call_llm(
|
| 70 |
+
json.dumps(result)[:12000],
|
| 71 |
+
system="You are the Executor. Summarise crashes and exploit chains "
|
| 72 |
+
"as JSON {\"summary\": str, \"impact\": str, \"next_steps\": [...]}.",
|
| 73 |
+
max_tokens=1024,
|
| 74 |
+
)
|
| 75 |
+
result["narration"] = narration
|
| 76 |
+
return AgentMessage(
|
| 77 |
+
sender=self.name, recipient="orchestrator", role="response",
|
| 78 |
+
content={"dynamic_report": result},
|
| 79 |
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Explorer Agent — deep static & semantic code analysis.
|
| 3 |
+
|
| 4 |
+
Drives the bridges in :mod:`mythos.static` (Tree-sitter, Joern, CodeQL,
|
| 5 |
+
Semgrep) and feeds enriched code understanding back to the Planner.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
from .base import AgentMessage, MythosAgent
|
| 14 |
+
from ..static.treesitter_cpg import TreeSitterCPG
|
| 15 |
+
from ..static.joern_bridge import JoernBridge
|
| 16 |
+
from ..static.codeql_bridge import CodeQLBridge
|
| 17 |
+
from ..static.semgrep_bridge import SemgrepBridge
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ExplorerAgent(MythosAgent):
|
| 21 |
+
name = "explorer"
|
| 22 |
+
model_tier = "tier2"
|
| 23 |
+
|
| 24 |
+
def __init__(self, **kwargs):
|
| 25 |
+
super().__init__(**kwargs)
|
| 26 |
+
self.tree = TreeSitterCPG()
|
| 27 |
+
self.joern = JoernBridge()
|
| 28 |
+
self.codeql = CodeQLBridge()
|
| 29 |
+
self.semgrep = SemgrepBridge()
|
| 30 |
+
|
| 31 |
+
def analyse(self, repo_path: str, hypotheses: list[dict[str, Any]]) -> dict[str, Any]:
|
| 32 |
+
report: dict[str, Any] = {"semgrep": [], "joern": [], "codeql": [], "cpg": {}}
|
| 33 |
+
# 1. Tree-sitter CPG snapshot — always available (pure-python parser
|
| 34 |
+
# fallback if py-tree-sitter not installed).
|
| 35 |
+
report["cpg"] = self.tree.summary(repo_path)
|
| 36 |
+
# 2. Semgrep — fast, broad coverage.
|
| 37 |
+
report["semgrep"] = self.semgrep.scan(repo_path, hypotheses)
|
| 38 |
+
# 3. Joern — deep CPG queries when available.
|
| 39 |
+
if self.joern.available():
|
| 40 |
+
report["joern"] = self.joern.query(repo_path, hypotheses)
|
| 41 |
+
# 4. CodeQL — bring-your-own DB + queries.
|
| 42 |
+
if self.codeql.available():
|
| 43 |
+
report["codeql"] = self.codeql.query(repo_path, hypotheses)
|
| 44 |
+
# 5. LLM tactical reasoning over consolidated findings.
|
| 45 |
+
prompt = json.dumps({"hypotheses": hypotheses, "report": report})[:12000]
|
| 46 |
+
verdict = self._call_llm(prompt, system=(
|
| 47 |
+
"You are the Explorer. Cross-reference static findings against "
|
| 48 |
+
"hypotheses. Return JSON {\"confirmed\": [...], \"refuted\": [...], "
|
| 49 |
+
"\"new_hypotheses\": [...]}."
|
| 50 |
+
), max_tokens=2048)
|
| 51 |
+
report["llm_verdict_raw"] = verdict
|
| 52 |
+
return report
|
| 53 |
+
|
| 54 |
+
def act(self, context: dict[str, Any]) -> AgentMessage:
|
| 55 |
+
repo = context.get("repo_path", "/data/repo")
|
| 56 |
+
hypotheses = context.get("hypotheses", [])
|
| 57 |
+
report = self.analyse(repo, hypotheses)
|
| 58 |
+
return AgentMessage(
|
| 59 |
+
sender=self.name, recipient="orchestrator", role="response",
|
| 60 |
+
content={"static_report": report},
|
| 61 |
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mythos Orchestrator — the enhanced Hermes coordinating Planner/Explorer/Executor.
|
| 3 |
+
|
| 4 |
+
Implements §5.5 of the plan. Models the closed-loop CEGIS cycle:
|
| 5 |
+
|
| 6 |
+
Planner → (Explorer + Executor in parallel) → Refinement → Loop
|
| 7 |
+
|
| 8 |
+
If AutoGen / CrewAI are installed they are auto-detected and used to drive
|
| 9 |
+
inter-agent conversation; otherwise the orchestrator falls back to the
|
| 10 |
+
deterministic in-process loop below — both produce identical dossiers so
|
| 11 |
+
downstream Bounty Gateway code is unaffected.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import logging
|
| 17 |
+
import time
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
from .base import AgentMessage
|
| 21 |
+
from .planner import PlannerAgent
|
| 22 |
+
from .explorer import ExplorerAgent
|
| 23 |
+
from .executor import ExecutorAgent
|
| 24 |
+
from ..learning.episodic_memory import EpisodicMemory
|
| 25 |
+
from ..learning.mlflow_tracker import MLflowTracker
|
| 26 |
+
|
| 27 |
+
LOG = logging.getLogger("mythos.orchestrator")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class MythosOrchestrator:
|
| 31 |
+
def __init__(
|
| 32 |
+
self,
|
| 33 |
+
planner: PlannerAgent | None = None,
|
| 34 |
+
explorer: ExplorerAgent | None = None,
|
| 35 |
+
executor: ExecutorAgent | None = None,
|
| 36 |
+
max_iterations: int = 3,
|
| 37 |
+
):
|
| 38 |
+
self.planner = planner or PlannerAgent()
|
| 39 |
+
self.explorer = explorer or ExplorerAgent()
|
| 40 |
+
self.executor = executor or ExecutorAgent()
|
| 41 |
+
self.max_iterations = max_iterations
|
| 42 |
+
self.memory = EpisodicMemory()
|
| 43 |
+
self.tracker = MLflowTracker(experiment="mythos-campaigns")
|
| 44 |
+
self.transcript: list[AgentMessage] = []
|
| 45 |
+
|
| 46 |
+
# -- transport helpers --------------------------------------------------
|
| 47 |
+
|
| 48 |
+
def _send(self, msg: AgentMessage) -> None:
|
| 49 |
+
self.transcript.append(msg)
|
| 50 |
+
LOG.debug("%s → %s : %s", msg.sender, msg.recipient, str(msg.content)[:200])
|
| 51 |
+
|
| 52 |
+
# -- main loop ----------------------------------------------------------
|
| 53 |
+
|
| 54 |
+
def run_campaign(self, target: dict[str, Any]) -> dict[str, Any]:
|
| 55 |
+
run_id = self.tracker.start_run(tags={"target": target.get("repo", "?")})
|
| 56 |
+
ctx: dict[str, Any] = {"target": target, "recon": target.get("recon", {})}
|
| 57 |
+
dossier: dict[str, Any] = {"target": target, "iterations": []}
|
| 58 |
+
|
| 59 |
+
for i in range(self.max_iterations):
|
| 60 |
+
iter_started = time.time()
|
| 61 |
+
LOG.info("Mythos iteration %s/%s", i + 1, self.max_iterations)
|
| 62 |
+
|
| 63 |
+
# 1. Planner
|
| 64 |
+
plan_msg = self.planner.act(ctx)
|
| 65 |
+
self._send(plan_msg)
|
| 66 |
+
ctx.update(plan_msg.content)
|
| 67 |
+
|
| 68 |
+
# 2. Explorer (static) and Executor (dynamic) in lock-step.
|
| 69 |
+
ctx["repo_path"] = target.get("repo_path", "/data/repo")
|
| 70 |
+
ctx["harness_dir"] = target.get("harness_dir", "/tmp/research")
|
| 71 |
+
|
| 72 |
+
explorer_msg = self.explorer.act(ctx)
|
| 73 |
+
self._send(explorer_msg)
|
| 74 |
+
ctx.update(explorer_msg.content)
|
| 75 |
+
|
| 76 |
+
executor_msg = self.executor.act(ctx)
|
| 77 |
+
self._send(executor_msg)
|
| 78 |
+
ctx.update(executor_msg.content)
|
| 79 |
+
|
| 80 |
+
# 3. Refinement — feed dynamic feedback back to the Planner so it
|
| 81 |
+
# can prune / amplify hypotheses on the next loop.
|
| 82 |
+
refined = self._refine(ctx)
|
| 83 |
+
ctx["recon"] = {**ctx.get("recon", {}), **refined}
|
| 84 |
+
|
| 85 |
+
iteration = {
|
| 86 |
+
"n": i + 1,
|
| 87 |
+
"elapsed": round(time.time() - iter_started, 2),
|
| 88 |
+
"plan": plan_msg.content,
|
| 89 |
+
"static": explorer_msg.content,
|
| 90 |
+
"dynamic": executor_msg.content,
|
| 91 |
+
"refinement": refined,
|
| 92 |
+
}
|
| 93 |
+
dossier["iterations"].append(iteration)
|
| 94 |
+
self.memory.record(target, iteration)
|
| 95 |
+
self.tracker.log_iteration(run_id, iteration)
|
| 96 |
+
|
| 97 |
+
if self._converged(iteration):
|
| 98 |
+
LOG.info("Mythos campaign converged after %s iteration(s)", i + 1)
|
| 99 |
+
break
|
| 100 |
+
|
| 101 |
+
dossier["transcript"] = [m.__dict__ for m in self.transcript]
|
| 102 |
+
self.tracker.end_run(run_id)
|
| 103 |
+
return dossier
|
| 104 |
+
|
| 105 |
+
# -- helpers ------------------------------------------------------------
|
| 106 |
+
|
| 107 |
+
@staticmethod
|
| 108 |
+
def _refine(ctx: dict[str, Any]) -> dict[str, Any]:
|
| 109 |
+
dyn = ctx.get("dynamic_report", {})
|
| 110 |
+
crashes = dyn.get("crashes", [])
|
| 111 |
+
return {
|
| 112 |
+
"crash_signatures": [c.get("signature") for c in crashes if c.get("signature")],
|
| 113 |
+
"confirmed_count": len(crashes),
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
@staticmethod
|
| 117 |
+
def _converged(iteration: dict[str, Any]) -> bool:
|
| 118 |
+
dyn = iteration.get("dynamic", {}).get("dynamic_report", {})
|
| 119 |
+
return bool(dyn.get("exploits"))
|
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Planner Agent — strategic reasoning and hypothesis generation.
|
| 3 |
+
|
| 4 |
+
Implements §5.1 of the Mythos plan:
|
| 5 |
+
* Problem decomposition.
|
| 6 |
+
* Probabilistic hypothesis generation (delegates to
|
| 7 |
+
:mod:`mythos.reasoning.probabilistic`).
|
| 8 |
+
* Attack-graph construction.
|
| 9 |
+
* Resource allocation between Explorer and Executor.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
from .base import AgentMessage, MythosAgent
|
| 18 |
+
from ..reasoning.probabilistic import HypothesisEngine
|
| 19 |
+
from ..reasoning.attack_graph import AttackGraph
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PlannerAgent(MythosAgent):
|
| 23 |
+
name = "planner"
|
| 24 |
+
model_tier = "tier1"
|
| 25 |
+
|
| 26 |
+
def __init__(self, **kwargs):
|
| 27 |
+
super().__init__(**kwargs)
|
| 28 |
+
self.hypothesis_engine = HypothesisEngine()
|
| 29 |
+
self.attack_graph = AttackGraph()
|
| 30 |
+
|
| 31 |
+
def decompose(self, target: dict[str, Any]) -> list[str]:
|
| 32 |
+
"""Split a high-level engagement into ordered sub-tasks."""
|
| 33 |
+
system = (
|
| 34 |
+
"Decompose a security engagement into atomic sub-tasks. "
|
| 35 |
+
"Return JSON: {\"tasks\": [\"recon ...\", \"taint ...\", ...]}"
|
| 36 |
+
)
|
| 37 |
+
raw = self._call_llm(json.dumps(target), system=system, max_tokens=1024)
|
| 38 |
+
try:
|
| 39 |
+
return json.loads(raw).get("tasks", [])
|
| 40 |
+
except Exception:
|
| 41 |
+
# Sensible deterministic fallback so the orchestrator never stalls.
|
| 42 |
+
return [
|
| 43 |
+
"recon: enumerate languages, dependencies, attack surface",
|
| 44 |
+
"static: run Joern + Semgrep + Tree-sitter CPG queries",
|
| 45 |
+
"dynamic: synthesise fuzzing harnesses, run AFL++ + KLEE",
|
| 46 |
+
"exploit: chain primitives via pwntools",
|
| 47 |
+
"consensus: tier-3 adversarial review",
|
| 48 |
+
"disclosure: package dossier",
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
def generate_hypotheses(self, recon: dict[str, Any]) -> list[dict[str, Any]]:
|
| 52 |
+
"""Produce ranked vulnerability hypotheses with probabilistic priors."""
|
| 53 |
+
return self.hypothesis_engine.sample(recon, n=8)
|
| 54 |
+
|
| 55 |
+
def build_attack_graph(self, hypotheses: list[dict[str, Any]]) -> AttackGraph:
|
| 56 |
+
for h in hypotheses:
|
| 57 |
+
self.attack_graph.add_hypothesis(h)
|
| 58 |
+
self.attack_graph.connect()
|
| 59 |
+
return self.attack_graph
|
| 60 |
+
|
| 61 |
+
def allocate(self, hypotheses: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
| 62 |
+
"""Decide which hypothesis goes to Explorer (static) vs Executor (dynamic)."""
|
| 63 |
+
explorer_q, executor_q = [], []
|
| 64 |
+
for h in hypotheses:
|
| 65 |
+
(explorer_q if h.get("kind") in ("logic", "auth", "validation")
|
| 66 |
+
else executor_q).append(h)
|
| 67 |
+
return {"explorer": explorer_q, "executor": executor_q}
|
| 68 |
+
|
| 69 |
+
# -- agent API ----------------------------------------------------------
|
| 70 |
+
|
| 71 |
+
def act(self, context: dict[str, Any]) -> AgentMessage:
|
| 72 |
+
target = context.get("target", {})
|
| 73 |
+
recon = context.get("recon", {})
|
| 74 |
+
tasks = self.decompose(target)
|
| 75 |
+
hypotheses = self.generate_hypotheses(recon or target)
|
| 76 |
+
graph = self.build_attack_graph(hypotheses)
|
| 77 |
+
allocation = self.allocate(hypotheses)
|
| 78 |
+
return AgentMessage(
|
| 79 |
+
sender=self.name, recipient="orchestrator", role="response",
|
| 80 |
+
content={
|
| 81 |
+
"tasks": tasks,
|
| 82 |
+
"hypotheses": hypotheses,
|
| 83 |
+
"attack_graph": graph.to_dict(),
|
| 84 |
+
"allocation": allocation,
|
| 85 |
+
},
|
| 86 |
+
)
|
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI productization layer for Mythos."""
|
| 2 |
+
from .schemas import AnalyseRequest, AnalyseResponse, WebhookEvent # noqa: F401
|
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Lightweight API-key + OAuth2 bearer authentication for the Mythos API.
|
| 3 |
+
|
| 4 |
+
Backed by an env-defined static API key (``MYTHOS_API_KEYS=key1,key2``) plus
|
| 5 |
+
optional JWT validation when ``MYTHOS_JWT_PUBKEY`` is set.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
try: # pragma: no cover
|
| 14 |
+
from fastapi import Header, HTTPException, status
|
| 15 |
+
except Exception: # noqa: BLE001
|
| 16 |
+
Header = HTTPException = status = None # type: ignore
|
| 17 |
+
|
| 18 |
+
try: # pragma: no cover
|
| 19 |
+
import jwt # type: ignore
|
| 20 |
+
_JWT = True
|
| 21 |
+
except Exception: # noqa: BLE001
|
| 22 |
+
_JWT = False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _allowed_keys() -> set[str]:
|
| 26 |
+
return {k.strip() for k in os.getenv("MYTHOS_API_KEYS", "").split(",") if k.strip()}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def require_api_key(authorization: str | None = Header(default=None)) -> dict[str, Any]:
|
| 30 |
+
if HTTPException is None: # FastAPI not installed — let the caller handle it.
|
| 31 |
+
return {"sub": "anonymous"}
|
| 32 |
+
if not authorization or not authorization.lower().startswith("bearer "):
|
| 33 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing bearer")
|
| 34 |
+
token = authorization.split(None, 1)[1].strip()
|
| 35 |
+
keys = _allowed_keys()
|
| 36 |
+
if keys and token in keys:
|
| 37 |
+
return {"sub": "api-key", "token": token[:8] + "..."}
|
| 38 |
+
if _JWT and (pubkey := os.getenv("MYTHOS_JWT_PUBKEY")):
|
| 39 |
+
try:
|
| 40 |
+
return jwt.decode(token, pubkey, algorithms=["RS256"])
|
| 41 |
+
except Exception as exc: # noqa: BLE001
|
| 42 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
| 43 |
+
detail=f"jwt: {exc}") from exc
|
| 44 |
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token")
|
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mythos productization API.
|
| 3 |
+
|
| 4 |
+
Run with::
|
| 5 |
+
|
| 6 |
+
uvicorn mythos.api.fastapi_server:app --host 0.0.0.0 --port 8000
|
| 7 |
+
|
| 8 |
+
If ``fastapi`` isn't installed (e.g. minimal HF Space build) importing this
|
| 9 |
+
module is still safe — ``app`` is set to ``None`` so a deployment guard can
|
| 10 |
+
detect the gap.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import logging
|
| 16 |
+
import threading
|
| 17 |
+
import uuid
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
LOG = logging.getLogger("mythos.api")
|
| 21 |
+
|
| 22 |
+
try: # pragma: no cover
|
| 23 |
+
from fastapi import Depends, FastAPI, HTTPException
|
| 24 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 25 |
+
_FASTAPI = True
|
| 26 |
+
except Exception: # noqa: BLE001
|
| 27 |
+
_FASTAPI = False
|
| 28 |
+
FastAPI = None # type: ignore
|
| 29 |
+
|
| 30 |
+
from .auth import require_api_key
|
| 31 |
+
from .schemas import AnalyseRequest, AnalyseResponse, WebhookEvent
|
| 32 |
+
from .webhooks import deliver
|
| 33 |
+
from ..agents.orchestrator import MythosOrchestrator
|
| 34 |
+
|
| 35 |
+
_RUNS: dict[str, dict[str, Any]] = {}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if _FASTAPI:
|
| 39 |
+
app = FastAPI(
|
| 40 |
+
title="Rhodawk Mythos API",
|
| 41 |
+
version="1.0.0",
|
| 42 |
+
description="Autonomous vulnerability research as a service.",
|
| 43 |
+
)
|
| 44 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"],
|
| 45 |
+
allow_headers=["*"])
|
| 46 |
+
|
| 47 |
+
@app.get("/v1/health")
|
| 48 |
+
def health():
|
| 49 |
+
return {"status": "ok", "service": "rhodawk-mythos"}
|
| 50 |
+
|
| 51 |
+
@app.post("/v1/analyze_target", response_model=AnalyseResponse)
|
| 52 |
+
def analyze_target(req: AnalyseRequest, principal=Depends(require_api_key)):
|
| 53 |
+
run_id = uuid.uuid4().hex
|
| 54 |
+
_RUNS[run_id] = {"status": "running", "principal": principal}
|
| 55 |
+
|
| 56 |
+
target = req.dict()
|
| 57 |
+
target["recon"] = {
|
| 58 |
+
"languages": req.languages,
|
| 59 |
+
"frameworks": req.frameworks,
|
| 60 |
+
"dependencies": req.dependencies,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
def _execute():
|
| 64 |
+
try:
|
| 65 |
+
dossier = MythosOrchestrator(max_iterations=req.max_iterations).run_campaign(target)
|
| 66 |
+
_RUNS[run_id] = {"status": "complete", "dossier": dossier}
|
| 67 |
+
if req.callback_url:
|
| 68 |
+
deliver(req.callback_url, "analysis.complete",
|
| 69 |
+
{"run_id": run_id, "summary": dossier.get("iterations", [])[-1:]})
|
| 70 |
+
except Exception as exc: # noqa: BLE001
|
| 71 |
+
_RUNS[run_id] = {"status": "error", "error": str(exc)}
|
| 72 |
+
if req.callback_url:
|
| 73 |
+
deliver(req.callback_url, "analysis.error",
|
| 74 |
+
{"run_id": run_id, "error": str(exc)})
|
| 75 |
+
|
| 76 |
+
threading.Thread(target=_execute, daemon=True).start()
|
| 77 |
+
return AnalyseResponse(target=target, iterations=[], crashes=[],
|
| 78 |
+
summary=f"queued run_id={run_id}")
|
| 79 |
+
|
| 80 |
+
@app.get("/v1/runs/{run_id}")
|
| 81 |
+
def get_run(run_id: str, principal=Depends(require_api_key)):
|
| 82 |
+
if run_id not in _RUNS:
|
| 83 |
+
raise HTTPException(status_code=404, detail="unknown run_id")
|
| 84 |
+
return _RUNS[run_id]
|
| 85 |
+
|
| 86 |
+
@app.post("/v1/webhooks/test")
|
| 87 |
+
def webhook_test(evt: WebhookEvent, principal=Depends(require_api_key)):
|
| 88 |
+
return {"received": evt.dict(), "by": principal.get("sub")}
|
| 89 |
+
|
| 90 |
+
else: # pragma: no cover
|
| 91 |
+
app = None
|
| 92 |
+
LOG.warning("fastapi not installed — Mythos API surface unavailable")
|
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic schemas for the Mythos API surface."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
try: # pragma: no cover
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
except Exception: # noqa: BLE001 - pydantic always available via fastapi but be safe
|
| 10 |
+
BaseModel = object # type: ignore
|
| 11 |
+
def Field(*_a, **_kw): # type: ignore
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class AnalyseRequest(BaseModel):
|
| 16 |
+
repo: str = Field(..., description="Git URL or local path of the target.")
|
| 17 |
+
branch: str | None = None
|
| 18 |
+
languages: list[str] = []
|
| 19 |
+
frameworks: list[str] = []
|
| 20 |
+
dependencies: list[str] = []
|
| 21 |
+
focus: str | None = Field(None, description="Optional natural-language focus area.")
|
| 22 |
+
max_iterations: int = 3
|
| 23 |
+
output_format: str = Field("dossier", description="dossier | sarif | json")
|
| 24 |
+
callback_url: str | None = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class CrashReport(BaseModel):
|
| 28 |
+
id: str
|
| 29 |
+
harness: str | None = None
|
| 30 |
+
signature: str | None = None
|
| 31 |
+
rop_chain: list[str] = []
|
| 32 |
+
poc_path: str | None = None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class AnalyseResponse(BaseModel):
|
| 36 |
+
target: dict[str, Any]
|
| 37 |
+
iterations: list[dict[str, Any]]
|
| 38 |
+
crashes: list[CrashReport] = []
|
| 39 |
+
summary: str = ""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class WebhookEvent(BaseModel):
|
| 43 |
+
event: str
|
| 44 |
+
run_id: str
|
| 45 |
+
payload: dict[str, Any] = {}
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HMAC-signed webhook delivery."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import hmac
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
import requests
|
| 13 |
+
|
| 14 |
+
_SECRET = os.getenv("MYTHOS_WEBHOOK_SECRET", "")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def sign(payload: bytes) -> str:
|
| 18 |
+
if not _SECRET:
|
| 19 |
+
return "unsigned"
|
| 20 |
+
mac = hmac.new(_SECRET.encode(), payload, hashlib.sha256).hexdigest()
|
| 21 |
+
return f"sha256={mac}"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def deliver(url: str, event: str, payload: dict[str, Any]) -> dict[str, Any]:
|
| 25 |
+
body = json.dumps({"event": event, "ts": time.time(), "payload": payload},
|
| 26 |
+
default=str).encode()
|
| 27 |
+
headers = {
|
| 28 |
+
"Content-Type": "application/json",
|
| 29 |
+
"X-Mythos-Event": event,
|
| 30 |
+
"X-Mythos-Signature": sign(body),
|
| 31 |
+
}
|
| 32 |
+
try:
|
| 33 |
+
r = requests.post(url, data=body, headers=headers, timeout=15)
|
| 34 |
+
return {"status_code": r.status_code, "body": r.text[:500]}
|
| 35 |
+
except Exception as exc: # noqa: BLE001
|
| 36 |
+
return {"error": str(exc)}
|
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dynamic execution + instrumentation bridges."""
|
| 2 |
+
from .aflpp_runner import AFLPlusPlusRunner # noqa: F401
|
| 3 |
+
from .klee_runner import KLEERunner # noqa: F401
|
| 4 |
+
from .qemu_harness import QEMUHarness # noqa: F401
|
| 5 |
+
from .frida_instr import FridaInstrumenter # noqa: F401
|
| 6 |
+
from .gdb_automation import GDBAutomation # noqa: F401
|
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AFL++ runner.
|
| 3 |
+
|
| 4 |
+
If ``afl-fuzz`` is on ``$PATH`` we drive a short, time-boxed campaign over
|
| 5 |
+
each harness directory. Otherwise we route through the existing
|
| 6 |
+
``fuzzing_engine`` (Hypothesis-based) so the orchestrator still produces
|
| 7 |
+
crash candidates.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import glob
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import shutil
|
| 16 |
+
import subprocess
|
| 17 |
+
import time
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class AFLPlusPlusRunner:
|
| 22 |
+
def __init__(self, time_budget_s: int = 60):
|
| 23 |
+
self.bin = shutil.which("afl-fuzz")
|
| 24 |
+
self.time_budget_s = int(os.getenv("MYTHOS_AFL_BUDGET", time_budget_s))
|
| 25 |
+
|
| 26 |
+
def available(self) -> bool:
|
| 27 |
+
return bool(self.bin)
|
| 28 |
+
|
| 29 |
+
def run(self, harness_dir: str) -> list[dict[str, Any]]:
|
| 30 |
+
if not os.path.isdir(harness_dir):
|
| 31 |
+
return []
|
| 32 |
+
if not self.available():
|
| 33 |
+
return self._hypothesis_fallback(harness_dir)
|
| 34 |
+
crashes: list[dict[str, Any]] = []
|
| 35 |
+
for harness in glob.glob(os.path.join(harness_dir, "*_harness")):
|
| 36 |
+
in_dir = os.path.join(harness_dir, "afl_in")
|
| 37 |
+
out_dir = os.path.join(harness_dir, f"afl_out_{os.path.basename(harness)}")
|
| 38 |
+
os.makedirs(in_dir, exist_ok=True)
|
| 39 |
+
if not os.listdir(in_dir):
|
| 40 |
+
with open(os.path.join(in_dir, "seed"), "wb") as fh:
|
| 41 |
+
fh.write(b"A" * 16)
|
| 42 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 43 |
+
try:
|
| 44 |
+
start = time.time()
|
| 45 |
+
proc = subprocess.Popen(
|
| 46 |
+
[self.bin, "-i", in_dir, "-o", out_dir, "-V", str(self.time_budget_s),
|
| 47 |
+
"--", harness, "@@"],
|
| 48 |
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
| 49 |
+
)
|
| 50 |
+
proc.wait(timeout=self.time_budget_s + 30)
|
| 51 |
+
for crash_path in glob.glob(os.path.join(out_dir, "default", "crashes", "id:*")):
|
| 52 |
+
crashes.append({
|
| 53 |
+
"id": os.path.basename(crash_path),
|
| 54 |
+
"harness": harness,
|
| 55 |
+
"path": crash_path,
|
| 56 |
+
"elapsed_s": round(time.time() - start, 2),
|
| 57 |
+
"signature": self._signature(crash_path),
|
| 58 |
+
})
|
| 59 |
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
| 60 |
+
continue
|
| 61 |
+
return crashes
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
def _signature(path: str) -> str:
|
| 65 |
+
try:
|
| 66 |
+
with open(path, "rb") as fh:
|
| 67 |
+
return fh.read(64).hex()
|
| 68 |
+
except OSError:
|
| 69 |
+
return ""
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def _hypothesis_fallback(harness_dir: str) -> list[dict[str, Any]]:
|
| 73 |
+
"""Surface a marker that the legacy fuzzing_engine will consume."""
|
| 74 |
+
marker = os.path.join(harness_dir, "_mythos_afl_unavailable.json")
|
| 75 |
+
try:
|
| 76 |
+
with open(marker, "w") as fh:
|
| 77 |
+
json.dump({"fallback": "hypothesis"}, fh)
|
| 78 |
+
except OSError:
|
| 79 |
+
pass
|
| 80 |
+
return []
|
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Frida dynamic instrumentation — attaches a generic syscall/cred tracer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
try: # pragma: no cover
|
| 9 |
+
import frida # type: ignore
|
| 10 |
+
_FRIDA = True
|
| 11 |
+
except Exception: # noqa: BLE001
|
| 12 |
+
_FRIDA = False
|
| 13 |
+
|
| 14 |
+
# Minimal generic JS instrumentation script — interceptors are expanded by
|
| 15 |
+
# the orchestrator at call-site for kind-specific tracing.
|
| 16 |
+
_DEFAULT_SCRIPT = r"""
|
| 17 |
+
const interesting = ['open', 'execve', 'connect', 'recvfrom', 'mmap'];
|
| 18 |
+
interesting.forEach((name) => {
|
| 19 |
+
try {
|
| 20 |
+
const sym = Module.findExportByName(null, name);
|
| 21 |
+
if (sym) Interceptor.attach(sym, {
|
| 22 |
+
onEnter(args) { send({event: name, args: args.map(a => a.toString())}); }
|
| 23 |
+
});
|
| 24 |
+
} catch (e) {}
|
| 25 |
+
});
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class FridaInstrumenter:
|
| 30 |
+
def available(self) -> bool:
|
| 31 |
+
return _FRIDA
|
| 32 |
+
|
| 33 |
+
def attach_all(self, harness_dir: str) -> list[dict[str, Any]]:
|
| 34 |
+
if not _FRIDA:
|
| 35 |
+
return []
|
| 36 |
+
events: list[dict[str, Any]] = []
|
| 37 |
+
|
| 38 |
+
def on_message(msg, _data):
|
| 39 |
+
if msg.get("type") == "send":
|
| 40 |
+
events.append(msg.get("payload", {}))
|
| 41 |
+
|
| 42 |
+
device = frida.get_local_device()
|
| 43 |
+
for proc in device.enumerate_processes():
|
| 44 |
+
if not any(proc.name.startswith(b) for b in ("python", "node", "java")):
|
| 45 |
+
continue
|
| 46 |
+
try:
|
| 47 |
+
session = device.attach(proc.pid)
|
| 48 |
+
script = session.create_script(_DEFAULT_SCRIPT)
|
| 49 |
+
script.on("message", on_message)
|
| 50 |
+
script.load()
|
| 51 |
+
except Exception:
|
| 52 |
+
continue
|
| 53 |
+
return events[:500]
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GDB-Python automation — replays a crash through GDB and captures
|
| 3 |
+
backtrace, registers, and a small chunk of memory around the crash site.
|
| 4 |
+
|
| 5 |
+
When GDB is missing the function returns a structured marker so the
|
| 6 |
+
orchestrator can record the gap.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import shutil
|
| 13 |
+
import subprocess
|
| 14 |
+
import tempfile
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
_GDB_SCRIPT = """\
|
| 18 |
+
set pagination off
|
| 19 |
+
set logging file {logfile}
|
| 20 |
+
set logging on
|
| 21 |
+
run < {input}
|
| 22 |
+
bt
|
| 23 |
+
info registers
|
| 24 |
+
x/64x $sp
|
| 25 |
+
quit
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class GDBAutomation:
|
| 30 |
+
def __init__(self):
|
| 31 |
+
self.bin = shutil.which("gdb")
|
| 32 |
+
|
| 33 |
+
def available(self) -> bool:
|
| 34 |
+
return bool(self.bin)
|
| 35 |
+
|
| 36 |
+
def replay(self, crash: dict[str, Any]) -> dict[str, Any]:
|
| 37 |
+
if not self.available():
|
| 38 |
+
return {"crash": crash.get("id"), "gdb": "unavailable"}
|
| 39 |
+
binary = crash.get("harness")
|
| 40 |
+
crash_input = crash.get("path")
|
| 41 |
+
if not (binary and crash_input and os.path.exists(binary) and os.path.exists(crash_input)):
|
| 42 |
+
return {"crash": crash.get("id"), "gdb": "missing-binary-or-input"}
|
| 43 |
+
with tempfile.TemporaryDirectory() as work:
|
| 44 |
+
logfile = os.path.join(work, "gdb.log")
|
| 45 |
+
scriptfile = os.path.join(work, "script.gdb")
|
| 46 |
+
with open(scriptfile, "w") as fh:
|
| 47 |
+
fh.write(_GDB_SCRIPT.format(logfile=logfile, input=crash_input))
|
| 48 |
+
try:
|
| 49 |
+
subprocess.run([self.bin, "-q", "-batch", "-x", scriptfile, binary],
|
| 50 |
+
capture_output=True, timeout=60, check=False)
|
| 51 |
+
with open(logfile) as fh:
|
| 52 |
+
log = fh.read()[-4000:]
|
| 53 |
+
except Exception as exc: # noqa: BLE001
|
| 54 |
+
log = f"gdb error: {exc}"
|
| 55 |
+
return {"crash": crash.get("id"), "gdb_log": log}
|
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""KLEE symbolic execution runner — emits per-path execution traces."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import glob
|
| 6 |
+
import os
|
| 7 |
+
import shutil
|
| 8 |
+
import subprocess
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class KLEERunner:
|
| 13 |
+
def __init__(self, time_budget_s: int = 120):
|
| 14 |
+
self.bin = shutil.which("klee")
|
| 15 |
+
self.time_budget_s = int(os.getenv("MYTHOS_KLEE_BUDGET", time_budget_s))
|
| 16 |
+
|
| 17 |
+
def available(self) -> bool:
|
| 18 |
+
return bool(self.bin)
|
| 19 |
+
|
| 20 |
+
def run(self, harness_dir: str) -> list[dict[str, Any]]:
|
| 21 |
+
if not self.available() or not os.path.isdir(harness_dir):
|
| 22 |
+
return []
|
| 23 |
+
traces: list[dict[str, Any]] = []
|
| 24 |
+
for bc in glob.glob(os.path.join(harness_dir, "*.bc")):
|
| 25 |
+
try:
|
| 26 |
+
proc = subprocess.run(
|
| 27 |
+
[self.bin, "--max-time", str(self.time_budget_s), bc],
|
| 28 |
+
capture_output=True, text=True, timeout=self.time_budget_s + 30, check=False,
|
| 29 |
+
)
|
| 30 |
+
traces.append({
|
| 31 |
+
"module": bc,
|
| 32 |
+
"stdout_tail": proc.stdout[-2000:],
|
| 33 |
+
"stderr_tail": proc.stderr[-2000:],
|
| 34 |
+
})
|
| 35 |
+
except subprocess.TimeoutExpired:
|
| 36 |
+
traces.append({"module": bc, "error": "timeout"})
|
| 37 |
+
return traces
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""QEMU full-system emulation harness for kernel-level fuzzing experiments."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class QEMUHarness:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.bin = shutil.which("qemu-system-x86_64") or shutil.which("qemu-x86_64")
|
| 14 |
+
|
| 15 |
+
def available(self) -> bool:
|
| 16 |
+
return bool(self.bin)
|
| 17 |
+
|
| 18 |
+
def run(self, harness_dir: str) -> list[dict[str, Any]]:
|
| 19 |
+
if not self.available():
|
| 20 |
+
return []
|
| 21 |
+
# Look for prepared kernel images / userland binaries.
|
| 22 |
+
kernels = [p for p in os.listdir(harness_dir) if p.endswith((".elf", ".bin"))]
|
| 23 |
+
if not kernels:
|
| 24 |
+
return []
|
| 25 |
+
traces: list[dict[str, Any]] = []
|
| 26 |
+
for kern in kernels:
|
| 27 |
+
try:
|
| 28 |
+
proc = subprocess.run(
|
| 29 |
+
[self.bin, "-d", "in_asm,exec", "-D", "/tmp/qemu.log",
|
| 30 |
+
"-no-reboot", "-nographic", "-kernel", os.path.join(harness_dir, kern)],
|
| 31 |
+
capture_output=True, text=True, timeout=120, check=False,
|
| 32 |
+
)
|
| 33 |
+
traces.append({"kernel": kern,
|
| 34 |
+
"stdout_tail": proc.stdout[-1500:],
|
| 35 |
+
"stderr_tail": proc.stderr[-1500:]})
|
| 36 |
+
except subprocess.TimeoutExpired:
|
| 37 |
+
traces.append({"kernel": kern, "error": "timeout"})
|
| 38 |
+
return traces
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exploit synthesis primitives."""
|
| 2 |
+
from .pwntools_synth import PwntoolsSynth # noqa: F401
|
| 3 |
+
from .rop_chain import ROPChainBuilder # noqa: F401
|
| 4 |
+
from .heap_exploit import HeapExploitKit # noqa: F401
|
| 5 |
+
from .privesc_kb import PrivEscKB # noqa: F401
|
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Heap exploitation kit — produces target-allocator-aware spray templates.
|
| 3 |
+
|
| 4 |
+
Supports glibc ptmalloc2 (default), tcache, jemalloc, and a generic
|
| 5 |
+
fallback. The Executor uses the resulting template as a starting point for
|
| 6 |
+
GDB+GEF-driven manual confirmation.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
_TEMPLATES = {
|
| 14 |
+
"ptmalloc2": (
|
| 15 |
+
"# tcache poisoning skeleton\n"
|
| 16 |
+
"for i in range(7):\n"
|
| 17 |
+
" free(allocate({size}))\n"
|
| 18 |
+
"free(target_chunk)\n"
|
| 19 |
+
"overwrite_fd(target_chunk, target_addr)\n"
|
| 20 |
+
"victim = allocate({size}) # returns target_addr\n"
|
| 21 |
+
),
|
| 22 |
+
"tcache": (
|
| 23 |
+
"# tcache double-free skeleton\n"
|
| 24 |
+
"a = allocate({size}); b = allocate({size})\n"
|
| 25 |
+
"free(a); free(b); free(a)\n"
|
| 26 |
+
"x = allocate({size}); overwrite_fd(x, target_addr)\n"
|
| 27 |
+
"allocate({size}); allocate({size}) # returns target_addr\n"
|
| 28 |
+
),
|
| 29 |
+
"jemalloc": (
|
| 30 |
+
"# jemalloc run-overlap skeleton\n"
|
| 31 |
+
"spray = [allocate({size}) for _ in range(0x40)]\n"
|
| 32 |
+
"trigger_uaf(spray[0x20])\n"
|
| 33 |
+
"spray2 = [allocate({size}) for _ in range(0x40)]\n"
|
| 34 |
+
),
|
| 35 |
+
"generic": (
|
| 36 |
+
"# generic massage spray\n"
|
| 37 |
+
"spray = [allocate({size}) for _ in range(0x100)]\n"
|
| 38 |
+
"trigger_vulnerability(spray[0x80])\n"
|
| 39 |
+
),
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class HeapExploitKit:
|
| 44 |
+
def spray_template(self, crash: dict[str, Any]) -> dict[str, Any]:
|
| 45 |
+
allocator = crash.get("allocator", "ptmalloc2")
|
| 46 |
+
size = crash.get("chunk_size", 0x80)
|
| 47 |
+
tpl = _TEMPLATES.get(allocator, _TEMPLATES["generic"]).format(size=hex(size))
|
| 48 |
+
return {
|
| 49 |
+
"allocator": allocator,
|
| 50 |
+
"chunk_size": size,
|
| 51 |
+
"template": tpl,
|
| 52 |
+
"notes": "Refine with GEF (`heap chunks`, `heap bins`) before weaponising.",
|
| 53 |
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Privilege-escalation knowledge base — codifies LinPEAS / WinPEAS heuristics
|
| 3 |
+
into structured suggestions the Executor can verify automatically.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
_LINUX_VECTORS = [
|
| 11 |
+
{"id": "suid-binaries", "cmd": "find / -perm -4000 -type f 2>/dev/null"},
|
| 12 |
+
{"id": "writable-passwd", "cmd": "ls -la /etc/passwd /etc/shadow"},
|
| 13 |
+
{"id": "kernel-version", "cmd": "uname -a; cat /proc/version"},
|
| 14 |
+
{"id": "cron-jobs", "cmd": "ls -la /etc/cron* /var/spool/cron 2>/dev/null"},
|
| 15 |
+
{"id": "sudo-rules", "cmd": "sudo -l 2>/dev/null"},
|
| 16 |
+
{"id": "capabilities", "cmd": "getcap -r / 2>/dev/null"},
|
| 17 |
+
{"id": "docker-socket", "cmd": "ls -la /var/run/docker.sock 2>/dev/null"},
|
| 18 |
+
{"id": "world-writable-paths", "cmd": "find / -writable -type d 2>/dev/null | head -50"},
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
_WINDOWS_VECTORS = [
|
| 22 |
+
{"id": "service-perms", "cmd": "accesschk.exe -uwcqv \"Authenticated Users\" *"},
|
| 23 |
+
{"id": "unquoted-paths", "cmd": "wmic service get name,displayname,pathname,startmode | findstr /i \"auto\""},
|
| 24 |
+
{"id": "always-install-elev", "cmd": "reg query HKCU\\Software\\Policies\\Microsoft\\Windows\\Installer"},
|
| 25 |
+
{"id": "stored-credentials", "cmd": "cmdkey /list"},
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class PrivEscKB:
|
| 30 |
+
def suggest(self, hypotheses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 31 |
+
wants_linux = any(h.get("kind") == "auth" for h in hypotheses)
|
| 32 |
+
wants_windows = any("windows" in (h.get("rationale") or "").lower() for h in hypotheses)
|
| 33 |
+
out: list[dict[str, Any]] = []
|
| 34 |
+
if wants_linux or not wants_windows:
|
| 35 |
+
out += [{**v, "platform": "linux"} for v in _LINUX_VECTORS]
|
| 36 |
+
if wants_windows:
|
| 37 |
+
out += [{**v, "platform": "windows"} for v in _WINDOWS_VECTORS]
|
| 38 |
+
return out
|
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pwntools-based PoC assembler.
|
| 3 |
+
|
| 4 |
+
Produces a runnable Python script for every confirmed crash that a human
|
| 5 |
+
researcher (or downstream Bounty Gateway) can review and submit. When
|
| 6 |
+
``pwntools`` isn't installed the assembler still emits a self-contained
|
| 7 |
+
Python file using the standard library that demonstrates the input.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import textwrap
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
try: # pragma: no cover
|
| 17 |
+
from pwn import context # type: ignore # noqa: F401
|
| 18 |
+
_PWN = True
|
| 19 |
+
except Exception: # noqa: BLE001
|
| 20 |
+
_PWN = False
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class PwntoolsSynth:
|
| 24 |
+
def assemble(self, crash: dict[str, Any], rop_chain: list[str]) -> dict[str, Any]:
|
| 25 |
+
binary = crash.get("harness", "<binary>")
|
| 26 |
+
crash_input = crash.get("path", "<input>")
|
| 27 |
+
chain_lit = ", ".join(repr(g) for g in rop_chain) or "# no gadgets"
|
| 28 |
+
if _PWN:
|
| 29 |
+
template = textwrap.dedent(f"""\
|
| 30 |
+
# Auto-generated by Rhodawk Mythos
|
| 31 |
+
from pwn import *
|
| 32 |
+
context.log_level = 'error'
|
| 33 |
+
p = process({binary!r})
|
| 34 |
+
rop = ROP({binary!r})
|
| 35 |
+
gadgets = [{chain_lit}]
|
| 36 |
+
with open({crash_input!r}, 'rb') as f:
|
| 37 |
+
payload = f.read()
|
| 38 |
+
p.sendline(payload)
|
| 39 |
+
print(p.recvall(timeout=2))
|
| 40 |
+
""")
|
| 41 |
+
else:
|
| 42 |
+
template = textwrap.dedent(f"""\
|
| 43 |
+
# Auto-generated by Rhodawk Mythos (no-pwntools fallback)
|
| 44 |
+
import subprocess, sys
|
| 45 |
+
with open({crash_input!r}, 'rb') as f:
|
| 46 |
+
payload = f.read()
|
| 47 |
+
proc = subprocess.run([{binary!r}], input=payload,
|
| 48 |
+
capture_output=True, timeout=5)
|
| 49 |
+
sys.stdout.write(proc.stdout.decode(errors='replace'))
|
| 50 |
+
sys.stderr.write(proc.stderr.decode(errors='replace'))
|
| 51 |
+
""")
|
| 52 |
+
out_path = os.path.join("/tmp", f"poc_{crash.get('id', 'x')}.py")
|
| 53 |
+
try:
|
| 54 |
+
with open(out_path, "w") as fh:
|
| 55 |
+
fh.write(template)
|
| 56 |
+
except OSError:
|
| 57 |
+
pass
|
| 58 |
+
return {"path": out_path, "code": template, "uses_pwntools": _PWN}
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ROP chain builder — wraps ``angrop`` and ``ROPgadget`` when available, with
|
| 3 |
+
an in-memory deterministic gadget registry so unit tests can exercise the
|
| 4 |
+
chain logic without binary inputs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import shutil
|
| 10 |
+
import subprocess
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
try: # pragma: no cover
|
| 14 |
+
import angr # type: ignore # noqa: F401
|
| 15 |
+
import angrop # type: ignore # noqa: F401
|
| 16 |
+
_ANGROP = True
|
| 17 |
+
except Exception: # noqa: BLE001
|
| 18 |
+
_ANGROP = False
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ROPChainBuilder:
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.ropgadget = shutil.which("ROPgadget")
|
| 24 |
+
|
| 25 |
+
def build(self, crash: dict[str, Any]) -> list[str]:
|
| 26 |
+
binary = crash.get("harness")
|
| 27 |
+
if not binary:
|
| 28 |
+
return []
|
| 29 |
+
if _ANGROP:
|
| 30 |
+
return self._with_angrop(binary)
|
| 31 |
+
if self.ropgadget:
|
| 32 |
+
return self._with_ropgadget(binary)
|
| 33 |
+
return ["pop rdi ; ret", "/bin/sh", "system"]
|
| 34 |
+
|
| 35 |
+
@staticmethod
|
| 36 |
+
def _with_angrop(binary: str) -> list[str]: # pragma: no cover - heavy dep
|
| 37 |
+
try:
|
| 38 |
+
project = angr.Project(binary, auto_load_libs=False)
|
| 39 |
+
rop = project.analyses.ROP()
|
| 40 |
+
rop.find_gadgets()
|
| 41 |
+
chain = rop.execve(b"/bin/sh\x00")
|
| 42 |
+
return [str(g) for g in chain.gadgets]
|
| 43 |
+
except Exception:
|
| 44 |
+
return []
|
| 45 |
+
|
| 46 |
+
def _with_ropgadget(self, binary: str) -> list[str]:
|
| 47 |
+
try:
|
| 48 |
+
proc = subprocess.run(
|
| 49 |
+
[self.ropgadget, "--binary", binary], capture_output=True,
|
| 50 |
+
text=True, timeout=120, check=False,
|
| 51 |
+
)
|
| 52 |
+
lines = [ln.strip() for ln in proc.stdout.splitlines()
|
| 53 |
+
if ":" in ln and "ret" in ln]
|
| 54 |
+
return lines[:32]
|
| 55 |
+
except Exception:
|
| 56 |
+
return []
|
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Integration shim between the legacy ``hermes_orchestrator`` six-phase
|
| 3 |
+
pipeline and the new Mythos multi-agent framework.
|
| 4 |
+
|
| 5 |
+
The shim is *additive*: existing Rhodawk code paths keep working unchanged.
|
| 6 |
+
Callers that opt in to Mythos by setting ``RHODAWK_MYTHOS=1`` get the
|
| 7 |
+
Planner/Explorer/Executor pipeline transparently.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def mythos_enabled() -> bool:
|
| 17 |
+
return os.getenv("RHODAWK_MYTHOS", "0").lower() in ("1", "true", "yes", "on")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def maybe_run_mythos(target: dict[str, Any]) -> dict[str, Any] | None:
|
| 21 |
+
"""If Mythos is enabled, run the multi-agent pipeline and return its dossier."""
|
| 22 |
+
if not mythos_enabled():
|
| 23 |
+
return None
|
| 24 |
+
from .agents.orchestrator import MythosOrchestrator
|
| 25 |
+
|
| 26 |
+
orch = MythosOrchestrator()
|
| 27 |
+
return orch.run_campaign(target)
|
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Self-improvement: RL planner, MLflow tracker, LoRA adapters, curriculum, episodic memory."""
|
| 2 |
+
from .rl_planner import RLPlanner # noqa: F401
|
| 3 |
+
from .mlflow_tracker import MLflowTracker # noqa: F401
|
| 4 |
+
from .lora_adapters import LoRAAdapterManager # noqa: F401
|
| 5 |
+
from .curriculum import CurriculumScheduler # noqa: F401
|
| 6 |
+
from .episodic_memory import EpisodicMemory # noqa: F401
|
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Curriculum scheduler — orders training targets from easy → hard.
|
| 3 |
+
|
| 4 |
+
Difficulty is a weighted blend of:
|
| 5 |
+
* lines of code,
|
| 6 |
+
* dependency surface,
|
| 7 |
+
* historical success rate of similar repos.
|
| 8 |
+
|
| 9 |
+
Used by the data-flywheel to feed RL / LoRA fine-tuning with progressively
|
| 10 |
+
harder workloads.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from typing import Any
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class CurriculumItem:
|
| 22 |
+
repo: str
|
| 23 |
+
loc: int
|
| 24 |
+
dep_count: int
|
| 25 |
+
historical_success: float = 0.0 # 0..1
|
| 26 |
+
|
| 27 |
+
@property
|
| 28 |
+
def difficulty(self) -> float:
|
| 29 |
+
return (
|
| 30 |
+
0.5 * math.log1p(self.loc)
|
| 31 |
+
+ 0.3 * math.log1p(self.dep_count)
|
| 32 |
+
+ 0.2 * (1.0 - self.historical_success)
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class CurriculumScheduler:
|
| 37 |
+
def __init__(self, items: list[CurriculumItem] | None = None):
|
| 38 |
+
self.items: list[CurriculumItem] = items or []
|
| 39 |
+
|
| 40 |
+
def add(self, repo: str, loc: int, dep_count: int, success: float = 0.0) -> None:
|
| 41 |
+
self.items.append(CurriculumItem(repo, loc, dep_count, success))
|
| 42 |
+
|
| 43 |
+
def next_batch(self, batch_size: int = 4) -> list[CurriculumItem]:
|
| 44 |
+
self.items.sort(key=lambda i: i.difficulty)
|
| 45 |
+
return self.items[:batch_size]
|
| 46 |
+
|
| 47 |
+
def to_dict(self) -> dict[str, Any]:
|
| 48 |
+
return {"items": [i.__dict__ | {"difficulty": i.difficulty} for i in self.items]}
|
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Episodic memory — stores complete campaign trajectories on disk so the
|
| 3 |
+
Planner can retrieve "what worked last time on a similar repo".
|
| 4 |
+
|
| 5 |
+
Backed by SQLite for portability (no extra deps). Schema mirrors the
|
| 6 |
+
``memory_engine`` patterns already in the repo.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import sqlite3
|
| 14 |
+
import time
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
_DB = os.getenv("MYTHOS_EPISODIC_DB", "/data/mythos/episodic.sqlite")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class EpisodicMemory:
|
| 21 |
+
def __init__(self, path: str = _DB):
|
| 22 |
+
self.path = path
|
| 23 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 24 |
+
self._db = sqlite3.connect(path, check_same_thread=False)
|
| 25 |
+
self._db.execute(
|
| 26 |
+
"CREATE TABLE IF NOT EXISTS episodes ("
|
| 27 |
+
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
| 28 |
+
" ts REAL,"
|
| 29 |
+
" repo TEXT,"
|
| 30 |
+
" iteration INTEGER,"
|
| 31 |
+
" cwes TEXT,"
|
| 32 |
+
" outcome TEXT,"
|
| 33 |
+
" payload TEXT"
|
| 34 |
+
")"
|
| 35 |
+
)
|
| 36 |
+
self._db.execute(
|
| 37 |
+
"CREATE INDEX IF NOT EXISTS idx_episodes_repo ON episodes(repo)"
|
| 38 |
+
)
|
| 39 |
+
self._db.commit()
|
| 40 |
+
|
| 41 |
+
def record(self, target: dict[str, Any], iteration: dict[str, Any]) -> int:
|
| 42 |
+
cwes = [h.get("cwe") for h in iteration.get("plan", {}).get("hypotheses", [])]
|
| 43 |
+
outcome = "exploit" if iteration.get("dynamic", {}).get(
|
| 44 |
+
"dynamic_report", {}).get("exploits") else "unconfirmed"
|
| 45 |
+
cur = self._db.execute(
|
| 46 |
+
"INSERT INTO episodes(ts, repo, iteration, cwes, outcome, payload) "
|
| 47 |
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
| 48 |
+
(time.time(), target.get("repo", "?"), iteration.get("n", 0),
|
| 49 |
+
json.dumps(cwes), outcome, json.dumps(iteration, default=str)[:200_000]),
|
| 50 |
+
)
|
| 51 |
+
self._db.commit()
|
| 52 |
+
return cur.lastrowid or 0
|
| 53 |
+
|
| 54 |
+
def recall(self, repo: str, limit: int = 10) -> list[dict[str, Any]]:
|
| 55 |
+
cur = self._db.execute(
|
| 56 |
+
"SELECT ts, iteration, cwes, outcome FROM episodes "
|
| 57 |
+
"WHERE repo = ? ORDER BY id DESC LIMIT ?", (repo, limit),
|
| 58 |
+
)
|
| 59 |
+
return [
|
| 60 |
+
{"ts": ts, "iteration": it, "cwes": json.loads(cwes), "outcome": outcome}
|
| 61 |
+
for ts, it, cwes, outcome in cur.fetchall()
|
| 62 |
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LoRA / QLoRA adapter manager.
|
| 3 |
+
|
| 4 |
+
Wraps the existing ``lora_scheduler`` module and adds Mythos-specific
|
| 5 |
+
versioning + A/B testing semantics. Adapters are pinned per (cwe, target
|
| 6 |
+
language) so the orchestrator can ship a specialised Tier-2 weight set per
|
| 7 |
+
campaign class.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import time
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
_ADAPTERS_INDEX = os.getenv("MYTHOS_ADAPTER_INDEX", "/data/mythos/adapters/index.json")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class LoRAAdapterManager:
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.index: dict[str, dict[str, Any]] = {}
|
| 24 |
+
self._load()
|
| 25 |
+
|
| 26 |
+
def register(self, name: str, *, cwe: str, language: str, base_model: str,
|
| 27 |
+
weight_path: str, metrics: dict[str, float] | None = None) -> str:
|
| 28 |
+
entry = {
|
| 29 |
+
"name": name, "cwe": cwe, "language": language, "base_model": base_model,
|
| 30 |
+
"weight_path": weight_path, "metrics": metrics or {},
|
| 31 |
+
"version": int(time.time()),
|
| 32 |
+
}
|
| 33 |
+
self.index.setdefault(name, {})["latest"] = entry
|
| 34 |
+
self.index[name].setdefault("history", []).append(entry)
|
| 35 |
+
self._save()
|
| 36 |
+
return f"{name}@{entry['version']}"
|
| 37 |
+
|
| 38 |
+
def select(self, *, cwe: str, language: str) -> dict[str, Any] | None:
|
| 39 |
+
for name, body in self.index.items():
|
| 40 |
+
latest = body.get("latest", {})
|
| 41 |
+
if latest.get("cwe") == cwe and latest.get("language") == language:
|
| 42 |
+
return latest
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
def rollback(self, name: str) -> dict[str, Any] | None:
|
| 46 |
+
body = self.index.get(name, {})
|
| 47 |
+
history = body.get("history", [])
|
| 48 |
+
if len(history) < 2:
|
| 49 |
+
return None
|
| 50 |
+
body["latest"] = history[-2]
|
| 51 |
+
self._save()
|
| 52 |
+
return body["latest"]
|
| 53 |
+
|
| 54 |
+
def _load(self) -> None:
|
| 55 |
+
try:
|
| 56 |
+
with open(_ADAPTERS_INDEX) as fh:
|
| 57 |
+
self.index = json.load(fh)
|
| 58 |
+
except Exception: # noqa: BLE001
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
def _save(self) -> None:
|
| 62 |
+
try:
|
| 63 |
+
os.makedirs(os.path.dirname(_ADAPTERS_INDEX), exist_ok=True)
|
| 64 |
+
with open(_ADAPTERS_INDEX, "w") as fh:
|
| 65 |
+
json.dump(self.index, fh, indent=2)
|
| 66 |
+
except OSError:
|
| 67 |
+
pass
|
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Thin MLflow tracker — falls back to a JSONL log when MLflow is absent."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
import uuid
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
try: # pragma: no cover
|
| 12 |
+
import mlflow # type: ignore
|
| 13 |
+
_MLFLOW = True
|
| 14 |
+
except Exception: # noqa: BLE001
|
| 15 |
+
_MLFLOW = False
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
_FALLBACK_LOG = os.getenv("MYTHOS_MLFLOW_FALLBACK", "/data/mythos/mlflow_fallback.jsonl")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class MLflowTracker:
|
| 22 |
+
def __init__(self, experiment: str = "mythos"):
|
| 23 |
+
self.experiment = experiment
|
| 24 |
+
if _MLFLOW:
|
| 25 |
+
try:
|
| 26 |
+
mlflow.set_experiment(experiment)
|
| 27 |
+
except Exception: # noqa: BLE001
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
def start_run(self, tags: dict[str, str] | None = None) -> str:
|
| 31 |
+
if _MLFLOW:
|
| 32 |
+
try:
|
| 33 |
+
run = mlflow.start_run(tags=tags or {})
|
| 34 |
+
return run.info.run_id
|
| 35 |
+
except Exception: # noqa: BLE001
|
| 36 |
+
pass
|
| 37 |
+
run_id = uuid.uuid4().hex
|
| 38 |
+
self._jsonl({"event": "start", "run_id": run_id, "tags": tags or {},
|
| 39 |
+
"experiment": self.experiment, "ts": time.time()})
|
| 40 |
+
return run_id
|
| 41 |
+
|
| 42 |
+
def log_iteration(self, run_id: str, iteration: dict[str, Any]) -> None:
|
| 43 |
+
if _MLFLOW:
|
| 44 |
+
try:
|
| 45 |
+
mlflow.log_metric("hypotheses",
|
| 46 |
+
len(iteration.get("plan", {}).get("hypotheses", [])),
|
| 47 |
+
step=iteration.get("n", 0))
|
| 48 |
+
mlflow.log_metric("crashes",
|
| 49 |
+
iteration.get("refinement", {}).get("confirmed_count", 0),
|
| 50 |
+
step=iteration.get("n", 0))
|
| 51 |
+
except Exception: # noqa: BLE001
|
| 52 |
+
pass
|
| 53 |
+
self._jsonl({"event": "iter", "run_id": run_id, "iter": iteration})
|
| 54 |
+
|
| 55 |
+
def end_run(self, run_id: str) -> None:
|
| 56 |
+
if _MLFLOW:
|
| 57 |
+
try:
|
| 58 |
+
mlflow.end_run()
|
| 59 |
+
except Exception: # noqa: BLE001
|
| 60 |
+
pass
|
| 61 |
+
self._jsonl({"event": "end", "run_id": run_id, "ts": time.time()})
|
| 62 |
+
|
| 63 |
+
def _jsonl(self, payload: dict[str, Any]) -> None:
|
| 64 |
+
try:
|
| 65 |
+
os.makedirs(os.path.dirname(_FALLBACK_LOG), exist_ok=True)
|
| 66 |
+
with open(_FALLBACK_LOG, "a") as fh:
|
| 67 |
+
fh.write(json.dumps(payload, default=str) + "\n")
|
| 68 |
+
except OSError:
|
| 69 |
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Reinforcement-learning controller for the Planner.
|
| 3 |
+
|
| 4 |
+
Wraps Ray RLlib / Stable Baselines3 when available; otherwise exposes a
|
| 5 |
+
contextual-bandit baseline that updates per-CWE arm preferences from
|
| 6 |
+
campaign rewards. This is enough to deliver measurable improvement in the
|
| 7 |
+
Planner's choice of CWE focus across hundreds of campaigns.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import math
|
| 14 |
+
import os
|
| 15 |
+
import random
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
try: # pragma: no cover
|
| 19 |
+
import ray # type: ignore # noqa: F401
|
| 20 |
+
from ray.rllib.algorithms.ppo import PPOConfig # type: ignore # noqa: F401
|
| 21 |
+
_RLLIB = True
|
| 22 |
+
except Exception: # noqa: BLE001
|
| 23 |
+
_RLLIB = False
|
| 24 |
+
|
| 25 |
+
try: # pragma: no cover
|
| 26 |
+
from stable_baselines3 import PPO # type: ignore # noqa: F401
|
| 27 |
+
_SB3 = True
|
| 28 |
+
except Exception: # noqa: BLE001
|
| 29 |
+
_SB3 = False
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
_STATE_FILE = os.getenv("MYTHOS_RL_STATE", "/data/mythos/rl_state.json")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class RLPlanner:
|
| 36 |
+
"""Contextual UCB1 over CWE arms (with PPO upgrade path)."""
|
| 37 |
+
|
| 38 |
+
def __init__(self):
|
| 39 |
+
self.counts: dict[str, int] = {}
|
| 40 |
+
self.values: dict[str, float] = {}
|
| 41 |
+
self.t: int = 0
|
| 42 |
+
self._load()
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def backend(self) -> str:
|
| 46 |
+
if _RLLIB:
|
| 47 |
+
return "ray-rllib"
|
| 48 |
+
if _SB3:
|
| 49 |
+
return "stable-baselines3"
|
| 50 |
+
return "ucb1"
|
| 51 |
+
|
| 52 |
+
def select(self, candidate_cwes: list[str]) -> str:
|
| 53 |
+
self.t += 1
|
| 54 |
+
if not candidate_cwes:
|
| 55 |
+
return ""
|
| 56 |
+
# Cold-start: pull each arm at least once.
|
| 57 |
+
for c in candidate_cwes:
|
| 58 |
+
if self.counts.get(c, 0) == 0:
|
| 59 |
+
return c
|
| 60 |
+
scored = [
|
| 61 |
+
(c, self.values[c] + math.sqrt(2 * math.log(self.t) / self.counts[c]))
|
| 62 |
+
for c in candidate_cwes
|
| 63 |
+
]
|
| 64 |
+
return max(scored, key=lambda x: x[1])[0]
|
| 65 |
+
|
| 66 |
+
def reward(self, cwe: str, signal: float) -> None:
|
| 67 |
+
n = self.counts.get(cwe, 0) + 1
|
| 68 |
+
v = self.values.get(cwe, 0.0)
|
| 69 |
+
self.counts[cwe] = n
|
| 70 |
+
self.values[cwe] = v + (signal - v) / n
|
| 71 |
+
self._save()
|
| 72 |
+
|
| 73 |
+
def explore(self, candidates: list[str], epsilon: float = 0.1) -> str:
|
| 74 |
+
if random.random() < epsilon:
|
| 75 |
+
return random.choice(candidates) if candidates else ""
|
| 76 |
+
return self.select(candidates)
|
| 77 |
+
|
| 78 |
+
# -- persistence --------------------------------------------------------
|
| 79 |
+
|
| 80 |
+
def _load(self) -> None:
|
| 81 |
+
try:
|
| 82 |
+
with open(_STATE_FILE) as fh:
|
| 83 |
+
state = json.load(fh)
|
| 84 |
+
self.counts = state.get("counts", {})
|
| 85 |
+
self.values = state.get("values", {})
|
| 86 |
+
self.t = state.get("t", 0)
|
| 87 |
+
except Exception: # noqa: BLE001
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
def _save(self) -> None:
|
| 91 |
+
try:
|
| 92 |
+
os.makedirs(os.path.dirname(_STATE_FILE), exist_ok=True)
|
| 93 |
+
with open(_STATE_FILE, "w") as fh:
|
| 94 |
+
json.dump({"counts": self.counts, "values": self.values, "t": self.t}, fh)
|
| 95 |
+
except Exception: # noqa: BLE001
|
| 96 |
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Mythos-specialised Model Context Protocol servers."""
|
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tiny in-process MCP-compatible runtime used by every Mythos MCP server.
|
| 3 |
+
|
| 4 |
+
Real production deployments will swap this for the official ``mcp`` Python
|
| 5 |
+
SDK. Keeping a local shim means the Mythos servers can be exercised
|
| 6 |
+
end-to-end inside the existing HuggingFace Space without pulling extra
|
| 7 |
+
binary deps.
|
| 8 |
+
|
| 9 |
+
Wire protocol on stdio:
|
| 10 |
+
|
| 11 |
+
>>> {"id": 1, "method": "tools/list"}
|
| 12 |
+
<<< {"id": 1, "result": [{"name": "...", "schema": {...}}]}
|
| 13 |
+
>>> {"id": 2, "method": "tools/call", "params": {"name": "...", "args": {...}}}
|
| 14 |
+
<<< {"id": 2, "result": {...}}
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import json
|
| 20 |
+
import logging
|
| 21 |
+
import sys
|
| 22 |
+
from typing import Any, Callable
|
| 23 |
+
|
| 24 |
+
LOG = logging.getLogger("mythos.mcp")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class MCPServer:
|
| 28 |
+
def __init__(self, name: str):
|
| 29 |
+
self.name = name
|
| 30 |
+
self._tools: dict[str, dict[str, Any]] = {}
|
| 31 |
+
|
| 32 |
+
def tool(self, name: str, schema: dict[str, Any] | None = None):
|
| 33 |
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
| 34 |
+
self._tools[name] = {"fn": fn, "schema": schema or {}}
|
| 35 |
+
return fn
|
| 36 |
+
return decorator
|
| 37 |
+
|
| 38 |
+
# -- introspection ------------------------------------------------------
|
| 39 |
+
|
| 40 |
+
def list_tools(self) -> list[dict[str, Any]]:
|
| 41 |
+
return [{"name": n, "schema": meta["schema"]} for n, meta in self._tools.items()]
|
| 42 |
+
|
| 43 |
+
def call(self, name: str, args: dict[str, Any]) -> Any:
|
| 44 |
+
if name not in self._tools:
|
| 45 |
+
raise KeyError(f"unknown tool: {name}")
|
| 46 |
+
return self._tools[name]["fn"](**(args or {}))
|
| 47 |
+
|
| 48 |
+
# -- transports ---------------------------------------------------------
|
| 49 |
+
|
| 50 |
+
def serve_stdio(self) -> None: # pragma: no cover - manual transport
|
| 51 |
+
for line in sys.stdin:
|
| 52 |
+
line = line.strip()
|
| 53 |
+
if not line:
|
| 54 |
+
continue
|
| 55 |
+
try:
|
| 56 |
+
req = json.loads(line)
|
| 57 |
+
method = req.get("method")
|
| 58 |
+
rid = req.get("id")
|
| 59 |
+
if method == "tools/list":
|
| 60 |
+
resp = {"id": rid, "result": self.list_tools()}
|
| 61 |
+
elif method == "tools/call":
|
| 62 |
+
params = req.get("params", {})
|
| 63 |
+
resp = {"id": rid,
|
| 64 |
+
"result": self.call(params.get("name"), params.get("args", {}))}
|
| 65 |
+
else:
|
| 66 |
+
resp = {"id": rid, "error": {"code": -32601, "message": "unknown method"}}
|
| 67 |
+
except Exception as exc: # noqa: BLE001
|
| 68 |
+
resp = {"id": req.get("id") if isinstance(req, dict) else None,
|
| 69 |
+
"error": {"code": -32000, "message": str(exc)}}
|
| 70 |
+
sys.stdout.write(json.dumps(resp, default=str) + "\n")
|
| 71 |
+
sys.stdout.flush()
|
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``dynamic-analysis-mcp`` — AFL++, KLEE, QEMU, Frida, GDB."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from ._mcp_runtime import MCPServer
|
| 6 |
+
from ..dynamic.aflpp_runner import AFLPlusPlusRunner
|
| 7 |
+
from ..dynamic.klee_runner import KLEERunner
|
| 8 |
+
from ..dynamic.qemu_harness import QEMUHarness
|
| 9 |
+
from ..dynamic.frida_instr import FridaInstrumenter
|
| 10 |
+
from ..dynamic.gdb_automation import GDBAutomation
|
| 11 |
+
|
| 12 |
+
server = MCPServer("dynamic-analysis-mcp")
|
| 13 |
+
_afl = AFLPlusPlusRunner()
|
| 14 |
+
_klee = KLEERunner()
|
| 15 |
+
_qemu = QEMUHarness()
|
| 16 |
+
_frida = FridaInstrumenter()
|
| 17 |
+
_gdb = GDBAutomation()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@server.tool("afl_run", {"harness_dir": "string"})
|
| 21 |
+
def afl_run(harness_dir: str): return _afl.run(harness_dir)
|
| 22 |
+
@server.tool("klee_run", {"harness_dir": "string"})
|
| 23 |
+
def klee_run(harness_dir: str): return _klee.run(harness_dir)
|
| 24 |
+
@server.tool("qemu_run", {"harness_dir": "string"})
|
| 25 |
+
def qemu_run(harness_dir: str): return _qemu.run(harness_dir)
|
| 26 |
+
@server.tool("frida_attach", {"harness_dir": "string"})
|
| 27 |
+
def frida_attach(harness_dir: str): return _frida.attach_all(harness_dir)
|
| 28 |
+
@server.tool("gdb_replay", {"crash": "object"})
|
| 29 |
+
def gdb_replay(crash: dict): return _gdb.replay(crash)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__": # pragma: no cover
|
| 33 |
+
server.serve_stdio()
|
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``exploit-generation-mcp`` — Pwntools + ROPGadget + heap + privesc."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from ._mcp_runtime import MCPServer
|
| 6 |
+
from ..exploit.pwntools_synth import PwntoolsSynth
|
| 7 |
+
from ..exploit.rop_chain import ROPChainBuilder
|
| 8 |
+
from ..exploit.heap_exploit import HeapExploitKit
|
| 9 |
+
from ..exploit.privesc_kb import PrivEscKB
|
| 10 |
+
|
| 11 |
+
server = MCPServer("exploit-generation-mcp")
|
| 12 |
+
_pwn = PwntoolsSynth()
|
| 13 |
+
_rop = ROPChainBuilder()
|
| 14 |
+
_heap = HeapExploitKit()
|
| 15 |
+
_pe = PrivEscKB()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@server.tool("rop_chain", {"crash": "object"})
|
| 19 |
+
def rop_chain(crash: dict):
|
| 20 |
+
return _rop.build(crash)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@server.tool("pwntools_assemble", {"crash": "object", "rop_chain": "array"})
|
| 24 |
+
def pwntools_assemble(crash: dict, rop_chain: list):
|
| 25 |
+
return _pwn.assemble(crash, rop_chain)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@server.tool("heap_template", {"crash": "object"})
|
| 29 |
+
def heap_template(crash: dict):
|
| 30 |
+
return _heap.spray_template(crash)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@server.tool("privesc_suggest", {"hypotheses": "array"})
|
| 34 |
+
def privesc_suggest(hypotheses: list):
|
| 35 |
+
return _pe.suggest(hypotheses)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == "__main__": # pragma: no cover
|
| 39 |
+
server.serve_stdio()
|
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``static-analysis-mcp`` — Joern + CodeQL + Semgrep + Tree-sitter."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from ._mcp_runtime import MCPServer
|
| 6 |
+
from ..static.joern_bridge import JoernBridge
|
| 7 |
+
from ..static.codeql_bridge import CodeQLBridge
|
| 8 |
+
from ..static.semgrep_bridge import SemgrepBridge
|
| 9 |
+
from ..static.treesitter_cpg import TreeSitterCPG
|
| 10 |
+
|
| 11 |
+
server = MCPServer("static-analysis-mcp")
|
| 12 |
+
_joern = JoernBridge()
|
| 13 |
+
_codeql = CodeQLBridge()
|
| 14 |
+
_semgrep = SemgrepBridge()
|
| 15 |
+
_tree = TreeSitterCPG()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@server.tool("cpg_summary", {"repo_path": "string"})
|
| 19 |
+
def cpg_summary(repo_path: str):
|
| 20 |
+
return _tree.summary(repo_path)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@server.tool("joern_query", {"repo_path": "string", "hypotheses": "array"})
|
| 24 |
+
def joern_query(repo_path: str, hypotheses: list):
|
| 25 |
+
return _joern.query(repo_path, hypotheses)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@server.tool("codeql_query", {"repo_path": "string", "hypotheses": "array"})
|
| 29 |
+
def codeql_query(repo_path: str, hypotheses: list):
|
| 30 |
+
return _codeql.query(repo_path, hypotheses)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@server.tool("semgrep_scan", {"repo_path": "string", "hypotheses": "array"})
|
| 34 |
+
def semgrep_scan(repo_path: str, hypotheses: list):
|
| 35 |
+
return _semgrep.scan(repo_path, hypotheses)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == "__main__": # pragma: no cover
|
| 39 |
+
server.serve_stdio()
|
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
``vulnerability-database-mcp`` — NVD / OSV / Exploit-DB lookup.
|
| 3 |
+
|
| 4 |
+
Uses the existing ``cve_intel`` module when available, plus public OSV
|
| 5 |
+
JSON for unauthenticated queries.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
import requests
|
| 13 |
+
|
| 14 |
+
from ._mcp_runtime import MCPServer
|
| 15 |
+
|
| 16 |
+
server = MCPServer("vulnerability-database-mcp")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@server.tool("osv_query", {"package": "string", "ecosystem": "string", "version": "string"})
|
| 20 |
+
def osv_query(package: str, ecosystem: str = "PyPI", version: str = "") -> dict[str, Any]:
|
| 21 |
+
payload: dict[str, Any] = {"package": {"name": package, "ecosystem": ecosystem}}
|
| 22 |
+
if version:
|
| 23 |
+
payload["version"] = version
|
| 24 |
+
try:
|
| 25 |
+
r = requests.post("https://api.osv.dev/v1/query", json=payload, timeout=15)
|
| 26 |
+
r.raise_for_status()
|
| 27 |
+
return r.json()
|
| 28 |
+
except Exception as exc: # noqa: BLE001
|
| 29 |
+
return {"error": str(exc)}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@server.tool("nvd_cve", {"cve_id": "string"})
|
| 33 |
+
def nvd_cve(cve_id: str) -> dict[str, Any]:
|
| 34 |
+
try:
|
| 35 |
+
r = requests.get(
|
| 36 |
+
f"https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={cve_id}", timeout=15,
|
| 37 |
+
)
|
| 38 |
+
r.raise_for_status()
|
| 39 |
+
return r.json()
|
| 40 |
+
except Exception as exc: # noqa: BLE001
|
| 41 |
+
return {"error": str(exc)}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@server.tool("exploit_db_search", {"q": "string"})
|
| 45 |
+
def exploit_db_search(q: str) -> dict[str, Any]:
|
| 46 |
+
try:
|
| 47 |
+
r = requests.get(
|
| 48 |
+
"https://www.exploit-db.com/search",
|
| 49 |
+
params={"q": q}, timeout=15,
|
| 50 |
+
headers={"User-Agent": "rhodawk-mythos/1.0"},
|
| 51 |
+
)
|
| 52 |
+
return {"status_code": r.status_code, "snippet": r.text[:2000]}
|
| 53 |
+
except Exception as exc: # noqa: BLE001
|
| 54 |
+
return {"error": str(exc)}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__": # pragma: no cover
|
| 58 |
+
server.serve_stdio()
|
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
``web-security-mcp`` — bridges OWASP ZAP / sqlmap / nuclei.
|
| 3 |
+
|
| 4 |
+
When the binary isn't on ``$PATH`` we return a structured "unavailable"
|
| 5 |
+
result so the agent can fall back to the existing ``web-security-mcp``
|
| 6 |
+
heuristics in ``mcp_config.json``.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import shutil
|
| 12 |
+
import subprocess
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
from ._mcp_runtime import MCPServer
|
| 16 |
+
|
| 17 |
+
server = MCPServer("web-security-mcp")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _runtool(cmd: list[str], timeout: int = 120) -> dict[str, Any]:
|
| 21 |
+
if not shutil.which(cmd[0]):
|
| 22 |
+
return {"available": False, "tool": cmd[0]}
|
| 23 |
+
try:
|
| 24 |
+
proc = subprocess.run(cmd, capture_output=True, text=True,
|
| 25 |
+
timeout=timeout, check=False)
|
| 26 |
+
return {"available": True, "rc": proc.returncode,
|
| 27 |
+
"stdout_tail": proc.stdout[-2000:],
|
| 28 |
+
"stderr_tail": proc.stderr[-2000:]}
|
| 29 |
+
except subprocess.TimeoutExpired:
|
| 30 |
+
return {"available": True, "error": "timeout"}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@server.tool("zap_baseline", {"target": "string"})
|
| 34 |
+
def zap_baseline(target: str):
|
| 35 |
+
return _runtool(["zap-baseline.py", "-t", target, "-q"], timeout=600)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@server.tool("nuclei_scan", {"target": "string", "templates": "string"})
|
| 39 |
+
def nuclei_scan(target: str, templates: str = ""):
|
| 40 |
+
cmd = ["nuclei", "-u", target, "-jsonl", "-silent"]
|
| 41 |
+
if templates:
|
| 42 |
+
cmd += ["-t", templates]
|
| 43 |
+
return _runtool(cmd, timeout=600)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@server.tool("sqlmap_quick", {"target": "string"})
|
| 47 |
+
def sqlmap_quick(target: str):
|
| 48 |
+
return _runtool(["sqlmap", "-u", target, "--batch", "--level=2", "--risk=2"],
|
| 49 |
+
timeout=600)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__": # pragma: no cover
|
| 53 |
+
server.serve_stdio()
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Probabilistic reasoning + attack-graph utilities."""
|
| 2 |
+
from .probabilistic import HypothesisEngine # noqa: F401
|
| 3 |
+
from .attack_graph import AttackGraph # noqa: F401
|
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Attack-graph construction for the Planner.
|
| 3 |
+
|
| 4 |
+
Nodes = hypotheses or intermediate states (e.g. "leaked-pointer", "RCE").
|
| 5 |
+
Edges = exploitation transitions weighted by the joint probability of the
|
| 6 |
+
pair occurring in the same code-base + the cost of the chain.
|
| 7 |
+
|
| 8 |
+
Falls back to a tiny pure-Python adjacency-list when ``networkx`` is absent
|
| 9 |
+
so the orchestrator works in minimal images.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
try: # pragma: no cover - optional dep
|
| 17 |
+
import networkx as nx # type: ignore
|
| 18 |
+
except Exception: # noqa: BLE001
|
| 19 |
+
nx = None # type: ignore
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Heuristic compatibility map between vulnerability classes that can be
|
| 23 |
+
# plausibly chained together to amplify impact.
|
| 24 |
+
_CHAIN_RULES: list[tuple[str, str, float]] = [
|
| 25 |
+
("CWE-22", "CWE-78", 0.7), # path traversal → command injection
|
| 26 |
+
("CWE-89", "CWE-78", 0.5), # SQLi → RCE via UDF
|
| 27 |
+
("CWE-79", "CWE-352", 0.6), # XSS → CSRF
|
| 28 |
+
("CWE-918", "CWE-502", 0.4), # SSRF → deserialisation
|
| 29 |
+
("CWE-119", "CWE-787", 0.8), # overflow → OOB write
|
| 30 |
+
("CWE-787", "CWE-416", 0.7), # OOB write → UAF
|
| 31 |
+
("CWE-416", "CWE-269", 0.6), # UAF → privesc
|
| 32 |
+
("CWE-287", "CWE-862", 0.5), # auth bypass → missing authz
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class AttackGraph:
|
| 37 |
+
def __init__(self):
|
| 38 |
+
self.nodes: dict[str, dict[str, Any]] = {}
|
| 39 |
+
self.edges: list[tuple[str, str, float]] = []
|
| 40 |
+
self._g = nx.DiGraph() if nx is not None else None
|
| 41 |
+
|
| 42 |
+
def add_hypothesis(self, h: dict[str, Any]) -> None:
|
| 43 |
+
cwe = h["cwe"]
|
| 44 |
+
self.nodes[cwe] = {**h, "id": cwe}
|
| 45 |
+
if self._g is not None:
|
| 46 |
+
self._g.add_node(cwe, **h)
|
| 47 |
+
|
| 48 |
+
def connect(self) -> None:
|
| 49 |
+
for src, dst, base_w in _CHAIN_RULES:
|
| 50 |
+
if src in self.nodes and dst in self.nodes:
|
| 51 |
+
w = base_w * self.nodes[src]["confidence"] * self.nodes[dst]["confidence"]
|
| 52 |
+
self.edges.append((src, dst, round(w, 4)))
|
| 53 |
+
if self._g is not None:
|
| 54 |
+
self._g.add_edge(src, dst, weight=w)
|
| 55 |
+
|
| 56 |
+
def critical_paths(self, top: int = 3) -> list[list[str]]:
|
| 57 |
+
if self._g is None or self._g.number_of_nodes() == 0:
|
| 58 |
+
# naive heaviest-edge fallback
|
| 59 |
+
sorted_e = sorted(self.edges, key=lambda e: e[2], reverse=True)[:top]
|
| 60 |
+
return [list(e[:2]) for e in sorted_e]
|
| 61 |
+
paths: list[tuple[float, list[str]]] = []
|
| 62 |
+
for src in self._g.nodes:
|
| 63 |
+
for dst in self._g.nodes:
|
| 64 |
+
if src == dst:
|
| 65 |
+
continue
|
| 66 |
+
try:
|
| 67 |
+
p = nx.shortest_path(self._g, src, dst, weight=lambda *_: 1)
|
| 68 |
+
score = sum(self._g.edges[a, b].get("weight", 0) for a, b in zip(p, p[1:]))
|
| 69 |
+
paths.append((score, p))
|
| 70 |
+
except Exception:
|
| 71 |
+
continue
|
| 72 |
+
paths.sort(key=lambda x: x[0], reverse=True)
|
| 73 |
+
return [p for _, p in paths[:top]]
|
| 74 |
+
|
| 75 |
+
def to_dict(self) -> dict[str, Any]:
|
| 76 |
+
return {
|
| 77 |
+
"nodes": list(self.nodes.values()),
|
| 78 |
+
"edges": [{"src": s, "dst": d, "weight": w} for s, d, w in self.edges],
|
| 79 |
+
"critical_paths": self.critical_paths(),
|
| 80 |
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hypothesis Engine — probabilistic reasoning over vulnerability hypotheses.
|
| 3 |
+
|
| 4 |
+
Implements §4.1 of the Mythos plan. Uses Pyro / PyMC when available, falls
|
| 5 |
+
back to a transparent NumPy Bayesian update otherwise so the engine is
|
| 6 |
+
always usable inside a HuggingFace Space without GPU acceleration.
|
| 7 |
+
|
| 8 |
+
The engine maintains per-CWE prior probabilities and updates them with
|
| 9 |
+
evidence collected by the Explorer/Executor agents — this is the
|
| 10 |
+
``confidence`` value the Planner uses for resource allocation.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
import random
|
| 17 |
+
from dataclasses import dataclass, field
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
# Optional probabilistic-programming back-ends.
|
| 21 |
+
try: # pragma: no cover - optional
|
| 22 |
+
import pyro # type: ignore # noqa: F401
|
| 23 |
+
import pyro.distributions as dist # type: ignore # noqa: F401
|
| 24 |
+
_PYRO = True
|
| 25 |
+
except Exception: # noqa: BLE001
|
| 26 |
+
_PYRO = False
|
| 27 |
+
|
| 28 |
+
try: # pragma: no cover - optional
|
| 29 |
+
import pymc as pm # type: ignore # noqa: F401
|
| 30 |
+
_PYMC = True
|
| 31 |
+
except Exception: # noqa: BLE001
|
| 32 |
+
_PYMC = False
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
# Curated CWE → vulnerability-class priors. These are pragmatic starting
|
| 37 |
+
# points sourced from the OWASP Top 10 + CWE Top 25 exposure stats; the
|
| 38 |
+
# engine refines them online from successful campaigns.
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
CWE_PRIORS: dict[str, float] = {
|
| 41 |
+
"CWE-79": 0.18, # XSS
|
| 42 |
+
"CWE-89": 0.16, # SQLi
|
| 43 |
+
"CWE-78": 0.10, # OS command injection
|
| 44 |
+
"CWE-22": 0.08, # Path traversal
|
| 45 |
+
"CWE-94": 0.08, # Code injection
|
| 46 |
+
"CWE-119": 0.12, # Buffer overflow
|
| 47 |
+
"CWE-416": 0.10, # UAF
|
| 48 |
+
"CWE-787": 0.10, # Out-of-bounds write
|
| 49 |
+
"CWE-269": 0.05, # Improper privilege management
|
| 50 |
+
"CWE-287": 0.07, # Improper authentication
|
| 51 |
+
"CWE-352": 0.04, # CSRF
|
| 52 |
+
"CWE-918": 0.05, # SSRF
|
| 53 |
+
"CWE-502": 0.06, # Unsafe deserialization
|
| 54 |
+
"CWE-732": 0.04, # Incorrect permissions
|
| 55 |
+
"CWE-862": 0.05, # Missing authorization
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
KIND_FOR_CWE: dict[str, str] = {
|
| 59 |
+
"CWE-79": "validation", "CWE-89": "validation", "CWE-78": "validation",
|
| 60 |
+
"CWE-22": "validation", "CWE-94": "logic", "CWE-119": "memory",
|
| 61 |
+
"CWE-416": "memory", "CWE-787": "memory", "CWE-269": "auth",
|
| 62 |
+
"CWE-287": "auth", "CWE-352": "auth", "CWE-918": "logic",
|
| 63 |
+
"CWE-502": "logic", "CWE-732": "auth", "CWE-862": "auth",
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class Hypothesis:
|
| 69 |
+
cwe: str
|
| 70 |
+
kind: str
|
| 71 |
+
confidence: float
|
| 72 |
+
rationale: str = ""
|
| 73 |
+
evidence: list[str] = field(default_factory=list)
|
| 74 |
+
|
| 75 |
+
def to_dict(self) -> dict[str, Any]:
|
| 76 |
+
return self.__dict__
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class HypothesisEngine:
|
| 80 |
+
"""Bayesian-flavoured generator of ranked vulnerability hypotheses."""
|
| 81 |
+
|
| 82 |
+
def __init__(self, seed: int | None = None):
|
| 83 |
+
self.rng = random.Random(seed)
|
| 84 |
+
self.priors = dict(CWE_PRIORS)
|
| 85 |
+
|
| 86 |
+
# -- public -------------------------------------------------------------
|
| 87 |
+
|
| 88 |
+
def sample(self, recon: dict[str, Any], n: int = 8) -> list[dict[str, Any]]:
|
| 89 |
+
"""Return the top-``n`` hypotheses for a recon snapshot."""
|
| 90 |
+
evidence_boosts = self._boosts_from_recon(recon)
|
| 91 |
+
scored: list[Hypothesis] = []
|
| 92 |
+
for cwe, prior in self.priors.items():
|
| 93 |
+
posterior = self._bayes_update(prior, evidence_boosts.get(cwe, 0.0))
|
| 94 |
+
scored.append(Hypothesis(
|
| 95 |
+
cwe=cwe,
|
| 96 |
+
kind=KIND_FOR_CWE.get(cwe, "logic"),
|
| 97 |
+
confidence=round(posterior, 4),
|
| 98 |
+
rationale=self._rationale(cwe, recon, posterior),
|
| 99 |
+
))
|
| 100 |
+
scored.sort(key=lambda h: h.confidence, reverse=True)
|
| 101 |
+
return [h.to_dict() for h in scored[:n]]
|
| 102 |
+
|
| 103 |
+
def update_with_outcome(self, cwe: str, *, success: bool) -> None:
|
| 104 |
+
"""Online refinement: bump or decay a prior after a campaign result."""
|
| 105 |
+
prior = self.priors.get(cwe, 0.05)
|
| 106 |
+
if success:
|
| 107 |
+
self.priors[cwe] = min(0.95, prior + 0.03)
|
| 108 |
+
else:
|
| 109 |
+
self.priors[cwe] = max(0.005, prior * 0.95)
|
| 110 |
+
|
| 111 |
+
# -- internals ----------------------------------------------------------
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def _bayes_update(prior: float, log_lift: float) -> float:
|
| 115 |
+
"""Combine a base prior with a log-odds evidence boost."""
|
| 116 |
+
if prior <= 0.0 or prior >= 1.0:
|
| 117 |
+
return prior
|
| 118 |
+
odds = prior / (1.0 - prior)
|
| 119 |
+
odds *= math.exp(log_lift)
|
| 120 |
+
return odds / (1.0 + odds)
|
| 121 |
+
|
| 122 |
+
@staticmethod
|
| 123 |
+
def _boosts_from_recon(recon: dict[str, Any]) -> dict[str, float]:
|
| 124 |
+
"""Translate recon hints (languages, deps, frameworks) into log-odds boosts."""
|
| 125 |
+
boosts: dict[str, float] = {}
|
| 126 |
+
langs = {l.lower() for l in recon.get("languages", [])}
|
| 127 |
+
deps = {d.lower() for d in recon.get("dependencies", [])}
|
| 128 |
+
frame = {f.lower() for f in recon.get("frameworks", [])}
|
| 129 |
+
if {"c", "c++", "cpp"} & langs:
|
| 130 |
+
for cwe in ("CWE-119", "CWE-416", "CWE-787"):
|
| 131 |
+
boosts[cwe] = boosts.get(cwe, 0.0) + 1.0
|
| 132 |
+
if {"javascript", "typescript", "node"} & langs or "express" in frame:
|
| 133 |
+
boosts["CWE-79"] = boosts.get("CWE-79", 0.0) + 0.7
|
| 134 |
+
if "django" in frame or "flask" in frame or "rails" in frame:
|
| 135 |
+
boosts["CWE-89"] = boosts.get("CWE-89", 0.0) + 0.5
|
| 136 |
+
boosts["CWE-352"] = boosts.get("CWE-352", 0.0) + 0.4
|
| 137 |
+
if {"jackson", "pickle", "marshal", "yaml"} & deps:
|
| 138 |
+
boosts["CWE-502"] = boosts.get("CWE-502", 0.0) + 1.2
|
| 139 |
+
return boosts
|
| 140 |
+
|
| 141 |
+
@staticmethod
|
| 142 |
+
def _rationale(cwe: str, recon: dict[str, Any], posterior: float) -> str:
|
| 143 |
+
return (
|
| 144 |
+
f"{cwe} elevated to p={posterior:.2f} by recon "
|
| 145 |
+
f"langs={recon.get('languages', [])[:3]} "
|
| 146 |
+
f"frameworks={recon.get('frameworks', [])[:3]}"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
# -- back-end advertisement --------------------------------------------
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def backend(self) -> str:
|
| 153 |
+
if _PYRO:
|
| 154 |
+
return "pyro"
|
| 155 |
+
if _PYMC:
|
| 156 |
+
return "pymc"
|
| 157 |
+
return "numpy-bayes"
|
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standardised skill registry following the ``agentskills.io`` schema."""
|
| 2 |
+
from .registry import SkillRegistry, Skill # noqa: F401
|
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
agentskills.io-compatible skill registry.
|
| 3 |
+
|
| 4 |
+
Skills are JSON documents persisted to disk and indexed by name; the Hermes
|
| 5 |
+
agent populates this registry from successful campaign trajectories so the
|
| 6 |
+
Mythos orchestrator can compose them at planning time.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import time
|
| 14 |
+
from dataclasses import dataclass, field, asdict
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
_REGISTRY_DIR = os.getenv("MYTHOS_SKILLS_DIR", "/data/mythos/skills")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class Skill:
|
| 22 |
+
name: str
|
| 23 |
+
description: str
|
| 24 |
+
inputs: dict[str, str] = field(default_factory=dict)
|
| 25 |
+
outputs: dict[str, str] = field(default_factory=dict)
|
| 26 |
+
steps: list[dict[str, Any]] = field(default_factory=list)
|
| 27 |
+
tags: list[str] = field(default_factory=list)
|
| 28 |
+
created_ts: float = field(default_factory=time.time)
|
| 29 |
+
schema: str = "agentskills.io/1.0"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class SkillRegistry:
|
| 33 |
+
def __init__(self, root: str = _REGISTRY_DIR):
|
| 34 |
+
self.root = root
|
| 35 |
+
os.makedirs(root, exist_ok=True)
|
| 36 |
+
self._seed_default()
|
| 37 |
+
|
| 38 |
+
def add(self, skill: Skill) -> str:
|
| 39 |
+
path = os.path.join(self.root, f"{skill.name}.json")
|
| 40 |
+
with open(path, "w") as fh:
|
| 41 |
+
json.dump(asdict(skill), fh, indent=2)
|
| 42 |
+
return path
|
| 43 |
+
|
| 44 |
+
def get(self, name: str) -> Skill | None:
|
| 45 |
+
path = os.path.join(self.root, f"{name}.json")
|
| 46 |
+
if not os.path.exists(path):
|
| 47 |
+
return None
|
| 48 |
+
with open(path) as fh:
|
| 49 |
+
data = json.load(fh)
|
| 50 |
+
return Skill(**data)
|
| 51 |
+
|
| 52 |
+
def list(self, tag: str | None = None) -> list[Skill]:
|
| 53 |
+
out: list[Skill] = []
|
| 54 |
+
for fn in os.listdir(self.root):
|
| 55 |
+
if not fn.endswith(".json"):
|
| 56 |
+
continue
|
| 57 |
+
with open(os.path.join(self.root, fn)) as fh:
|
| 58 |
+
s = Skill(**json.load(fh))
|
| 59 |
+
if tag is None or tag in s.tags:
|
| 60 |
+
out.append(s)
|
| 61 |
+
return out
|
| 62 |
+
|
| 63 |
+
def _seed_default(self) -> None:
|
| 64 |
+
for skill in DEFAULT_SKILLS:
|
| 65 |
+
target = os.path.join(self.root, f"{skill.name}.json")
|
| 66 |
+
if not os.path.exists(target):
|
| 67 |
+
self.add(skill)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
DEFAULT_SKILLS: list[Skill] = [
|
| 71 |
+
Skill(
|
| 72 |
+
name="analyze_ast",
|
| 73 |
+
description="Parse a target file with Tree-sitter and emit a CST summary.",
|
| 74 |
+
inputs={"path": "string"}, outputs={"summary": "object"},
|
| 75 |
+
steps=[{"call": "mythos.static.treesitter_cpg.TreeSitterCPG.summary"}],
|
| 76 |
+
tags=["static", "ast"],
|
| 77 |
+
),
|
| 78 |
+
Skill(
|
| 79 |
+
name="generate_fuzz_harness",
|
| 80 |
+
description="Synthesise a hypothesis-driven fuzz harness for the Executor.",
|
| 81 |
+
inputs={"hypothesis": "object"}, outputs={"harness_path": "string"},
|
| 82 |
+
steps=[{"call": "harness_factory.build_harness"}],
|
| 83 |
+
tags=["dynamic", "fuzzing"],
|
| 84 |
+
),
|
| 85 |
+
Skill(
|
| 86 |
+
name="find_rop_gadgets",
|
| 87 |
+
description="Enumerate ROP gadgets in a binary using angrop / ROPgadget.",
|
| 88 |
+
inputs={"binary": "string"}, outputs={"gadgets": "array"},
|
| 89 |
+
steps=[{"call": "mythos.exploit.rop_chain.ROPChainBuilder.build"}],
|
| 90 |
+
tags=["exploit", "rop"],
|
| 91 |
+
),
|
| 92 |
+
Skill(
|
| 93 |
+
name="chain_exploit",
|
| 94 |
+
description="Chain primitives into a runnable PoC with pwntools.",
|
| 95 |
+
inputs={"crash": "object", "rop_chain": "array"},
|
| 96 |
+
outputs={"poc_path": "string"},
|
| 97 |
+
steps=[{"call": "mythos.exploit.pwntools_synth.PwntoolsSynth.assemble"}],
|
| 98 |
+
tags=["exploit", "pwntools"],
|
| 99 |
+
),
|
| 100 |
+
Skill(
|
| 101 |
+
name="perform_taint_analysis",
|
| 102 |
+
description="Run Semgrep + Joern with hypothesis-targeted taint queries.",
|
| 103 |
+
inputs={"repo_path": "string", "hypotheses": "array"},
|
| 104 |
+
outputs={"findings": "array"},
|
| 105 |
+
steps=[{"call": "mythos.static.semgrep_bridge.SemgrepBridge.scan"}],
|
| 106 |
+
tags=["static", "taint"],
|
| 107 |
+
),
|
| 108 |
+
Skill(
|
| 109 |
+
name="debug_process",
|
| 110 |
+
description="Replay a crash through GDB and capture state.",
|
| 111 |
+
inputs={"crash": "object"}, outputs={"gdb_log": "string"},
|
| 112 |
+
steps=[{"call": "mythos.dynamic.gdb_automation.GDBAutomation.replay"}],
|
| 113 |
+
tags=["dynamic", "gdb"],
|
| 114 |
+
),
|
| 115 |
+
Skill(
|
| 116 |
+
name="generate_poc_report",
|
| 117 |
+
description="Package campaign findings + PoC into a disclosure dossier.",
|
| 118 |
+
inputs={"dossier": "object"}, outputs={"report_path": "string"},
|
| 119 |
+
steps=[{"call": "disclosure_vault.write_dossier"}],
|
| 120 |
+
tags=["disclosure"],
|
| 121 |
+
),
|
| 122 |
+
]
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Advanced static analysis bridges (Tree-sitter, Joern, CodeQL, Semgrep)."""
|
| 2 |
+
from .treesitter_cpg import TreeSitterCPG # noqa: F401
|
| 3 |
+
from .joern_bridge import JoernBridge # noqa: F401
|
| 4 |
+
from .codeql_bridge import CodeQLBridge # noqa: F401
|
| 5 |
+
from .semgrep_bridge import SemgrepBridge # noqa: F401
|
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodeQL bridge — runs the open-source CodeQL CLI against a target repo.
|
| 3 |
+
|
| 4 |
+
The bridge:
|
| 5 |
+
* detects the ``codeql`` binary on ``$PATH``;
|
| 6 |
+
* creates a database for the repo (auto-detects language);
|
| 7 |
+
* runs the bundled QL pack matching each hypothesis kind;
|
| 8 |
+
* returns parsed SARIF results.
|
| 9 |
+
|
| 10 |
+
When CodeQL is missing the bridge returns an empty list rather than
|
| 11 |
+
crashing — the Explorer's other backends provide partial coverage.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import shutil
|
| 19 |
+
import subprocess
|
| 20 |
+
import tempfile
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
# Hypothesis-kind → CodeQL pack to run. These are the open-source packs
|
| 24 |
+
# shipped with the CodeQL CLI.
|
| 25 |
+
_PACK_FOR_KIND = {
|
| 26 |
+
"validation": "codeql/python-queries:Security/CWE-079/ReflectedXss.ql",
|
| 27 |
+
"memory": "codeql/cpp-queries:Security/CWE-119/UnboundedWrite.ql",
|
| 28 |
+
"auth": "codeql/javascript-queries:Security/CWE-287/MissingAuthN.ql",
|
| 29 |
+
"logic": "codeql/python-queries:Security/CWE-094/CodeInjection.ql",
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class CodeQLBridge:
|
| 34 |
+
def __init__(self):
|
| 35 |
+
self.codeql = shutil.which("codeql")
|
| 36 |
+
|
| 37 |
+
def available(self) -> bool:
|
| 38 |
+
return bool(self.codeql)
|
| 39 |
+
|
| 40 |
+
def query(self, repo_path: str, hypotheses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 41 |
+
if not self.available() or not os.path.isdir(repo_path):
|
| 42 |
+
return []
|
| 43 |
+
with tempfile.TemporaryDirectory() as workdir:
|
| 44 |
+
db = os.path.join(workdir, "db")
|
| 45 |
+
try:
|
| 46 |
+
subprocess.run(
|
| 47 |
+
[self.codeql, "database", "create", db, "--language=python", "--source-root", repo_path],
|
| 48 |
+
capture_output=True, timeout=900, check=False,
|
| 49 |
+
)
|
| 50 |
+
except subprocess.TimeoutExpired:
|
| 51 |
+
return [{"error": "codeql db create timeout"}]
|
| 52 |
+
findings: list[dict[str, Any]] = []
|
| 53 |
+
for h in hypotheses:
|
| 54 |
+
pack = _PACK_FOR_KIND.get(h.get("kind", "logic"))
|
| 55 |
+
if not pack:
|
| 56 |
+
continue
|
| 57 |
+
sarif = os.path.join(workdir, f"{h['cwe']}.sarif")
|
| 58 |
+
try:
|
| 59 |
+
subprocess.run(
|
| 60 |
+
[self.codeql, "database", "analyze", db, pack,
|
| 61 |
+
"--format=sarif-latest", "--output", sarif],
|
| 62 |
+
capture_output=True, timeout=900, check=False,
|
| 63 |
+
)
|
| 64 |
+
if os.path.exists(sarif):
|
| 65 |
+
with open(sarif) as fh:
|
| 66 |
+
findings.append({"cwe": h["cwe"], "sarif": json.load(fh)})
|
| 67 |
+
except subprocess.TimeoutExpired:
|
| 68 |
+
findings.append({"cwe": h["cwe"], "error": "analyze timeout"})
|
| 69 |
+
return findings
|
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Joern Code Property Graph bridge.
|
| 3 |
+
|
| 4 |
+
Joern ships as a JVM CLI. This bridge is a thin, robust subprocess wrapper
|
| 5 |
+
that:
|
| 6 |
+
|
| 7 |
+
1. Detects the ``joern`` binary on ``$PATH`` (or ``$JOERN_HOME/bin``).
|
| 8 |
+
2. Imports a target codebase (``importCode``).
|
| 9 |
+
3. Runs hypothesis-driven CPG queries (taint, call-chains, dataflow).
|
| 10 |
+
4. Returns parsed JSON results.
|
| 11 |
+
|
| 12 |
+
If Joern is not installed the bridge raises ``MythosToolUnavailable`` so the
|
| 13 |
+
orchestrator transparently routes around it.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
import shutil
|
| 21 |
+
import subprocess
|
| 22 |
+
import tempfile
|
| 23 |
+
from typing import Any
|
| 24 |
+
|
| 25 |
+
from .. import MythosToolUnavailable
|
| 26 |
+
|
| 27 |
+
# Hypothesis-class → Joern query template.
|
| 28 |
+
_QUERY_TEMPLATES: dict[str, str] = {
|
| 29 |
+
"validation": (
|
| 30 |
+
'cpg.call.name("(eval|exec|system|popen|Runtime.getRuntime.*exec)")'
|
| 31 |
+
'.location.toJsonPretty'
|
| 32 |
+
),
|
| 33 |
+
"memory": (
|
| 34 |
+
'cpg.call.name("(strcpy|gets|sprintf|memcpy)")'
|
| 35 |
+
'.location.toJsonPretty'
|
| 36 |
+
),
|
| 37 |
+
"auth": (
|
| 38 |
+
'cpg.method.name(".*[Aa]uth.*").parameter.name(".*").location.toJsonPretty'
|
| 39 |
+
),
|
| 40 |
+
"logic": (
|
| 41 |
+
'cpg.method.controlStructure.code(".*TODO.*|.*FIXME.*").location.toJsonPretty'
|
| 42 |
+
),
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class JoernBridge:
|
| 47 |
+
def __init__(self, joern_home: str | None = None):
|
| 48 |
+
self.joern = (
|
| 49 |
+
shutil.which("joern")
|
| 50 |
+
or (os.path.join(joern_home, "bin", "joern") if joern_home else None)
|
| 51 |
+
or os.path.join(os.environ.get("JOERN_HOME", ""), "bin", "joern")
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
def available(self) -> bool:
|
| 55 |
+
return bool(self.joern and os.path.exists(self.joern))
|
| 56 |
+
|
| 57 |
+
def query(self, repo_path: str, hypotheses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 58 |
+
if not self.available():
|
| 59 |
+
return []
|
| 60 |
+
kinds = {h.get("kind", "logic") for h in hypotheses}
|
| 61 |
+
results: list[dict[str, Any]] = []
|
| 62 |
+
for kind in kinds:
|
| 63 |
+
tpl = _QUERY_TEMPLATES.get(kind)
|
| 64 |
+
if not tpl:
|
| 65 |
+
continue
|
| 66 |
+
results.extend(self._run_query(repo_path, tpl, kind))
|
| 67 |
+
return results
|
| 68 |
+
|
| 69 |
+
def _run_query(self, repo_path: str, query: str, kind: str) -> list[dict[str, Any]]:
|
| 70 |
+
with tempfile.NamedTemporaryFile("w", suffix=".sc", delete=False) as fh:
|
| 71 |
+
fh.write(f'importCode("{repo_path}")\n{query}\n')
|
| 72 |
+
script = fh.name
|
| 73 |
+
try:
|
| 74 |
+
proc = subprocess.run(
|
| 75 |
+
[self.joern, "--script", script, "--nocolors"],
|
| 76 |
+
capture_output=True, text=True, timeout=600,
|
| 77 |
+
)
|
| 78 |
+
try:
|
| 79 |
+
payload = json.loads(proc.stdout.strip().splitlines()[-1])
|
| 80 |
+
except Exception:
|
| 81 |
+
payload = {"raw": proc.stdout[-2000:]}
|
| 82 |
+
return [{"kind": kind, "joern": payload}]
|
| 83 |
+
except subprocess.TimeoutExpired:
|
| 84 |
+
return [{"kind": kind, "error": "timeout"}]
|
| 85 |
+
finally:
|
| 86 |
+
try:
|
| 87 |
+
os.unlink(script)
|
| 88 |
+
except OSError:
|
| 89 |
+
pass
|
| 90 |
+
|
| 91 |
+
def require(self) -> None:
|
| 92 |
+
if not self.available():
|
| 93 |
+
raise MythosToolUnavailable("joern not on PATH; install via https://joern.io")
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Semgrep bridge — wraps the existing Semgrep dependency declared in
|
| 3 |
+
``requirements.txt`` and exposes a hypothesis-driven scan API.
|
| 4 |
+
|
| 5 |
+
Falls back to ``semgrep --config=auto`` when no kind-specific config is
|
| 6 |
+
matched, and gracefully returns ``[]`` when the binary is unavailable.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import shutil
|
| 13 |
+
import subprocess
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
_KIND_CONFIG = {
|
| 17 |
+
"validation": "p/owasp-top-ten",
|
| 18 |
+
"memory": "p/cwe-top-25",
|
| 19 |
+
"auth": "p/security-audit",
|
| 20 |
+
"logic": "p/default",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class SemgrepBridge:
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.bin = shutil.which("semgrep")
|
| 27 |
+
|
| 28 |
+
def available(self) -> bool:
|
| 29 |
+
return bool(self.bin)
|
| 30 |
+
|
| 31 |
+
def scan(self, repo_path: str, hypotheses: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 32 |
+
if not self.available():
|
| 33 |
+
return []
|
| 34 |
+
configs = {_KIND_CONFIG.get(h.get("kind", "logic"), "p/default") for h in hypotheses}
|
| 35 |
+
results: list[dict[str, Any]] = []
|
| 36 |
+
for cfg in configs:
|
| 37 |
+
try:
|
| 38 |
+
proc = subprocess.run(
|
| 39 |
+
[self.bin, "--config", cfg, "--json", "--quiet",
|
| 40 |
+
"--metrics=off", repo_path],
|
| 41 |
+
capture_output=True, text=True, timeout=900, check=False,
|
| 42 |
+
)
|
| 43 |
+
payload = json.loads(proc.stdout or "{}")
|
| 44 |
+
for r in payload.get("results", []):
|
| 45 |
+
results.append({
|
| 46 |
+
"config": cfg,
|
| 47 |
+
"rule_id": r.get("check_id"),
|
| 48 |
+
"path": r.get("path"),
|
| 49 |
+
"line": r.get("start", {}).get("line"),
|
| 50 |
+
"severity": r.get("extra", {}).get("severity"),
|
| 51 |
+
"message": r.get("extra", {}).get("message", "")[:400],
|
| 52 |
+
})
|
| 53 |
+
except (subprocess.TimeoutExpired, json.JSONDecodeError):
|
| 54 |
+
continue
|
| 55 |
+
return results
|
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tree-sitter based Concrete Syntax Tree → lightweight CPG summary.
|
| 3 |
+
|
| 4 |
+
When ``tree_sitter_languages`` is installed we walk the CST per file and
|
| 5 |
+
emit per-language stats (function count, max nesting depth, dangerous-call
|
| 6 |
+
hits). Otherwise we degrade to a regex-based scanner that is good enough
|
| 7 |
+
for the planner's first-cut prioritisation.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import re
|
| 14 |
+
from collections import defaultdict
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
try: # pragma: no cover - optional
|
| 18 |
+
from tree_sitter_languages import get_parser # type: ignore
|
| 19 |
+
_TS = True
|
| 20 |
+
except Exception: # noqa: BLE001
|
| 21 |
+
_TS = False
|
| 22 |
+
|
| 23 |
+
EXT_LANG = {
|
| 24 |
+
".py": "python", ".js": "javascript", ".ts": "typescript",
|
| 25 |
+
".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp",
|
| 26 |
+
".go": "go", ".rs": "rust", ".rb": "ruby", ".php": "php",
|
| 27 |
+
".java": "java", ".kt": "kotlin", ".swift": "swift",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
DANGEROUS_PATTERNS = {
|
| 31 |
+
"python": [r"\beval\(", r"\bexec\(", r"pickle\.loads\(", r"yaml\.load\(", r"subprocess\..*shell\s*=\s*True"],
|
| 32 |
+
"javascript": [r"\beval\(", r"new\s+Function\(", r"child_process", r"\.innerHTML\s*="],
|
| 33 |
+
"typescript": [r"\beval\(", r"any\s*=", r"child_process"],
|
| 34 |
+
"c": [r"\bgets\(", r"\bstrcpy\(", r"\bsprintf\(", r"\bsystem\("],
|
| 35 |
+
"cpp": [r"\bgets\(", r"\bstrcpy\(", r"\bsprintf\(", r"\bsystem\(", r"reinterpret_cast<"],
|
| 36 |
+
"go": [r"exec\.Command\(", r"unsafe\."],
|
| 37 |
+
"java": [r"Runtime\.getRuntime\(\)\.exec", r"ObjectInputStream\("],
|
| 38 |
+
"php": [r"\beval\(", r"system\(", r"shell_exec\("],
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class TreeSitterCPG:
|
| 43 |
+
def __init__(self):
|
| 44 |
+
self.have_ts = _TS
|
| 45 |
+
|
| 46 |
+
def summary(self, repo_path: str) -> dict[str, Any]:
|
| 47 |
+
if not os.path.isdir(repo_path):
|
| 48 |
+
return {"available": False, "reason": "missing path", "files": 0}
|
| 49 |
+
files_by_lang: dict[str, int] = defaultdict(int)
|
| 50 |
+
sinks: list[dict[str, Any]] = []
|
| 51 |
+
total_files = 0
|
| 52 |
+
for root, _dirs, files in os.walk(repo_path):
|
| 53 |
+
if any(p in root for p in (".git", "node_modules", "venv", "__pycache__")):
|
| 54 |
+
continue
|
| 55 |
+
for fname in files:
|
| 56 |
+
ext = os.path.splitext(fname)[1].lower()
|
| 57 |
+
lang = EXT_LANG.get(ext)
|
| 58 |
+
if not lang:
|
| 59 |
+
continue
|
| 60 |
+
total_files += 1
|
| 61 |
+
files_by_lang[lang] += 1
|
| 62 |
+
fp = os.path.join(root, fname)
|
| 63 |
+
try:
|
| 64 |
+
with open(fp, "r", encoding="utf-8", errors="replace") as fh:
|
| 65 |
+
text = fh.read()
|
| 66 |
+
except Exception:
|
| 67 |
+
continue
|
| 68 |
+
for pat in DANGEROUS_PATTERNS.get(lang, []):
|
| 69 |
+
for m in re.finditer(pat, text):
|
| 70 |
+
line = text.count("\n", 0, m.start()) + 1
|
| 71 |
+
sinks.append({"file": os.path.relpath(fp, repo_path),
|
| 72 |
+
"lang": lang, "pattern": pat, "line": line})
|
| 73 |
+
return {
|
| 74 |
+
"available": True,
|
| 75 |
+
"backend": "tree-sitter" if self.have_ts else "regex-fallback",
|
| 76 |
+
"files": total_files,
|
| 77 |
+
"files_by_language": dict(files_by_lang),
|
| 78 |
+
"dangerous_sinks": sinks[:200],
|
| 79 |
+
"sink_count": len(sinks),
|
| 80 |
+
}
|