redrun-scan 0.1.0__tar.gz
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.
- redrun_scan-0.1.0/PKG-INFO +178 -0
- redrun_scan-0.1.0/README.md +153 -0
- redrun_scan-0.1.0/pyproject.toml +41 -0
- redrun_scan-0.1.0/redrun/__init__.py +0 -0
- redrun_scan-0.1.0/redrun/__main__.py +5 -0
- redrun_scan-0.1.0/redrun/app/__init__.py +1 -0
- redrun_scan-0.1.0/redrun/app/auth.py +50 -0
- redrun_scan-0.1.0/redrun/app/db.py +57 -0
- redrun_scan-0.1.0/redrun/app/events.py +31 -0
- redrun_scan-0.1.0/redrun/app/models.py +60 -0
- redrun_scan-0.1.0/redrun/app/phases.py +257 -0
- redrun_scan-0.1.0/redrun/app/runner.py +101 -0
- redrun_scan-0.1.0/redrun/app/server.py +281 -0
- redrun_scan-0.1.0/redrun/app/store.py +159 -0
- redrun_scan-0.1.0/redrun/cli.py +409 -0
- redrun_scan-0.1.0/redrun/cloud.py +112 -0
- redrun_scan-0.1.0/redrun/engine/__init__.py +0 -0
- redrun_scan-0.1.0/redrun/engine/active_scanner.py +887 -0
- redrun_scan-0.1.0/redrun/engine/config.py +42 -0
- redrun_scan-0.1.0/redrun/engine/docker/egress-proxy/Dockerfile +23 -0
- redrun_scan-0.1.0/redrun/engine/docker/egress-proxy/entrypoint.sh +55 -0
- redrun_scan-0.1.0/redrun/engine/docker_sandbox.py +190 -0
- redrun_scan-0.1.0/redrun/engine/egress.py +98 -0
- redrun_scan-0.1.0/redrun/engine/exploitation.py +231 -0
- redrun_scan-0.1.0/redrun/engine/nuclei_scanner.py +170 -0
- redrun_scan-0.1.0/redrun/engine/planner.py +211 -0
- redrun_scan-0.1.0/redrun/engine/recon.py +612 -0
- redrun_scan-0.1.0/redrun/engine/reporter.py +315 -0
- redrun_scan-0.1.0/redrun/engine/sandbox.py +187 -0
- redrun_scan-0.1.0/redrun/engine/schemas.py +258 -0
- redrun_scan-0.1.0/redrun/engine/scope.py +237 -0
- redrun_scan-0.1.0/redrun/engine/tools/__init__.py +0 -0
- redrun_scan-0.1.0/redrun/engine/tools/base.py +244 -0
- redrun_scan-0.1.0/redrun/engine/tools/broken_auth.py +183 -0
- redrun_scan-0.1.0/redrun/engine/tools/sqli.py +162 -0
- redrun_scan-0.1.0/redrun/engine/tools/ssrf.py +176 -0
- redrun_scan-0.1.0/redrun/engine/tools/xss.py +104 -0
- redrun_scan-0.1.0/redrun/engine/zone.py +159 -0
- redrun_scan-0.1.0/redrun/licensing.py +116 -0
- redrun_scan-0.1.0/redrun/output.py +93 -0
- redrun_scan-0.1.0/redrun_scan.egg-info/PKG-INFO +178 -0
- redrun_scan-0.1.0/redrun_scan.egg-info/SOURCES.txt +60 -0
- redrun_scan-0.1.0/redrun_scan.egg-info/dependency_links.txt +1 -0
- redrun_scan-0.1.0/redrun_scan.egg-info/entry_points.txt +2 -0
- redrun_scan-0.1.0/redrun_scan.egg-info/requires.txt +19 -0
- redrun_scan-0.1.0/redrun_scan.egg-info/top_level.txt +1 -0
- redrun_scan-0.1.0/setup.cfg +4 -0
- redrun_scan-0.1.0/tests/test_active_scanner.py +63 -0
- redrun_scan-0.1.0/tests/test_auth.py +76 -0
- redrun_scan-0.1.0/tests/test_db.py +29 -0
- redrun_scan-0.1.0/tests/test_desktop_handoff.py +59 -0
- redrun_scan-0.1.0/tests/test_events.py +42 -0
- redrun_scan-0.1.0/tests/test_integration_e2e.py +53 -0
- redrun_scan-0.1.0/tests/test_models.py +29 -0
- redrun_scan-0.1.0/tests/test_phases.py +51 -0
- redrun_scan-0.1.0/tests/test_post_comment.py +88 -0
- redrun_scan-0.1.0/tests/test_runner.py +48 -0
- redrun_scan-0.1.0/tests/test_scan_fail_on.py +39 -0
- redrun_scan-0.1.0/tests/test_serve_cmd.py +65 -0
- redrun_scan-0.1.0/tests/test_server.py +312 -0
- redrun_scan-0.1.0/tests/test_sslyze_adapter.py +51 -0
- redrun_scan-0.1.0/tests/test_store.py +95 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: redrun-scan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RedRun — continuous, proof-backed security testing you run yourself.
|
|
5
|
+
Author: RedRun
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pydantic>=2.10.0
|
|
10
|
+
Requires-Dist: httpx>=0.28.0
|
|
11
|
+
Requires-Dist: dnspython>=2.7.0
|
|
12
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
13
|
+
Requires-Dist: lxml>=5.3.0
|
|
14
|
+
Requires-Dist: cryptography>=42.0.0
|
|
15
|
+
Requires-Dist: fastapi>=0.115.0
|
|
16
|
+
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
17
|
+
Provides-Extra: ai
|
|
18
|
+
Requires-Dist: anthropic>=0.40.0; extra == "ai"
|
|
19
|
+
Provides-Extra: tools
|
|
20
|
+
Requires-Dist: sslyze>=6.0.0; extra == "tools"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
24
|
+
Requires-Dist: httpx>=0.28.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# RedRun CLI
|
|
27
|
+
|
|
28
|
+
**Proof-backed security testing you run yourself.** A standalone command-line tool
|
|
29
|
+
that runs RedRun's scanning engine **locally on your machine** — passive recon and
|
|
30
|
+
real, evidence-verified exploitation (SQLi, XSS, SSRF, IDOR, broken auth) — with
|
|
31
|
+
no cloud dependency.
|
|
32
|
+
|
|
33
|
+
This is the licensed-software M1 (see `../LICENSED-SOFTWARE-PLAN.md`): the engine
|
|
34
|
+
runs inside your own environment, so authorization is implicit (you point it at
|
|
35
|
+
your own assets) and deeper/internal testing is possible.
|
|
36
|
+
|
|
37
|
+
## Install (customers)
|
|
38
|
+
```bash
|
|
39
|
+
# Recommended: isolated install on PATH
|
|
40
|
+
pipx install ./dist/redrun-0.1.0-py3-none-any.whl
|
|
41
|
+
# or
|
|
42
|
+
pip install ./dist/redrun-0.1.0-py3-none-any.whl
|
|
43
|
+
```
|
|
44
|
+
For AI executive summaries: `pipx install "redrun[ai]"` and set `ANTHROPIC_API_KEY`.
|
|
45
|
+
|
|
46
|
+
## Licensing
|
|
47
|
+
Passive scans are **free**. Active exploitation requires a license.
|
|
48
|
+
```bash
|
|
49
|
+
redrun license status # show current license
|
|
50
|
+
redrun license activate your.lic # install a license file (offline-verified)
|
|
51
|
+
```
|
|
52
|
+
Licenses are Ed25519-signed and verified **offline** with an embedded public key —
|
|
53
|
+
no server call, air-gap friendly. Tampering invalidates the signature.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
```bash
|
|
57
|
+
# Passive scan — recon, headers, TLS, DNS, exposed paths, nuclei CVE templates
|
|
58
|
+
redrun scan example.com
|
|
59
|
+
|
|
60
|
+
# Active exploitation — requires explicit authorization
|
|
61
|
+
redrun scan staging.myapp.com --active --authorized
|
|
62
|
+
|
|
63
|
+
# Production-looking host needs an extra confirmation
|
|
64
|
+
redrun scan myapp.com --active --authorized --confirm-production
|
|
65
|
+
|
|
66
|
+
# Extra in-scope hosts, JSON export, kernel sandbox
|
|
67
|
+
redrun scan myapp.com --active --authorized --scope api.myapp.com --json out.json
|
|
68
|
+
redrun scan myapp.com --active --authorized --sandbox docker
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Options
|
|
72
|
+
| Flag | Meaning |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `--active` | run real exploitation (not just passive observation) |
|
|
75
|
+
| `--authorized` | confirm you own / may test the target (**required for `--active`**) |
|
|
76
|
+
| `--confirm-production` | authorize an active scan against a production-looking host |
|
|
77
|
+
| `--scope a,b` | additional in-scope hosts |
|
|
78
|
+
| `--sandbox local\|docker\|auto` | containment for active scans (default `local` egress guard; `docker` = kernel iptables allowlist, needs a Docker host) |
|
|
79
|
+
| `--json FILE` | write full results to JSON |
|
|
80
|
+
| `--no-ai` | skip the AI executive summary |
|
|
81
|
+
|
|
82
|
+
## Local console (web UI)
|
|
83
|
+
|
|
84
|
+
RedRun ships a local web console — a targets dashboard, per-target scan history
|
|
85
|
+
with live progress, an add-target flow, and license/settings — served by
|
|
86
|
+
`redrun serve` from a single loopback process. The UI is a Vite + React SPA in
|
|
87
|
+
`ui/`, built to `redrun/app/static/` and served alongside the token-guarded
|
|
88
|
+
`/v1` API on `127.0.0.1`.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# one-time: install UI deps
|
|
92
|
+
npm --prefix ui install
|
|
93
|
+
# build the UI (outputs to redrun/app/static/, served by `redrun serve`)
|
|
94
|
+
npm --prefix ui run build
|
|
95
|
+
# run the console — prints a URL containing the per-launch token
|
|
96
|
+
redrun serve
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`redrun serve` binds `127.0.0.1:7800` by default and generates a fresh token each
|
|
100
|
+
launch; open the printed `http://127.0.0.1:7800/?token=…` URL (the desktop shell
|
|
101
|
+
will inject this for you in a later release). Every API request is checked for
|
|
102
|
+
that token plus a same-origin guard, so a malicious web page cannot drive the
|
|
103
|
+
local engine. If the UI hasn't been built yet, the API still runs and `serve`
|
|
104
|
+
says so.
|
|
105
|
+
|
|
106
|
+
For UI development with hot reload, run the API and the Vite dev server side by
|
|
107
|
+
side (the dev server proxies `/v1` HTTP + WebSocket to the API):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
redrun serve --port 7800 # terminal 1 (API + token)
|
|
111
|
+
npm --prefix ui run dev # terminal 2 (proxies /v1 → 7800)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
UI tests: `npm --prefix ui test` (Vitest + React Testing Library).
|
|
115
|
+
|
|
116
|
+
## Desktop app (macOS)
|
|
117
|
+
|
|
118
|
+
RedRun ships as a native macOS app (Tauri) that bundles the Python control plane
|
|
119
|
+
as a sidecar — no Python install required on the end-user machine. The Rust shell
|
|
120
|
+
generates a per-launch loopback token, starts the sidecar, polls it until ready,
|
|
121
|
+
and opens the console in the OS webview.
|
|
122
|
+
|
|
123
|
+
**Build prerequisites:** Rust (`rustup`), the Tauri CLI (`cargo install tauri-cli
|
|
124
|
+
--version "^2.0.0" --locked`), and PyInstaller in the project venv
|
|
125
|
+
(`.venv/bin/python -m pip install "pyinstaller>=6.0"`).
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# 1. Freeze the Python control plane into a Tauri resource (also builds the UI)
|
|
129
|
+
packaging/build-sidecar.sh
|
|
130
|
+
# 2. Run the desktop app in dev
|
|
131
|
+
cd desktop/src-tauri && cargo tauri dev
|
|
132
|
+
# 3. Produce a distributable .app / .dmg
|
|
133
|
+
cd desktop/src-tauri && cargo tauri build
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The shell is the security boundary: it binds the API to `127.0.0.1`, mints a
|
|
137
|
+
fresh 256-bit token each launch, and passes it to the sidecar via env — the token
|
|
138
|
+
is never on the command line. The shell supervises the sidecar (restarting it
|
|
139
|
+
with a fresh token+port if it crashes) and kills it on quit, so no engine process
|
|
140
|
+
is orphaned. A scan left running by a crash is marked `interrupted` on the next
|
|
141
|
+
start.
|
|
142
|
+
|
|
143
|
+
Rust shell tests: `cd desktop/src-tauri && cargo test`. The end-to-end
|
|
144
|
+
token-handoff is covered headlessly by `tests/test_desktop_handoff.py`
|
|
145
|
+
(`pytest -m network`). Windows/Linux bundles, code-signing, and auto-update are
|
|
146
|
+
not yet wired up.
|
|
147
|
+
|
|
148
|
+
## Safety
|
|
149
|
+
- **Passive** scans are read-only and legal on any domain.
|
|
150
|
+
- **Active** scans send real attack payloads — only run them against systems you
|
|
151
|
+
own or are authorized to test. The `--authorized` flag is your rules-of-engagement.
|
|
152
|
+
- Active scanning is **detection-only**: it proves a vulnerability exists with
|
|
153
|
+
request/response evidence, then stops — it never exfiltrates data or causes damage.
|
|
154
|
+
- Outbound traffic is scope-enforced (egress guard by default; optional Docker
|
|
155
|
+
kernel sandbox).
|
|
156
|
+
|
|
157
|
+
## Optional
|
|
158
|
+
- **AI summaries:** set `ANTHROPIC_API_KEY` and install the `[ai]` extra for an
|
|
159
|
+
executive summary. Without it, the CLI runs fully offline.
|
|
160
|
+
- **Nuclei:** if the `nuclei` binary is on PATH, CVE templates run automatically;
|
|
161
|
+
otherwise that step is skipped.
|
|
162
|
+
|
|
163
|
+
## Build & release (maintainers)
|
|
164
|
+
```bash
|
|
165
|
+
python -m build --wheel # → dist/redrun-<v>-py3-none-any.whl
|
|
166
|
+
```
|
|
167
|
+
Issue a license (internal — needs the private signing key in `scripts/.keys/`,
|
|
168
|
+
which is gitignored and must never ship):
|
|
169
|
+
```bash
|
|
170
|
+
python scripts/issue_license.py --email user@co.com --tier pro --days 365
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Architecture
|
|
174
|
+
`redrun/engine/` is a vendored copy of the scanning engine (recon, scope, egress
|
|
175
|
+
guard, exploit tools, reporter). The CLI orchestrates it locally. Vendored for
|
|
176
|
+
M1 to keep the tool standalone and zero-risk to the live web backend; a shared
|
|
177
|
+
`redrun_core` package can de-duplicate later. `redrun/licensing.py` holds the
|
|
178
|
+
embedded license-verification public key.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# RedRun CLI
|
|
2
|
+
|
|
3
|
+
**Proof-backed security testing you run yourself.** A standalone command-line tool
|
|
4
|
+
that runs RedRun's scanning engine **locally on your machine** — passive recon and
|
|
5
|
+
real, evidence-verified exploitation (SQLi, XSS, SSRF, IDOR, broken auth) — with
|
|
6
|
+
no cloud dependency.
|
|
7
|
+
|
|
8
|
+
This is the licensed-software M1 (see `../LICENSED-SOFTWARE-PLAN.md`): the engine
|
|
9
|
+
runs inside your own environment, so authorization is implicit (you point it at
|
|
10
|
+
your own assets) and deeper/internal testing is possible.
|
|
11
|
+
|
|
12
|
+
## Install (customers)
|
|
13
|
+
```bash
|
|
14
|
+
# Recommended: isolated install on PATH
|
|
15
|
+
pipx install ./dist/redrun-0.1.0-py3-none-any.whl
|
|
16
|
+
# or
|
|
17
|
+
pip install ./dist/redrun-0.1.0-py3-none-any.whl
|
|
18
|
+
```
|
|
19
|
+
For AI executive summaries: `pipx install "redrun[ai]"` and set `ANTHROPIC_API_KEY`.
|
|
20
|
+
|
|
21
|
+
## Licensing
|
|
22
|
+
Passive scans are **free**. Active exploitation requires a license.
|
|
23
|
+
```bash
|
|
24
|
+
redrun license status # show current license
|
|
25
|
+
redrun license activate your.lic # install a license file (offline-verified)
|
|
26
|
+
```
|
|
27
|
+
Licenses are Ed25519-signed and verified **offline** with an embedded public key —
|
|
28
|
+
no server call, air-gap friendly. Tampering invalidates the signature.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
```bash
|
|
32
|
+
# Passive scan — recon, headers, TLS, DNS, exposed paths, nuclei CVE templates
|
|
33
|
+
redrun scan example.com
|
|
34
|
+
|
|
35
|
+
# Active exploitation — requires explicit authorization
|
|
36
|
+
redrun scan staging.myapp.com --active --authorized
|
|
37
|
+
|
|
38
|
+
# Production-looking host needs an extra confirmation
|
|
39
|
+
redrun scan myapp.com --active --authorized --confirm-production
|
|
40
|
+
|
|
41
|
+
# Extra in-scope hosts, JSON export, kernel sandbox
|
|
42
|
+
redrun scan myapp.com --active --authorized --scope api.myapp.com --json out.json
|
|
43
|
+
redrun scan myapp.com --active --authorized --sandbox docker
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Options
|
|
47
|
+
| Flag | Meaning |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `--active` | run real exploitation (not just passive observation) |
|
|
50
|
+
| `--authorized` | confirm you own / may test the target (**required for `--active`**) |
|
|
51
|
+
| `--confirm-production` | authorize an active scan against a production-looking host |
|
|
52
|
+
| `--scope a,b` | additional in-scope hosts |
|
|
53
|
+
| `--sandbox local\|docker\|auto` | containment for active scans (default `local` egress guard; `docker` = kernel iptables allowlist, needs a Docker host) |
|
|
54
|
+
| `--json FILE` | write full results to JSON |
|
|
55
|
+
| `--no-ai` | skip the AI executive summary |
|
|
56
|
+
|
|
57
|
+
## Local console (web UI)
|
|
58
|
+
|
|
59
|
+
RedRun ships a local web console — a targets dashboard, per-target scan history
|
|
60
|
+
with live progress, an add-target flow, and license/settings — served by
|
|
61
|
+
`redrun serve` from a single loopback process. The UI is a Vite + React SPA in
|
|
62
|
+
`ui/`, built to `redrun/app/static/` and served alongside the token-guarded
|
|
63
|
+
`/v1` API on `127.0.0.1`.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# one-time: install UI deps
|
|
67
|
+
npm --prefix ui install
|
|
68
|
+
# build the UI (outputs to redrun/app/static/, served by `redrun serve`)
|
|
69
|
+
npm --prefix ui run build
|
|
70
|
+
# run the console — prints a URL containing the per-launch token
|
|
71
|
+
redrun serve
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`redrun serve` binds `127.0.0.1:7800` by default and generates a fresh token each
|
|
75
|
+
launch; open the printed `http://127.0.0.1:7800/?token=…` URL (the desktop shell
|
|
76
|
+
will inject this for you in a later release). Every API request is checked for
|
|
77
|
+
that token plus a same-origin guard, so a malicious web page cannot drive the
|
|
78
|
+
local engine. If the UI hasn't been built yet, the API still runs and `serve`
|
|
79
|
+
says so.
|
|
80
|
+
|
|
81
|
+
For UI development with hot reload, run the API and the Vite dev server side by
|
|
82
|
+
side (the dev server proxies `/v1` HTTP + WebSocket to the API):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
redrun serve --port 7800 # terminal 1 (API + token)
|
|
86
|
+
npm --prefix ui run dev # terminal 2 (proxies /v1 → 7800)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
UI tests: `npm --prefix ui test` (Vitest + React Testing Library).
|
|
90
|
+
|
|
91
|
+
## Desktop app (macOS)
|
|
92
|
+
|
|
93
|
+
RedRun ships as a native macOS app (Tauri) that bundles the Python control plane
|
|
94
|
+
as a sidecar — no Python install required on the end-user machine. The Rust shell
|
|
95
|
+
generates a per-launch loopback token, starts the sidecar, polls it until ready,
|
|
96
|
+
and opens the console in the OS webview.
|
|
97
|
+
|
|
98
|
+
**Build prerequisites:** Rust (`rustup`), the Tauri CLI (`cargo install tauri-cli
|
|
99
|
+
--version "^2.0.0" --locked`), and PyInstaller in the project venv
|
|
100
|
+
(`.venv/bin/python -m pip install "pyinstaller>=6.0"`).
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# 1. Freeze the Python control plane into a Tauri resource (also builds the UI)
|
|
104
|
+
packaging/build-sidecar.sh
|
|
105
|
+
# 2. Run the desktop app in dev
|
|
106
|
+
cd desktop/src-tauri && cargo tauri dev
|
|
107
|
+
# 3. Produce a distributable .app / .dmg
|
|
108
|
+
cd desktop/src-tauri && cargo tauri build
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The shell is the security boundary: it binds the API to `127.0.0.1`, mints a
|
|
112
|
+
fresh 256-bit token each launch, and passes it to the sidecar via env — the token
|
|
113
|
+
is never on the command line. The shell supervises the sidecar (restarting it
|
|
114
|
+
with a fresh token+port if it crashes) and kills it on quit, so no engine process
|
|
115
|
+
is orphaned. A scan left running by a crash is marked `interrupted` on the next
|
|
116
|
+
start.
|
|
117
|
+
|
|
118
|
+
Rust shell tests: `cd desktop/src-tauri && cargo test`. The end-to-end
|
|
119
|
+
token-handoff is covered headlessly by `tests/test_desktop_handoff.py`
|
|
120
|
+
(`pytest -m network`). Windows/Linux bundles, code-signing, and auto-update are
|
|
121
|
+
not yet wired up.
|
|
122
|
+
|
|
123
|
+
## Safety
|
|
124
|
+
- **Passive** scans are read-only and legal on any domain.
|
|
125
|
+
- **Active** scans send real attack payloads — only run them against systems you
|
|
126
|
+
own or are authorized to test. The `--authorized` flag is your rules-of-engagement.
|
|
127
|
+
- Active scanning is **detection-only**: it proves a vulnerability exists with
|
|
128
|
+
request/response evidence, then stops — it never exfiltrates data or causes damage.
|
|
129
|
+
- Outbound traffic is scope-enforced (egress guard by default; optional Docker
|
|
130
|
+
kernel sandbox).
|
|
131
|
+
|
|
132
|
+
## Optional
|
|
133
|
+
- **AI summaries:** set `ANTHROPIC_API_KEY` and install the `[ai]` extra for an
|
|
134
|
+
executive summary. Without it, the CLI runs fully offline.
|
|
135
|
+
- **Nuclei:** if the `nuclei` binary is on PATH, CVE templates run automatically;
|
|
136
|
+
otherwise that step is skipped.
|
|
137
|
+
|
|
138
|
+
## Build & release (maintainers)
|
|
139
|
+
```bash
|
|
140
|
+
python -m build --wheel # → dist/redrun-<v>-py3-none-any.whl
|
|
141
|
+
```
|
|
142
|
+
Issue a license (internal — needs the private signing key in `scripts/.keys/`,
|
|
143
|
+
which is gitignored and must never ship):
|
|
144
|
+
```bash
|
|
145
|
+
python scripts/issue_license.py --email user@co.com --tier pro --days 365
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Architecture
|
|
149
|
+
`redrun/engine/` is a vendored copy of the scanning engine (recon, scope, egress
|
|
150
|
+
guard, exploit tools, reporter). The CLI orchestrates it locally. Vendored for
|
|
151
|
+
M1 to keep the tool standalone and zero-risk to the live web backend; a shared
|
|
152
|
+
`redrun_core` package can de-duplicate later. `redrun/licensing.py` holds the
|
|
153
|
+
embedded license-verification public key.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Distribution name on PyPI ("redrun" is taken). The CLI command stays `redrun`
|
|
7
|
+
# (see [project.scripts]) and the import package stays `redrun/`.
|
|
8
|
+
name = "redrun-scan"
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
description = "RedRun — continuous, proof-backed security testing you run yourself."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license = { text = "Proprietary" }
|
|
14
|
+
authors = [{ name = "RedRun" }]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pydantic>=2.10.0",
|
|
17
|
+
"httpx>=0.28.0",
|
|
18
|
+
"dnspython>=2.7.0",
|
|
19
|
+
"beautifulsoup4>=4.12.0",
|
|
20
|
+
"lxml>=5.3.0",
|
|
21
|
+
"cryptography>=42.0.0",
|
|
22
|
+
"fastapi>=0.115.0",
|
|
23
|
+
"uvicorn[standard]>=0.30.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# AI executive summaries / attack-chain narrative (set ANTHROPIC_API_KEY to use).
|
|
28
|
+
ai = ["anthropic>=0.40.0"]
|
|
29
|
+
tools = ["sslyze>=6.0.0"]
|
|
30
|
+
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0", "httpx>=0.28.0"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
redrun = "redrun.cli:main"
|
|
34
|
+
|
|
35
|
+
# Explicitly list packages so setuptools does NOT treat the hyphenated
|
|
36
|
+
# docker/egress-proxy data dir as a (invalid) namespace package.
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
packages = ["redrun", "redrun.app", "redrun.engine", "redrun.engine.tools"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
"redrun.engine" = ["docker/egress-proxy/*"]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""RedRun local control plane — the FastAPI app behind `redrun serve`."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Per-launch local API token + same-origin guard.
|
|
2
|
+
|
|
3
|
+
The control plane runs an exploitation engine, so the only caller allowed is the
|
|
4
|
+
app's own webview. We require a per-launch token header AND reject any cross-site
|
|
5
|
+
Origin (defeats a malicious page reaching 127.0.0.1 via DNS-rebinding/CSRF).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hmac
|
|
10
|
+
import secrets
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
from fastapi import Header, HTTPException
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_allowed_origin(origin: Optional[str]) -> bool:
|
|
18
|
+
"""True if the Origin is same-origin/loopback or the Tauri shell, or absent
|
|
19
|
+
(non-browser client). A foreign site is rejected (anti DNS-rebind/CSRF).
|
|
20
|
+
|
|
21
|
+
Uses exact hostname matching (not prefix) so spoofed domains like
|
|
22
|
+
http://127.0.0.1.evil.com cannot bypass the guard.
|
|
23
|
+
"""
|
|
24
|
+
if origin is None:
|
|
25
|
+
return True
|
|
26
|
+
parsed = urlparse(origin)
|
|
27
|
+
host = parsed.hostname # None for schemes like tauri:// without //host
|
|
28
|
+
return (
|
|
29
|
+
host in ("127.0.0.1", "localhost", "::1")
|
|
30
|
+
or (parsed.scheme == "tauri" and host in ("localhost", None))
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LocalAuth:
|
|
35
|
+
def __init__(self, token: str):
|
|
36
|
+
self.token = token
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def new_token() -> str:
|
|
40
|
+
return secrets.token_urlsafe(32)
|
|
41
|
+
|
|
42
|
+
async def require(
|
|
43
|
+
self,
|
|
44
|
+
x_redrun_token: Optional[str] = Header(default=None),
|
|
45
|
+
origin: Optional[str] = Header(default=None),
|
|
46
|
+
) -> None:
|
|
47
|
+
if not is_allowed_origin(origin):
|
|
48
|
+
raise HTTPException(status_code=403, detail="Cross-origin blocked")
|
|
49
|
+
if not x_redrun_token or not hmac.compare_digest(x_redrun_token, self.token):
|
|
50
|
+
raise HTTPException(status_code=401, detail="Invalid local token")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""SQLite connection + schema for the local control plane."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sqlite3
|
|
5
|
+
|
|
6
|
+
_SCHEMA = """
|
|
7
|
+
CREATE TABLE IF NOT EXISTS targets (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
label TEXT NOT NULL,
|
|
10
|
+
host_or_url TEXT NOT NULL,
|
|
11
|
+
scope TEXT NOT NULL DEFAULT '[]', -- JSON list of hosts/CIDRs
|
|
12
|
+
authorized INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
enabled_phases TEXT NOT NULL DEFAULT '[]', -- JSON list of phase names
|
|
14
|
+
tags TEXT NOT NULL DEFAULT '[]', -- JSON list
|
|
15
|
+
created_at TEXT NOT NULL
|
|
16
|
+
);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
target_id TEXT NOT NULL REFERENCES targets(id) ON DELETE CASCADE,
|
|
20
|
+
mode TEXT NOT NULL,
|
|
21
|
+
status TEXT NOT NULL,
|
|
22
|
+
phase TEXT,
|
|
23
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
started_at TEXT,
|
|
25
|
+
completed_at TEXT,
|
|
26
|
+
duration REAL,
|
|
27
|
+
report TEXT NOT NULL DEFAULT '{}',
|
|
28
|
+
source TEXT NOT NULL DEFAULT 'manual'
|
|
29
|
+
);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_scans_target ON scans(target_id);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
32
|
+
key TEXT PRIMARY KEY,
|
|
33
|
+
value TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def connect(path: str) -> sqlite3.Connection:
|
|
39
|
+
conn = sqlite3.connect(path, check_same_thread=False)
|
|
40
|
+
conn.row_factory = sqlite3.Row
|
|
41
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
42
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
|
43
|
+
return conn
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _migrate(conn: sqlite3.Connection) -> None:
|
|
47
|
+
"""Idempotent column adds for DBs created before a column existed."""
|
|
48
|
+
cols = {r["name"] for r in conn.execute("PRAGMA table_info(scans)")}
|
|
49
|
+
if "source" not in cols:
|
|
50
|
+
conn.execute(
|
|
51
|
+
"ALTER TABLE scans ADD COLUMN source TEXT NOT NULL DEFAULT 'manual'")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def init_schema(conn: sqlite3.Connection) -> None:
|
|
55
|
+
conn.executescript(_SCHEMA)
|
|
56
|
+
_migrate(conn)
|
|
57
|
+
conn.commit()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""In-process async pub/sub. One queue per WebSocket subscriber, keyed by scan id.
|
|
2
|
+
A small per-scan backlog lets a client that connects mid-scan replay the events it
|
|
3
|
+
missed; the authoritative final state is always in GET /v1/scans/{id}."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from collections import defaultdict, deque
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventHub:
|
|
11
|
+
def __init__(self, backlog: int = 64):
|
|
12
|
+
self._subs: dict[str, list[asyncio.Queue]] = defaultdict(list)
|
|
13
|
+
self._backlog: dict[str, deque] = defaultdict(lambda: deque(maxlen=backlog))
|
|
14
|
+
|
|
15
|
+
def subscribe(self, scan_id: str) -> asyncio.Queue:
|
|
16
|
+
q: asyncio.Queue = asyncio.Queue()
|
|
17
|
+
for event in self._backlog.get(scan_id, ()): # replay what was missed
|
|
18
|
+
q.put_nowait(event)
|
|
19
|
+
self._subs[scan_id].append(q)
|
|
20
|
+
return q
|
|
21
|
+
|
|
22
|
+
def unsubscribe(self, scan_id: str, q: asyncio.Queue) -> None:
|
|
23
|
+
if q in self._subs.get(scan_id, []):
|
|
24
|
+
self._subs[scan_id].remove(q)
|
|
25
|
+
if not self._subs.get(scan_id):
|
|
26
|
+
self._subs.pop(scan_id, None)
|
|
27
|
+
|
|
28
|
+
async def publish(self, scan_id: str, event: dict) -> None:
|
|
29
|
+
self._backlog[scan_id].append(event)
|
|
30
|
+
for q in list(self._subs.get(scan_id, [])):
|
|
31
|
+
await q.put(event)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Local control-plane domain types. Finding is reused from the engine schema."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Phase(str, Enum):
|
|
12
|
+
RECON = "recon"
|
|
13
|
+
ENUMERATION = "enumeration"
|
|
14
|
+
VULN_ASSESSMENT = "vuln_assessment"
|
|
15
|
+
EXPLOITATION = "exploitation"
|
|
16
|
+
EXPOSURE = "exposure"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Phases an ACTIVE scan may run; passive excludes EXPLOITATION.
|
|
20
|
+
PASSIVE_PHASES = [Phase.RECON, Phase.ENUMERATION, Phase.VULN_ASSESSMENT, Phase.EXPOSURE]
|
|
21
|
+
ACTIVE_PHASES = [Phase.RECON, Phase.ENUMERATION, Phase.VULN_ASSESSMENT,
|
|
22
|
+
Phase.EXPLOITATION, Phase.EXPOSURE]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ScanStatus(str, Enum):
|
|
26
|
+
PENDING = "pending"
|
|
27
|
+
RUNNING = "running"
|
|
28
|
+
COMPLETED = "completed"
|
|
29
|
+
FAILED = "failed"
|
|
30
|
+
INTERRUPTED = "interrupted"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Target(BaseModel):
|
|
34
|
+
id: str
|
|
35
|
+
label: str
|
|
36
|
+
host_or_url: str
|
|
37
|
+
scope: list[str] = Field(default_factory=list)
|
|
38
|
+
authorized: bool = False
|
|
39
|
+
enabled_phases: list[str] = Field(default_factory=list)
|
|
40
|
+
tags: list[str] = Field(default_factory=list)
|
|
41
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
42
|
+
# rolled-up display state (computed from scans, not stored on the row)
|
|
43
|
+
status: str = "idle"
|
|
44
|
+
last_scan_at: Optional[datetime] = None
|
|
45
|
+
latest_risk_score: Optional[int] = None
|
|
46
|
+
open_findings: dict = Field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ScanRecord(BaseModel):
|
|
50
|
+
id: str
|
|
51
|
+
target_id: str
|
|
52
|
+
mode: str = "passive"
|
|
53
|
+
status: ScanStatus = ScanStatus.PENDING
|
|
54
|
+
phase: Optional[str] = None
|
|
55
|
+
progress: int = 0
|
|
56
|
+
started_at: Optional[datetime] = None
|
|
57
|
+
completed_at: Optional[datetime] = None
|
|
58
|
+
duration: Optional[float] = None
|
|
59
|
+
report: dict = Field(default_factory=dict)
|
|
60
|
+
source: str = "manual"
|