splitagent 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- splitagent/__init__.py +8 -0
- splitagent/__main__.py +6 -0
- splitagent/agents/__init__.py +10 -0
- splitagent/agents/base.py +477 -0
- splitagent/agents/blue.py +57 -0
- splitagent/agents/chat.py +60 -0
- splitagent/agents/prompts.py +462 -0
- splitagent/agents/red.py +75 -0
- splitagent/cli.py +701 -0
- splitagent/config.py +697 -0
- splitagent/core/__init__.py +19 -0
- splitagent/core/bus.py +62 -0
- splitagent/core/context.py +587 -0
- splitagent/core/context_manager.py +381 -0
- splitagent/core/engine.py +424 -0
- splitagent/core/models.py +310 -0
- splitagent/core/proc.py +73 -0
- splitagent/core/sandbox.py +184 -0
- splitagent/core/toolbox.py +520 -0
- splitagent/core/workspace.py +420 -0
- splitagent/desktop/__init__.py +7 -0
- splitagent/desktop/api.py +525 -0
- splitagent/desktop/app.py +1131 -0
- splitagent/desktop/web/app.js +3067 -0
- splitagent/desktop/web/assets/Inter.ttf +0 -0
- splitagent/desktop/web/assets/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- splitagent/desktop/web/index.html +760 -0
- splitagent/desktop/web/styles.css +1612 -0
- splitagent/errors.py +27 -0
- splitagent/llm/__init__.py +8 -0
- splitagent/llm/client.py +488 -0
- splitagent/llm/types.py +172 -0
- splitagent/report/__init__.py +9 -0
- splitagent/report/cvss.py +93 -0
- splitagent/report/generator.py +733 -0
- splitagent/tools/__init__.py +8 -0
- splitagent/tools/base.py +135 -0
- splitagent/tools/defense.py +475 -0
- splitagent/tools/exploit.py +318 -0
- splitagent/tools/http_pool.py +109 -0
- splitagent/tools/knowledge.py +376 -0
- splitagent/tools/recon.py +182 -0
- splitagent/tools/registry.py +62 -0
- splitagent/tools/validate.py +908 -0
- splitagent/tools/web.py +386 -0
- splitagent/tools/workspace_tools.py +411 -0
- splitagent/ui/__init__.py +5 -0
- splitagent/ui/app.py +389 -0
- splitagent/ui/stream.py +234 -0
- splitagent/ui/theme.py +72 -0
- splitagent-0.0.3.dist-info/METADATA +987 -0
- splitagent-0.0.3.dist-info/RECORD +56 -0
- splitagent-0.0.3.dist-info/WHEEL +5 -0
- splitagent-0.0.3.dist-info/entry_points.txt +2 -0
- splitagent-0.0.3.dist-info/licenses/LICENSE +21 -0
- splitagent-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: splitagent
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Autonomous dual-team (Purple Team) security framework powered by configurable LLM APIs.
|
|
5
|
+
Author: SplitAgent Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NahuelGomez-Dev/SplitAgent
|
|
8
|
+
Project-URL: Repository, https://github.com/NahuelGomez-Dev/SplitAgent
|
|
9
|
+
Project-URL: Issues, https://github.com/NahuelGomez-Dev/SplitAgent/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/NahuelGomez-Dev/SplitAgent/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: security,pentest,agents,purple-team,llm,automation
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: rich>=13.7
|
|
22
|
+
Requires-Dist: textual>=0.60
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: PyYAML>=6.0
|
|
25
|
+
Requires-Dist: cryptography>=42.0
|
|
26
|
+
Requires-Dist: pywebview>=5.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
32
|
+
Provides-Extra: http2
|
|
33
|
+
Requires-Dist: h2>=4.1; extra == "http2"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# SplitAgent
|
|
37
|
+
|
|
38
|
+
[](https://github.com/NahuelGomez-Dev/SplitAgent/actions/workflows/ci.yml)
|
|
39
|
+
[](https://pypi.org/project/splitagent/)
|
|
40
|
+
[](https://pypi.org/project/splitagent/)
|
|
41
|
+
[](https://github.com/NahuelGomez-Dev/SplitAgent/blob/main/LICENSE)
|
|
42
|
+
|
|
43
|
+
**Autonomous dual-team (Purple Team) security framework.**
|
|
44
|
+
|
|
45
|
+
SplitAgent automates penetration testing and defensive hardening with two
|
|
46
|
+
teams of AI agents that operate against the same target, in real time:
|
|
47
|
+
|
|
48
|
+
- **Red Agent** — reconnaissance, attack-surface mapping and controlled,
|
|
49
|
+
non-destructive exploitation with evidence.
|
|
50
|
+
- **Blue Agent** — log triage, detection and concrete mitigations (firewall
|
|
51
|
+
rules, hardening config and code patches), plus verification.
|
|
52
|
+
|
|
53
|
+
Everything is driven by a **central orchestrator** that runs Red → Blue rounds,
|
|
54
|
+
keeps an **encrypted shared context** and compiles a professional report with
|
|
55
|
+
CVSS v3.1 scoring.
|
|
56
|
+
|
|
57
|
+
> The model is **never bundled**: SplitAgent talks to *any* model over an HTTP
|
|
58
|
+
> API (OpenAI-compatible or Anthropic). You choose the provider and paste the
|
|
59
|
+
> API key from inside the program — no local AI required.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Highlights
|
|
64
|
+
|
|
65
|
+
- **Dual-agent Purple Team loop** — attack, mitigate, verify, repeat.
|
|
66
|
+
- **Any model, any provider** — OpenAI, OpenRouter, Anthropic, Groq, DeepSeek,
|
|
67
|
+
Together, Mistral, xAI, vLLM, LM Studio, Ollama (OpenAI mode) or a custom
|
|
68
|
+
endpoint.
|
|
69
|
+
- **In-app configuration** — `splitagent config setup` or press `c` in the TUI.
|
|
70
|
+
Keys are stored with `0600` permissions in the global config directory.
|
|
71
|
+
- **Encrypted sessions** — findings and evidence are encrypted at rest with a
|
|
72
|
+
Fernet key (`~/.splitagent/key.bin`).
|
|
73
|
+
- **Ephemeral Docker sandbox** — spin up a disposable vulnerable target
|
|
74
|
+
(`juice-shop`, `dvwa`, `bwapp`, `webgoat`) on an isolated network.
|
|
75
|
+
- **Native desktop application** — a real window (pywebview + WebView2) whose
|
|
76
|
+
interface is built on OpenCode's *Desktop v2* design language: near-black
|
|
77
|
+
canvas, sidebar, central stream, review panel and composer. A Rich streaming
|
|
78
|
+
CLI and a Textual TUI are included too.
|
|
79
|
+
- **Reports** — Markdown, standalone HTML and JSON, with CVSS v3.1 vectors,
|
|
80
|
+
full traceability and ready-to-apply patches.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
+-------------------------------+
|
|
88
|
+
| Core Engine |
|
|
89
|
+
| rounds · shared context |
|
|
90
|
+
| event bus · sandbox control |
|
|
91
|
+
+---------------+---------------+
|
|
92
|
+
|
|
|
93
|
+
+----------------------+----------------------+
|
|
94
|
+
| |
|
|
95
|
+
+---------v----------+ +----------v---------+
|
|
96
|
+
| RED AGENT | encrypted context | BLUE AGENT |
|
|
97
|
+
| recon · probe |<--------------------->| triage · patch |
|
|
98
|
+
| record findings | | firewall · verify |
|
|
99
|
+
+---------+----------+ +----------+---------+
|
|
100
|
+
| |
|
|
101
|
+
+----------------------+----------------------+
|
|
102
|
+
|
|
|
103
|
+
+---------------v---------------+
|
|
104
|
+
| Target / Docker sandbox |
|
|
105
|
+
+-------------------------------+
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The engine emits events (`agent.text`, `agent.tool_call`, `finding`,
|
|
109
|
+
`mitigation`, `round.end`, …) that drive both the CLI stream renderer and the
|
|
110
|
+
Textual TUI.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Installation
|
|
115
|
+
|
|
116
|
+
Requires **Python 3.10+**. Docker is optional (only for the sandbox).
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install splitagent
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
From source:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://github.com/NahuelGomez-Dev/SplitAgent
|
|
126
|
+
cd SplitAgent
|
|
127
|
+
python -m venv .venv
|
|
128
|
+
. .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
|
|
129
|
+
pip install -e ".[dev]"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Quick start
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 1. Configure the model API (once). Stored in ~/.splitagent/config.yaml
|
|
138
|
+
splitagent config setup
|
|
139
|
+
|
|
140
|
+
# 2. Create a project file (splitagent.yaml) for your target
|
|
141
|
+
splitagent init --preset juice-shop -y
|
|
142
|
+
|
|
143
|
+
# 3. Launch the desktop app (or `run --tui` / plain `run` for the terminal)
|
|
144
|
+
splitagent desktop
|
|
145
|
+
|
|
146
|
+
# 4. Reports land in ./reports
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Setup wizard
|
|
150
|
+
|
|
151
|
+
`splitagent config setup` lists the supported providers, lets you override the
|
|
152
|
+
base URL, model, temperature and API key, and validates the credentials with a
|
|
153
|
+
live round-trip before saving. You can also skip the wizard and use environment
|
|
154
|
+
variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `SPLITAGENT_API_KEY`, …).
|
|
155
|
+
|
|
156
|
+
Non-interactive setup:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
splitagent config preset openrouter
|
|
160
|
+
splitagent config set api_key sk-or-...
|
|
161
|
+
splitagent config set model anthropic/claude-3.5-sonnet
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Commands
|
|
167
|
+
|
|
168
|
+
| Command | Description |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `splitagent init` | Create `splitagent.yaml` (interactive or with flags). |
|
|
171
|
+
| `splitagent config setup` | Configure the model API and test it. |
|
|
172
|
+
| `splitagent config show` | Show the global config (key redacted). |
|
|
173
|
+
| `splitagent config set <key> <value>` | Update a single LLM setting. |
|
|
174
|
+
| `splitagent config preset <provider>` | Apply a provider preset. |
|
|
175
|
+
| `splitagent desktop` | Launch the native desktop application. |
|
|
176
|
+
| `splitagent toolbox` | Manage the isolated Docker execution environment. |
|
|
177
|
+
| `splitagent run` | Run the full audit in the terminal. |
|
|
178
|
+
| `splitagent tui` | Launch the interactive Textual interface. |
|
|
179
|
+
| `splitagent report --session <id>` | Re-render reports from a saved session. |
|
|
180
|
+
| `splitagent sandbox up\|down\|status\|logs` | Manage the Docker sandbox. |
|
|
181
|
+
|
|
182
|
+
### Useful `run` flags
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
--url URL Override the target URL
|
|
186
|
+
--rounds N Number of Red/Blue cycles
|
|
187
|
+
--max-steps N Max tool-calling steps per agent turn
|
|
188
|
+
--no-sandbox Do not use Docker
|
|
189
|
+
--no-report Skip report generation
|
|
190
|
+
--allow-network Disable scope enforcement (lab use only)
|
|
191
|
+
--tui Launch the interactive interface
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Desktop application
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
splitagent desktop # or: splitagent run --desktop
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Two work modes, switched from the titlebar:
|
|
203
|
+
|
|
204
|
+
- **Audit** — the autonomous Red vs Blue run.
|
|
205
|
+
- **Copilot** — a conversational assistant that helps you *do* the pentest:
|
|
206
|
+
plan the engagement, explain a vulnerability, inspect the target with the
|
|
207
|
+
full toolset, draft payloads, firewall rules or patches, and interpret
|
|
208
|
+
findings. It is a **single continuous conversation** — ask a follow-up and it
|
|
209
|
+
keeps the whole thread and its context.
|
|
210
|
+
|
|
211
|
+
### One cockpit
|
|
212
|
+
|
|
213
|
+
There is a single interface. The panels adapt to the work:
|
|
214
|
+
|
|
215
|
+
- **Resizable columns** — drag the dividers between the sidebar, the stream and
|
|
216
|
+
the review panel; hide either side with the titlebar buttons or `Ctrl+B`
|
|
217
|
+
(sidebar) / `Ctrl+J` (panel). The centre expands to fill the space.
|
|
218
|
+
- **Dashboard** — after a run, the review panel opens on a summary: overall
|
|
219
|
+
risk, severity distribution, top findings, round timeline and the resilience
|
|
220
|
+
score, all from live session state.
|
|
221
|
+
- **One activity line** — a single `Thinking` / `Exploring` line stays pinned at
|
|
222
|
+
the bottom of the stream and flips verb as the agent works, instead of
|
|
223
|
+
stacking. The full reasoning is one click away.
|
|
224
|
+
- **Response timer** — a small live counter next to the agent's name while it
|
|
225
|
+
answers.
|
|
226
|
+
|
|
227
|
+
The **guided setup** asks a few questions before running (target kind →
|
|
228
|
+
address and scope → sandbox or existing target → optional credentials →
|
|
229
|
+
depth) so you do not have to know the config format.
|
|
230
|
+
|
|
231
|
+
**Models & Providers settings** (`Ctrl+,`) copy OpenCode's own menus:
|
|
232
|
+
|
|
233
|
+
- **Models** — the full catalogue, searchable, grouped by provider with
|
|
234
|
+
collapsible sections and a per-provider/per-model switch, plus "Custom
|
|
235
|
+
model" to add an id the catalogue does not list.
|
|
236
|
+
- **Providers** — **Connected** (base URL, model count, enable switch,
|
|
237
|
+
Refresh, Edit, Disconnect) and **Popular** with a Connect button. Connecting
|
|
238
|
+
asks for the base URL and API key, then **fetches the provider's `/models`
|
|
239
|
+
catalogue automatically** and can make it the active provider.
|
|
240
|
+
- **General** — temperature, max tokens, config-file shortcut and a connection
|
|
241
|
+
test.
|
|
242
|
+
|
|
243
|
+
The **model picker** in the composer is also a faithful copy of OpenCode's:
|
|
244
|
+
a searchable popover grouped by provider with a "Manage models…" entry that
|
|
245
|
+
opens the settings above.
|
|
246
|
+
|
|
247
|
+
A frameless native window (pywebview + the system WebView2 runtime) rendering
|
|
248
|
+
an interface modelled on OpenCode's Desktop v2 design language:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
252
|
+
│ ◇ SplitAgent Audit session ⌘K ⚙ ─ ▢ ✕ │
|
|
253
|
+
├───────────────┬──────────────────────────────────┬───────────────────┤
|
|
254
|
+
│ Engagement │ ▸ Round 1 · offence │ Findings 3 │
|
|
255
|
+
│ web localhost │ RED analysis / tool calls … │ Mitigations 2 │
|
|
256
|
+
│ scope … │ ▸ Round 1 · defence │ Report │
|
|
257
|
+
│ Sessions … │ BLUE triage / patches … │ Activity │
|
|
258
|
+
│ │ ┌────────────────────────────┐ │ │
|
|
259
|
+
│ model chip │ │ objective… Run audit │ │ Export report │
|
|
260
|
+
├───────────────┴──────────────────────────────────┴───────────────────┤
|
|
261
|
+
│ provider/model · round 1/3 · defence · 42 events · resilience ▓▓▓░ 78 │
|
|
262
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- **Quiet, OpenCode-style stream** — nothing floods the timeline while an agent
|
|
266
|
+
works:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Thinking checking the login form for reflected input… ›
|
|
270
|
+
Exploring · 3 scans, 1 request ›
|
|
271
|
+
✓ port_scan {"host":"127.0.0.1"} ›
|
|
272
|
+
✓ http_request {"url":"…/login"} ›
|
|
273
|
+
! test_xss {"parameter":"q"} ›
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Reasoning collapses to **one shimmering line** that becomes `Thought` when
|
|
277
|
+
done, and switches verb dynamically: `Thinking · considering next steps…`
|
|
278
|
+
while the model deliberates, `Exploring · running tools…` while tools run.
|
|
279
|
+
Every action lives in a **collapsible group** (the OpenCode `BasicTool`
|
|
280
|
+
pattern): a single header with verb + counts, and inside it one compact row
|
|
281
|
+
per call — status, title, args — with its **own chevron** that opens the full
|
|
282
|
+
arguments and output. Tool cards are never appended to the timeline directly,
|
|
283
|
+
so long runs stay short. The **Trace** tab always holds the complete replay.
|
|
284
|
+
|
|
285
|
+
- **Task dock** (`SessionTodoDock` port) — a collapsible bar above the composer
|
|
286
|
+
reading `2 of 5 todos completed` with the active task as a preview, a chevron
|
|
287
|
+
that rotates 180°, and a checklist where the in-progress item pulses
|
|
288
|
+
(`pulse-scale 1.2s ease-in-out infinite`), completed items strike through
|
|
289
|
+
with an animated line and cancelled items dim. It appears automatically when
|
|
290
|
+
an agent calls `todowrite`.
|
|
291
|
+
- **Live stream** — Red and Blue messages stream in with tool calls that
|
|
292
|
+
expand to show the exact arguments and output, plus inline severity cards and
|
|
293
|
+
context-checkpoint cards.
|
|
294
|
+
- **Review panel** — Dashboard, Findings, Mitigations, the generated Report,
|
|
295
|
+
the full Trace and an Activity log, all updating in real time.
|
|
296
|
+
- **Composer** — set the objective, pick the model, toggle the Docker sandbox
|
|
297
|
+
and rounds, then **Run audit** (`Ctrl+Enter`). The **Guided** button opens
|
|
298
|
+
the wizard.
|
|
299
|
+
- **Authenticated testing** — optional credentials (username/password, bearer
|
|
300
|
+
token, cookies, custom headers) are injected into the agents' context and
|
|
301
|
+
applied as default headers by the web tools.
|
|
302
|
+
- **In-app model configuration** — the Models/Providers settings manage every
|
|
303
|
+
provider and its models, and *Test connection* validates credentials before
|
|
304
|
+
saving. Nothing is pre-baked: any OpenAI-compatible or Anthropic endpoint
|
|
305
|
+
works, and connected providers are stored in the global config so both the
|
|
306
|
+
Audit and the Copilot modes share them.
|
|
307
|
+
- **Command palette** — `Ctrl+K` for run/stop, settings, export and sandbox
|
|
308
|
+
presets.
|
|
309
|
+
- Sessions are encrypted and can be reopened from the sidebar; reports open
|
|
310
|
+
directly from the review panel.
|
|
311
|
+
|
|
312
|
+
> Requirements: Windows 10/11 with the WebView2 runtime (bundled with Edge),
|
|
313
|
+
> macOS or Linux with GTK/Qt WebKit. On Windows, `pip install pywebview`
|
|
314
|
+
> pulls `pythonnet` automatically.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Configuration
|
|
319
|
+
|
|
320
|
+
### Global — `~/.splitagent/config.yaml`
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
llm:
|
|
324
|
+
provider: openai # any provider preset or "custom"
|
|
325
|
+
protocol: openai # openai | anthropic
|
|
326
|
+
base_url: https://api.openai.com/v1
|
|
327
|
+
api_key: sk-...
|
|
328
|
+
model: gpt-4o-mini
|
|
329
|
+
temperature: 0.2
|
|
330
|
+
max_tokens: 4096
|
|
331
|
+
timeout: 120
|
|
332
|
+
stream: true
|
|
333
|
+
authorized: true # you accepted the responsible-use notice
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Project — `splitagent.yaml`
|
|
337
|
+
|
|
338
|
+
```yaml
|
|
339
|
+
project:
|
|
340
|
+
name: my-audit
|
|
341
|
+
target:
|
|
342
|
+
kind: web # web | api | network | repo
|
|
343
|
+
url: http://localhost:3000
|
|
344
|
+
hosts: []
|
|
345
|
+
ports: [80, 443, 3000]
|
|
346
|
+
scope: [localhost]
|
|
347
|
+
out_of_scope: []
|
|
348
|
+
run:
|
|
349
|
+
rounds: 3
|
|
350
|
+
max_steps: 12
|
|
351
|
+
safe_mode: true
|
|
352
|
+
allow_network: false
|
|
353
|
+
sandbox:
|
|
354
|
+
enabled: true
|
|
355
|
+
image: bkimminich/juice-shop:latest
|
|
356
|
+
network: splitagent-net
|
|
357
|
+
port_map: { "3000": 3000 }
|
|
358
|
+
auth: # optional, for authenticated testing
|
|
359
|
+
username: ""
|
|
360
|
+
password: ""
|
|
361
|
+
token: ""
|
|
362
|
+
cookies: ""
|
|
363
|
+
headers: {}
|
|
364
|
+
workspace: # the agents' own directory
|
|
365
|
+
path: "" # empty -> ./splitagent-workspace
|
|
366
|
+
allow_install: true
|
|
367
|
+
allow_external_tools: true
|
|
368
|
+
instructions: ""
|
|
369
|
+
run:
|
|
370
|
+
execution: # isolated Docker environment
|
|
371
|
+
mode: auto # auto | toolbox | local
|
|
372
|
+
edition: standard # standard | kali
|
|
373
|
+
auto_start: true
|
|
374
|
+
agents:
|
|
375
|
+
red: { enabled: true, temperature: 0.3 }
|
|
376
|
+
blue: { enabled: true, temperature: 0.2 }
|
|
377
|
+
report:
|
|
378
|
+
formats: [markdown, html, json]
|
|
379
|
+
output_dir: reports
|
|
380
|
+
include_patches: true
|
|
381
|
+
include_evidence: true
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Context & token management
|
|
387
|
+
|
|
388
|
+
Ported from OpenCode's session model (`session/overflow.ts`,
|
|
389
|
+
`session/compaction.ts`, `tool/truncate.ts`):
|
|
390
|
+
|
|
391
|
+
| Concept | Where | Behaviour |
|
|
392
|
+
| --- | --- | --- |
|
|
393
|
+
| Usable budget | `core/context_manager.py:usable` | `context_window − reserved`, reserving room for the reply (buffer capped at 20 000). |
|
|
394
|
+
| Overflow | `is_overflow` | Compares the last turn's usage against that budget. |
|
|
395
|
+
| **Prompt caching** | `llm/client.py:_with_cache_points` | System prompt + the newest messages are marked as cache breakpoints, exactly as OpenCode's `applyCaching` does. Anthropic/Bedrock/OpenRouter get `cache_control: {type: "ephemeral"}`; OpenAI-compatible gateways cache automatically on a stable prefix. Measured at **79.8%** cache hit rate. |
|
|
396
|
+
| **Stable prefix** | `agents/prompts.py` | The system prompt is **byte-identical for the whole session**. Round counter, findings, task list, notes and tool inventory are appended to the newest user turn by `volatile_context()` instead of living in the system prompt — otherwise every request invalidates the cache. |
|
|
397
|
+
| Per-request projection | `optimize` | Runs before **every** request: drops the reasoning of settled steps, blanks superseded state snapshots, prunes old tool output. This is what keeps the billed prompt small even on 1M-token models. |
|
|
398
|
+
| Reasoning stripping | `strip_reasoning` | Reasoning is scaffolding — once a step produced tool calls it has been acted on. Only the last turn keeps it (OpenCode does the same per step). |
|
|
399
|
+
| Snapshot superseding | `supersede_stateful_results` | `workspace_info`, `read_shared_context`, `list_findings`… return full current state; only the newest copy is meaningful. |
|
|
400
|
+
| Pruning | `prune` | Walks backwards keeping the newest tool result, protects ~40 000 tokens of recent tool output, then blanks older results to `"[Old tool result content cleared]"`. Savings under 20 000 tokens are discarded. |
|
|
401
|
+
| Compaction | `compact` | Keeps a tail of recent turns within ~25 % of the budget (clamped to 2 000–15 000) and replaces the head with a structured checkpoint message. |
|
|
402
|
+
| Tool bounding | `prune_tool_output` | Any tool result entering the conversation is capped at 2 000 chars. |
|
|
403
|
+
|
|
404
|
+
Each model carries a context/output limit (`config.model_spec`, overridable per
|
|
405
|
+
provider in the settings) that feeds the policy. Everything is configurable in
|
|
406
|
+
`splitagent.yaml`:
|
|
407
|
+
|
|
408
|
+
```yaml
|
|
409
|
+
run:
|
|
410
|
+
compaction:
|
|
411
|
+
auto: true
|
|
412
|
+
prune: true
|
|
413
|
+
reserved: null # null = min(20k, max_output)
|
|
414
|
+
preserve_recent_tokens: null # null = clamp(25% of budget, 2k..15k)
|
|
415
|
+
tail_turns: null # limit how many turns are kept
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Why the prefix must stay stable
|
|
419
|
+
|
|
420
|
+
Caching only works if the prefix is byte-identical between requests. The
|
|
421
|
+
runtime enforces that split:
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
system -> RED_SYSTEM + engagement + scope + workspace paths (frozen)
|
|
425
|
+
history -> previous turns
|
|
426
|
+
user (last) -> the task + "=== CURRENT STATE ===" + findings, todo list,
|
|
427
|
+
round counter, notes (fresh every step)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
So findings can be recorded mid-run without invalidating a single cached
|
|
431
|
+
token. Disable it if you need to compare against a cold prompt:
|
|
432
|
+
|
|
433
|
+
```yaml
|
|
434
|
+
# global config
|
|
435
|
+
llm:
|
|
436
|
+
prompt_cache: true
|
|
437
|
+
cache_system_messages: 2 # how many leading system messages to mark
|
|
438
|
+
cache_tail_messages: 2 # how many trailing messages to mark
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Measured on a real run (OpenCode Go / DeepSeek V4.1 Flash):
|
|
442
|
+
|
|
443
|
+
```text
|
|
444
|
+
step 1: prompt= 4363 cached= 0
|
|
445
|
+
step 2: prompt= 4871 cached= 4736
|
|
446
|
+
step 3: prompt= 5170 cached= 4992
|
|
447
|
+
step 4: prompt= 5325 cached= 5120
|
|
448
|
+
step 5: prompt= 5623 cached= 5376
|
|
449
|
+
cache hit rate: 79.8%
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Full trace & replay
|
|
453
|
+
|
|
454
|
+
Nothing is lost when context is trimmed. Every reasoning step, tool call with
|
|
455
|
+
its exact arguments, the full tool output, token usage and each context event
|
|
456
|
+
are recorded:
|
|
457
|
+
|
|
458
|
+
- **Trace tab** — a live, colour-coded replay panel; "Dump JSON" writes the
|
|
459
|
+
whole trace to `~/.splitagent/traces/`.
|
|
460
|
+
- **Session file** — encrypted checkpoints and per-agent traces travel with the
|
|
461
|
+
session, so a finished audit can be replayed end to end.
|
|
462
|
+
- **Status bar** — a live context meter (`ctx 42%`) that turns amber then
|
|
463
|
+
orange as the window fills.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Tools
|
|
468
|
+
|
|
469
|
+
**Shared** — `todowrite`, `record_finding`, `list_findings`, `get_finding`,
|
|
470
|
+
`record_mitigation`, `read_shared_context`.
|
|
471
|
+
|
|
472
|
+
**Workspace** — `workspace_info`, `workspace_write`, `workspace_read`,
|
|
473
|
+
`workspace_list`, `install_tool`, `run_tool`.
|
|
474
|
+
|
|
475
|
+
`todowrite` is a faithful port of OpenCode's task tool: the same description
|
|
476
|
+
(proactive when 3+ steps, one `in_progress` at a time, `pending` /
|
|
477
|
+
`in_progress` / `completed` / `cancelled`, `high` / `medium` / `low`) and the
|
|
478
|
+
same `{content, status, priority}` item shape. The list is stored in the
|
|
479
|
+
session and surfaced in the UI as a collapsible **task dock**.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Real servers, not just labs
|
|
484
|
+
|
|
485
|
+
SplitAgent audits **any reachable host**: a production web app, an API, a
|
|
486
|
+
public IP, an internal range. The sandbox is a convenience for practising, not
|
|
487
|
+
a requirement.
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
splitagent init --url https://your-server.example.com -y
|
|
491
|
+
splitagent run
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Pointing at a real server used to be a trap: `init` left the sandbox enabled,
|
|
495
|
+
so the run would spin up a Juice Shop container and audit *that* instead of
|
|
496
|
+
your server. It now detects the difference and configures itself:
|
|
497
|
+
|
|
498
|
+
| Target | Sandbox | Network |
|
|
499
|
+
| --- | --- | --- |
|
|
500
|
+
| `example.com`, `api.example.com` | **off** | allowed |
|
|
501
|
+
| `93.184.216.34` (public IP) | **off** | allowed |
|
|
502
|
+
| `localhost`, `127.0.0.1` | on | scope-bound |
|
|
503
|
+
| `192.168.x.x`, `10.x.x.x` | on | scope-bound |
|
|
504
|
+
|
|
505
|
+
The engine enforces the same rule at run time, so even a hand-edited config
|
|
506
|
+
cannot make it audit a container instead of the host you asked for. Verified
|
|
507
|
+
against a live internet target:
|
|
508
|
+
|
|
509
|
+
```text
|
|
510
|
+
target=scanme.nmap.org
|
|
511
|
+
external detected : True
|
|
512
|
+
docker 'up' launched : False
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
A full agent audit of `scanme.nmap.org` (a real, internet-reachable host):
|
|
516
|
+
|
|
517
|
+
```text
|
|
518
|
+
tools used : dns_lookup, port_scan, run_tool, check_tool,
|
|
519
|
+
record_finding, workspace_write, todo…
|
|
520
|
+
findings : Outdated Apache httpd 2.4.7 (Ubuntu) CVSS 6.5
|
|
521
|
+
Outdated OpenSSH 6.6.1p1 (Ubuntu) CVSS 5.3
|
|
522
|
+
Service version banner disclosure CVSS 5.3
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Scope enforcement
|
|
526
|
+
|
|
527
|
+
`target.scope` is the authorisation list. Any tool that reaches a host outside
|
|
528
|
+
it raises `ScopeError` and the run continues with the rest:
|
|
529
|
+
|
|
530
|
+
```yaml
|
|
531
|
+
target:
|
|
532
|
+
url: https://app.example.com
|
|
533
|
+
scope: [app.example.com] # only these hosts may be touched
|
|
534
|
+
out_of_scope: [admin.example.com]
|
|
535
|
+
run:
|
|
536
|
+
allow_network: true # required for non-local targets
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Set `run.allow_network: false` to hard-confine a run to `target.scope`, which
|
|
540
|
+
is what a lab or a segmented engagement wants.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Isolated toolbox (recommended)
|
|
545
|
+
|
|
546
|
+
The agents install and run their security tooling **inside a disposable Docker
|
|
547
|
+
container**, so your computer is never modified. This is the default in `auto`
|
|
548
|
+
mode and it is what makes a run reproducible: same tools, same versions, every
|
|
549
|
+
time.
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
splitagent toolbox status # docker? daemon? image? container?
|
|
553
|
+
splitagent toolbox install # build the image and start it
|
|
554
|
+
splitagent toolbox up|down|reset # lifecycle
|
|
555
|
+
splitagent toolbox shell # prints the docker exec command
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### One-time consent, then silent
|
|
559
|
+
|
|
560
|
+
On first launch the app checks for Docker and, if the image is not built yet,
|
|
561
|
+
asks **once**:
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
┌ Set up the isolated environment ────────────────────────┐
|
|
565
|
+
│ Docker installed │
|
|
566
|
+
│ Engine running │
|
|
567
|
+
│ Image not built │
|
|
568
|
+
│ Workspace …\splitagent-workspace │
|
|
569
|
+
│ │
|
|
570
|
+
│ Edition [ Standard — Debian + recon tools (~450 MB) ▾ ] │
|
|
571
|
+
│ │
|
|
572
|
+
│ [Continue without it] [Later] [Install environment]│
|
|
573
|
+
└───────────────────────────────────────────────────────────┘
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
After that it never asks again: if the daemon is stopped SplitAgent **starts
|
|
577
|
+
Docker Desktop itself** and brings the container up in the background. You
|
|
578
|
+
never have to touch Docker again.
|
|
579
|
+
|
|
580
|
+
### Editions
|
|
581
|
+
|
|
582
|
+
| Edition | Base | Size | Contents |
|
|
583
|
+
| --- | --- | --- | --- |
|
|
584
|
+
| `standard` *(default)* | Debian bookworm-slim | ~450 MB | nmap, masscan, nikto (pip), sqlmap, whatweb, wafw00f, gobuster, ffuf, dirb, nuclei, subfinder, httpx, dnsx, curl, git, python3 + venv |
|
|
585
|
+
| `kali` | kalilinux/kali-rolling | ~2.5 GB | the above via `kali-linux-headless`, plus the wider Kali toolset |
|
|
586
|
+
|
|
587
|
+
Go-based tools are compiled against a modern Go toolchain at build time (the
|
|
588
|
+
one in Debian's repos is too old), then the toolchain is discarded.
|
|
589
|
+
|
|
590
|
+
### How execution is routed
|
|
591
|
+
|
|
592
|
+
```yaml
|
|
593
|
+
run:
|
|
594
|
+
execution:
|
|
595
|
+
mode: auto # auto | toolbox | local
|
|
596
|
+
edition: standard # standard | kali
|
|
597
|
+
auto_start: true # start Docker Desktop silently when needed
|
|
598
|
+
network_mode: bridge # bridge | host
|
|
599
|
+
allow_install: true
|
|
600
|
+
cpus: "" # e.g. "2"
|
|
601
|
+
memory: "" # e.g. "2g"
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
| Mode | Behaviour |
|
|
605
|
+
| --- | --- |
|
|
606
|
+
| `auto` *(default)* | Use the toolbox when Docker works, otherwise fall back to local. |
|
|
607
|
+
| `toolbox` | Require the toolbox; the run fails with a clear message if Docker is missing. |
|
|
608
|
+
| `local` | Legacy: install and run on the host. |
|
|
609
|
+
|
|
610
|
+
`install_tool`, `run_tool` and `check_tool` become backend-aware: in toolbox
|
|
611
|
+
mode they run `docker exec` against the container, with the workspace mounted
|
|
612
|
+
at `/workspace`. The agent's tool output reports `"backend": "toolbox"`, and
|
|
613
|
+
`workspace_info` tells the model it is sandboxed and can install freely.
|
|
614
|
+
|
|
615
|
+
```text
|
|
616
|
+
run_tool nmap --version -> backend toolbox
|
|
617
|
+
Nmap version 7.93 ( x86_64-pc-linux-gnu )
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
The container is on `splitagent-net`, the same isolated network as the target
|
|
621
|
+
sandbox, and gets `NET_RAW`/`NET_ADMIN` (needed by `nmap -sS`). Without
|
|
622
|
+
`network_mode: host` it cannot reach your LAN, which keeps a mis-scoped scan
|
|
623
|
+
contained.
|
|
624
|
+
|
|
625
|
+
### Security properties
|
|
626
|
+
|
|
627
|
+
- The host is **never modified**: no winget, no `npm -g`, no system packages.
|
|
628
|
+
- Only the workspace directory is bind-mounted; no Docker socket, no home.
|
|
629
|
+
- The container is disposable — `toolbox reset` recreates it from scratch
|
|
630
|
+
while keeping everything under `/workspace`.
|
|
631
|
+
- With no Docker at all, SplitAgent degrades to local mode and the native
|
|
632
|
+
Python tools keep working.
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## Agent workspace
|
|
637
|
+
|
|
638
|
+
The agents get their own persistent directory — a place to install tooling,
|
|
639
|
+
keep reconnaissance output, write notes and carry context between runs (the
|
|
640
|
+
same idea as OpenCode rooting an agent in a working directory and reading
|
|
641
|
+
`AGENTS.md` from it).
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
splitagent-workspace/
|
|
645
|
+
AGENTS.md operator instructions (highest priority in the system prompt)
|
|
646
|
+
README.md
|
|
647
|
+
tools/ virtualenvs, installed packages, cloned repos, tools/bin
|
|
648
|
+
recon/ raw scans, crawls, HTTP captures
|
|
649
|
+
loot/ downloaded artefacts and evidence
|
|
650
|
+
notes/ the agent's own structured context (plan.md, findings.md, …)
|
|
651
|
+
sessions/ per-run logs and traces
|
|
652
|
+
cache/
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Configured under `workspace` in `splitagent.yaml` (or step 4 of the guided
|
|
656
|
+
setup):
|
|
657
|
+
|
|
658
|
+
```yaml
|
|
659
|
+
workspace:
|
|
660
|
+
path: "" # empty -> <project>/splitagent-workspace
|
|
661
|
+
allow_install: true # let the agents install tooling
|
|
662
|
+
allow_external_tools: true # let them run commands
|
|
663
|
+
max_install_seconds: 900
|
|
664
|
+
instructions: "" # inline operator guidance
|
|
665
|
+
instruction_files: [] # extra files to load into the prompt
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- **Check before installing** — `check_tool` resolves a binary across `PATH`,
|
|
669
|
+
the workspace `tools/bin` and Go's bin, and returns install suggestions when
|
|
670
|
+
missing. `workspace_info` reports which common security tools are already
|
|
671
|
+
present (`nmap`, `nuclei`, `ffuf`, `gobuster`, `sqlmap`, `nikto`,
|
|
672
|
+
`subfinder`, `httpx`, `masscan`, `whatweb`).
|
|
673
|
+
- **Install tooling** — `install_tool` supports pip (into an isolated
|
|
674
|
+
`tools/venv`), pipx, npm, go, cargo, apt, brew, **winget** and `git clone`.
|
|
675
|
+
Go installs are redirected to `tools/bin` via `GOBIN` so they are immediately
|
|
676
|
+
runnable. Known tools carry preferred `(manager, package)` pairs, e.g.
|
|
677
|
+
`nmap` → winget `Insecure.Nmap`, `ffuf` → `go install
|
|
678
|
+
github.com/ffuf/ffuf/v2@latest`.
|
|
679
|
+
- **Run tooling** — `run_tool` executes a command with the workspace as cwd; the
|
|
680
|
+
binary is resolved across `PATH`, `tools/bin` (including Go installs) and the
|
|
681
|
+
pip venv, and `SPLITAGENT_WORKSPACE`, `SPLITAGENT_TOOLS`,
|
|
682
|
+
`SPLITAGENT_RECON` and `SPLITAGENT_NOTES` are exported. So `nmap -sV`,
|
|
683
|
+
`nuclei`, `ffuf` and friends run straight from the workspace.
|
|
684
|
+
|
|
685
|
+
```text
|
|
686
|
+
run_tool nmap -Pn -sT -p 80,443 127.0.0.1 -> Nmap scan report, 80/tcp open
|
|
687
|
+
```
|
|
688
|
+
- **Instructions** — `AGENTS.md` plus anything in `instructions` /
|
|
689
|
+
`instruction_files` is injected into every agent's system prompt under
|
|
690
|
+
*OPERATOR INSTRUCTIONS (highest priority)*.
|
|
691
|
+
- The desktop sidebar shows a **Workspace** card with the file count; click it
|
|
692
|
+
to open the directory in the file manager.
|
|
693
|
+
|
|
694
|
+
### Speed
|
|
695
|
+
|
|
696
|
+
Three changes, each measured against the real target:
|
|
697
|
+
|
|
698
|
+
| Change | Where | Measured |
|
|
699
|
+
| --- | --- | --- |
|
|
700
|
+
| **Parallel tool calls** | `agents/base.py:_execute_tools` | **19.2x** — 5 independent probes went from 3.32 s to 0.17 s |
|
|
701
|
+
| **Shared connection pool** | `tools/http_pool.py` | **2.07x** — 10 HTTP requests from 208 ms to 101 ms (no repeat TCP/TLS/DNS) |
|
|
702
|
+
| **Prompt caching** | `llm/client.py` | **62-80% cache hit rate** on real runs |
|
|
703
|
+
|
|
704
|
+
Tools declare `parallel_safe`. Read-only probes (`audit_security_headers`,
|
|
705
|
+
`test_cors`, `probe_paths`, `dns_lookup`…) run concurrently bounded by
|
|
706
|
+
`run.tool_concurrency`; anything that mutates state, installs or shells out
|
|
707
|
+
(`run_tool`, `install_tool`) keeps the ordered sequential path. Results are
|
|
708
|
+
always reassembled in the model's original call order, so the transcript is
|
|
709
|
+
deterministic regardless of completion order.
|
|
710
|
+
|
|
711
|
+
Live end-to-end run, same task and model:
|
|
712
|
+
|
|
713
|
+
```text
|
|
714
|
+
wall clock : 81.5s for 8 steps (10.2s/step)
|
|
715
|
+
tool calls : 24 (3.0/step)
|
|
716
|
+
prompt tokens : 74,387
|
|
717
|
+
cached tokens : 46,720 (62.8% hit rate)
|
|
718
|
+
optimizer saved : 1,318 tokens
|
|
719
|
+
wrapped up : True | hit limit: False
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
```yaml
|
|
723
|
+
run:
|
|
724
|
+
tool_concurrency: 4 # bounded parallelism, polite to the target
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Findings are deduplicated
|
|
728
|
+
|
|
729
|
+
The Red Agent re-tests the same surface every round, so the raw output filled
|
|
730
|
+
with near-identical entries. Two findings are merged when they share an
|
|
731
|
+
endpoint **and** either the same category (other than the default `general`) or
|
|
732
|
+
strongly overlapping titles:
|
|
733
|
+
|
|
734
|
+
```
|
|
735
|
+
endpoint normalised + category matches -> merge
|
|
736
|
+
endpoint normalised + decisive token matches -> merge
|
|
737
|
+
endpoint normalised + >50% identity overlap -> merge
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
The merge keeps the highest CVSS score and appends the new evidence, so nothing
|
|
741
|
+
is lost. Precision matters as much as recall here: `Missing CSP header` and
|
|
742
|
+
`Missing HSTS header`, or `SQL injection` and `XSS` on the same URL, stay
|
|
743
|
+
separate entries.
|
|
744
|
+
|
|
745
|
+
Measured on a real Metasploitable 2 run: **16 raw findings became 7 distinct
|
|
746
|
+
ones**, with all 7 genuinely different.
|
|
747
|
+
|
|
748
|
+
### Versioned service detection
|
|
749
|
+
|
|
750
|
+
The single biggest efficacy factor. `nmap -sV` returns `vsftpd 2.3.4` or
|
|
751
|
+
`Metasploitable root shell`, which map directly to CVEs. The Red Agent's prompt
|
|
752
|
+
now makes `check_tool` -> `run_tool <scanner>` the first move on a network
|
|
753
|
+
target, before any HTTP probing:
|
|
754
|
+
|
|
755
|
+
```text
|
|
756
|
+
run_tool nmap -Pn -sV -p <ports> <host>
|
|
757
|
+
run_tool nmap --script vuln -p <ports> <host>
|
|
758
|
+
run_tool nikto -h http://<host>
|
|
759
|
+
run_tool nuclei -u http://<host> -severity critical,high
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Validated, not guessed
|
|
763
|
+
|
|
764
|
+
A version banner is a lead; a pentest *proves* the flaw. The Red Agent has
|
|
765
|
+
validators that trigger the vulnerability, observe a benign side effect and
|
|
766
|
+
clean up:
|
|
767
|
+
|
|
768
|
+
| Validator | Proves | How |
|
|
769
|
+
| --- | --- | --- |
|
|
770
|
+
| `validate_vsftpd_backdoor` | CVE-2011-2523 | Sends the `:)` trigger and checks whether TCP/6200 opens. Runs nothing on the shell. |
|
|
771
|
+
| `validate_root_shell` | Unauthenticated shell | Reads the prompt (`root@host:/#`), disconnects. |
|
|
772
|
+
| `validate_samba_usermap` | CVE-2007-2447 | Sends the username payload, or asks `nmap` when a toolbox is available. |
|
|
773
|
+
| `validate_mysql_blank_password` | Exposed database | Parses the handshake version, returns the exact confirming command. |
|
|
774
|
+
| `validate_unrealircd_backdoor` | CVE-2010-2075 | Sends the `AB` token to the IRC daemon. |
|
|
775
|
+
| `validate_vnc_no_auth` | Exposed VNC | Reads the RFB handshake; no session is opened. |
|
|
776
|
+
| `validate_nfs_export` | World-readable NFS | Runs `showmount -e` for the real export list. |
|
|
777
|
+
| `validate_proftpd` | ProFTPD (incl. 2121) | Matches the banner to the product, so a vsftpd port is not mislabelled. |
|
|
778
|
+
| `validate_open_shell_port` | Any unauthenticated shell | Banner check with no command execution. |
|
|
779
|
+
|
|
780
|
+
Every one reports `validated: true/false` with raw evidence, and refuses to
|
|
781
|
+
claim success when it cannot observe the effect. Verified against a live
|
|
782
|
+
Metasploitable 2:
|
|
783
|
+
|
|
784
|
+
```text
|
|
785
|
+
1524 root shell -> validated: True 'root@metasploitable:/#'
|
|
786
|
+
vsftpd backdoor -> validated: True trigger opened TCP/6200 (root shell)
|
|
787
|
+
proftpd 2121 -> validated: True 'ProFTPD 1.3.1 Server (Debian)'
|
|
788
|
+
proftpd on 21 -> validated: False 'runs a different FTP daemon (vsFTPd)'
|
|
789
|
+
nfs export -> validated: False 'no exports: NFS is exposing nothing'
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Two rules the validators follow:
|
|
793
|
+
|
|
794
|
+
- **A missing tool is not a finding.** If `showmount` is unavailable the result
|
|
795
|
+
says so and explicitly instructs the model not to record it, instead of
|
|
796
|
+
parsing the error as a world-readable export.
|
|
797
|
+
- **The product must match.** A vsftpd banner is never reported as a ProFTPD
|
|
798
|
+
finding.
|
|
799
|
+
|
|
800
|
+
Findings carry `confidence: high` only when a validator confirmed them.
|
|
801
|
+
|
|
802
|
+
### Sweep every port
|
|
803
|
+
|
|
804
|
+
A curated port list is how a backdoor on 6200, an IRC trojan on 6667 or an NFS
|
|
805
|
+
export on 2049 stays hidden. `port_scan` therefore supports a full sweep:
|
|
806
|
+
|
|
807
|
+
```text
|
|
808
|
+
port_scan {"host": "<target>", "full": true}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
All 65535 ports in about 45 seconds, then validated one by one.
|
|
812
|
+
|
|
813
|
+
### Resilience
|
|
814
|
+
|
|
815
|
+
Long engagements used to die on a single provider hiccup. Now:
|
|
816
|
+
|
|
817
|
+
- **Retries with exponential backoff** on transient failures (429, 5xx,
|
|
818
|
+
timeouts, resets). Permanent errors (401, 400, model not found) fail fast so
|
|
819
|
+
the real cause surfaces. A stream that already produced content is never
|
|
820
|
+
retried blindly.
|
|
821
|
+
- **Wall-clock deadline** (`run.max_duration_minutes`, default 90) so an
|
|
822
|
+
engagement cannot hang forever; it stops cleanly with everything persisted.
|
|
823
|
+
|
|
824
|
+
```yaml
|
|
825
|
+
run:
|
|
826
|
+
max_duration_minutes: 90
|
|
827
|
+
|
|
828
|
+
# global config
|
|
829
|
+
llm:
|
|
830
|
+
max_retries: 3
|
|
831
|
+
retry_initial_delay: 1.0
|
|
832
|
+
retry_max_delay: 30.0
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Never run out of steps
|
|
836
|
+
|
|
837
|
+
Production runs used to end with `reached the step limit without a final
|
|
838
|
+
answer`: the agent burned its whole budget exploring and returned nothing.
|
|
839
|
+
The runtime now reserves the last `wrap_up_at` steps (default 3) and injects a
|
|
840
|
+
mandatory wrap-up instruction:
|
|
841
|
+
|
|
842
|
+
```
|
|
843
|
+
You are almost out of steps (3 left of 20).
|
|
844
|
+
STOP exploring. Do this now, in this order:
|
|
845
|
+
1. Persist every weakness you have already confirmed with record_finding…
|
|
846
|
+
2. Save your working state with workspace_write to notes/<scope>-state.md…
|
|
847
|
+
3. Reply with a concise markdown summary…
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
If even that turn produces no text, `_fallback_summary()` builds one from the
|
|
851
|
+
persisted state (findings, tool calls, remaining task list) instead of a bare
|
|
852
|
+
error line. The `agent.wrap_up` event drives the UI phase indicator.
|
|
853
|
+
|
|
854
|
+
Configure it under `agents`:
|
|
855
|
+
|
|
856
|
+
```yaml
|
|
857
|
+
agents:
|
|
858
|
+
red:
|
|
859
|
+
wrap_up_at: 3 # steps reserved for persisting + summarising (0 = off)
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Plan-before-you-scan
|
|
863
|
+
|
|
864
|
+
The Red Agent's prompt mandates a planning phase **before launching anything**:
|
|
865
|
+
|
|
866
|
+
1. `workspace_info` — what tooling and package managers exist.
|
|
867
|
+
2. `read_shared_context` + `list_findings` — what is already known.
|
|
868
|
+
3. Write `notes/<topic>.md`, then register the plan with `todowrite`, covering:
|
|
869
|
+
- which mapping technique fits the target (passive vs active, web vs network
|
|
870
|
+
vs API) and why;
|
|
871
|
+
- which tools answer the question with the fewest requests (reuse before
|
|
872
|
+
installing);
|
|
873
|
+
- **how to avoid being blocked** — realistic User-Agent and headers,
|
|
874
|
+
throttling and jitter, low thread counts, limited port ranges,
|
|
875
|
+
retry-with-backoff, rate limits;
|
|
876
|
+
- the fallback when a control blocks you (WAF, 403/429, connection resets):
|
|
877
|
+
change technique or slow down rather than brute-forcing through.
|
|
878
|
+
|
|
879
|
+
**Red** — `dns_lookup`, `port_scan`, `http_request`, `audit_security_headers`,
|
|
880
|
+
`probe_paths`, `crawl`, `test_sql_injection`, `test_xss`,
|
|
881
|
+
`test_path_traversal`, `test_open_redirect`, `test_command_injection`,
|
|
882
|
+
`test_cors`, `test_http_methods`, `test_directory_listing`.
|
|
883
|
+
|
|
884
|
+
**Blue** — `analyze_logs`, `generate_firewall_rule`, `harden_headers`,
|
|
885
|
+
`suggest_patch`, `verify_control`.
|
|
886
|
+
|
|
887
|
+
All probes are non-destructive: they inject benign markers and inspect the
|
|
888
|
+
response. No tool deletes data, writes files on the target or opens a shell.
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## Reports
|
|
893
|
+
|
|
894
|
+
Each session produces:
|
|
895
|
+
|
|
896
|
+
- **Markdown** — portable, versionable.
|
|
897
|
+
- **HTML** — a standalone, **printable A4** document with a cover, document
|
|
898
|
+
control and the full penetration-test structure.
|
|
899
|
+
- **JSON** — machine-readable, including metrics and the full session state.
|
|
900
|
+
|
|
901
|
+
The report layout is **owned by code, not by the model**, so two sessions with
|
|
902
|
+
the same data produce the same document, byte for byte. The AI only fills
|
|
903
|
+
structured fields; it never composes the document. Every report follows the same
|
|
904
|
+
section order:
|
|
905
|
+
|
|
906
|
+
| # | Section |
|
|
907
|
+
| --- | --- |
|
|
908
|
+
| — | Document control (version, classification, scope, model, dates) |
|
|
909
|
+
| 1 | Executive summary (overall risk, severity counts, top findings, resilience) |
|
|
910
|
+
| 2 | Scope and rules of engagement |
|
|
911
|
+
| 3 | Methodology and standards (OWASP WSTG/ASVS, PTES, NIST SP 800-115, ATT&CK) |
|
|
912
|
+
| 4 | Severity model (CVSS v3.1 bands) |
|
|
913
|
+
| 5 | Findings summary table |
|
|
914
|
+
| 6 | Detailed findings (one repeatable block per finding) |
|
|
915
|
+
| 7 | Remediation roadmap (priority + target SLA) |
|
|
916
|
+
| 8 | Retest and status tracking |
|
|
917
|
+
| 9 | Appendices (model usage, notes, round timeline) |
|
|
918
|
+
| 10 | Limitations and disclaimer |
|
|
919
|
+
|
|
920
|
+
Each finding block carries CVSS v3.1 score **and vector**, **CWE**, **OWASP**
|
|
921
|
+
category, affected asset, description, business **impact**, evidence, steps to
|
|
922
|
+
**reproduce**, remediation and status. Fields the model did not provide render as
|
|
923
|
+
*"Not provided"* rather than being guessed or left blank.
|
|
924
|
+
|
|
925
|
+
The **Dashboard** tab is the at-a-glance counterpart inside the app: overall
|
|
926
|
+
risk, severity distribution, top findings, round timeline and resilience,
|
|
927
|
+
rendered live from the session.
|
|
928
|
+
|
|
929
|
+
### The resilience score tells the truth
|
|
930
|
+
|
|
931
|
+
The score counts **only findings whose mitigation was re-tested** by
|
|
932
|
+
`verify_control` and confirmed to hold:
|
|
933
|
+
|
|
934
|
+
| State | Score |
|
|
935
|
+
| --- | --- |
|
|
936
|
+
| Finding with no mitigation | 0 |
|
|
937
|
+
| Mitigation written but never applied | **0** |
|
|
938
|
+
| `verify_control` on an unpatched target | **0**, `still_exploitable: true` |
|
|
939
|
+
| Fix applied and confirmed by re-test | counted |
|
|
940
|
+
|
|
941
|
+
Proposing a firewall rule changes nothing on the target, so scoring proposals
|
|
942
|
+
as fixes reports a confident number that is untrue — which is worse than
|
|
943
|
+
reporting nothing, because the operator stops looking. When nothing has been
|
|
944
|
+
verified the report says so in as many words:
|
|
945
|
+
|
|
946
|
+
> **Nothing is verified yet.** Every mitigation below is a proposal. The score
|
|
947
|
+
> counts only issues re-tested after a control was applied, so it will not move
|
|
948
|
+
> until the fixes are actually deployed and the audit is re-run.
|
|
949
|
+
|
|
950
|
+
A lab target with no operator applying patches therefore scores **0**, which is
|
|
951
|
+
correct: every hole is still open.
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
## Responsible use
|
|
956
|
+
|
|
957
|
+
SplitAgent is for **authorised security testing only**. You must have explicit
|
|
958
|
+
permission to test the target. The framework enforces the configured `scope`
|
|
959
|
+
and refuses out-of-scope hosts; safe mode is on by default. The authors accept
|
|
960
|
+
no liability for misuse.
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
## Development
|
|
965
|
+
|
|
966
|
+
```bash
|
|
967
|
+
pip install -e ".[dev]"
|
|
968
|
+
pre-commit install # lint, format and a secret scan on every commit
|
|
969
|
+
pytest
|
|
970
|
+
ruff check splitagent tests
|
|
971
|
+
ruff format splitagent tests
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
CI runs lint, the full test suite on Linux/Windows/macOS across Python
|
|
975
|
+
3.10-3.12, and builds the distribution to confirm the web assets ship in the
|
|
976
|
+
wheel. A pre-commit hook refuses to commit anything that looks like a real
|
|
977
|
+
provider key.
|
|
978
|
+
|
|
979
|
+
Tests cover CVSS scoring, configuration, encrypted context, tools (with a local
|
|
980
|
+
HTTP server), the full engine loop and the desktop pipeline (against a mock LLM
|
|
981
|
+
API) and the TUI.
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## License
|
|
986
|
+
|
|
987
|
+
MIT © SplitAgent Contributors
|