8.5 KiB
recon-triage
Defensive bug-bounty reconnaissance & triage — authorized security testing only.
recon-triage runs a fixed, fail-soft pipeline of open-source recon tools against
in-scope targets, normalizes every tool's output into one unified JSON schema,
grounds findings against Exploit-DB via searchsploit, and produces a prioritized
triage report (report.json + report.md). An optional, fully-optional LLM stage
enriches the ranking — but the build and every run work with no model available.
⚠️ Authorized use only — recon & triage scope
This tool performs reconnaissance and triage only. It never runs, generates, downloads, or executes exploits. It enumerates assets, standardizes output, and cites candidate references (e.g. unverified Exploit-DB IDs). A human decides what to do next. You are responsible for ensuring every target is explicitly in scope and that you are authorized to test it.
Pipeline
scope.yaml ─▶ subfinder ─▶ dnsx ─▶ naabu ─▶ nmap -sV ─▶ httpx ─▶ [nuclei] ─▶ searchsploit ─▶ report.{json,md}
(passive) (resolve) (ports) (versions) (http) (opt-in) (Exploit-DB) (+ optional LLM triage)
Every stage fails soft: a tool that crashes, times out, or returns nothing is
recorded as failed/empty in the report and the run continues.
| Stage | Tool | Structured output |
|---|---|---|
| 1 | subfinder |
-silent -oJ (JSONL) |
| 2 | dnsx |
-json (A/AAAA/CNAME) |
| 3 | naabu |
-json -scan-type connect (unprivileged) |
| 4 | nmap |
-sV -sT -oX - (product + version) |
| 5 | httpx |
-json -td (status/title/tech/TLS) |
| 6 | nuclei |
-jsonl — off by default, enable with --enable-nuclei |
| 7 | searchsploit |
--json — Exploit-DB grounding |
nmap's product + version per service is the key signal feeding Exploit-DB
matching, so it is captured precisely.
Exploit-DB grounding (the anti-hallucination layer)
For each detected service with a product, the tool builds a query ("Apache httpd 2.4.49", falling back to just the product) and runs searchsploit --json. Each hit
is attached as an ExploitDBMatch with verified=false. Nothing is fabricated or
inferred — only what searchsploit actually returned is emitted. The Exploit-DB
ships inside the image, so this needs no network at runtime.
Quick start (Docker only — no host tool installs)
# 1. Build the runtime image
make build
# 2. Run the full offline demo on committed fixtures -> ./out (no network, no scan)
make replay
cat out/report.md
# 3. Run the offline test suite (schema, normalizers, scope, Exploit-DB matcher)
make test
# 4. Live scan an in-scope target (copy and edit the example scope first)
cp scope.example.yaml scope.yaml # edit to YOUR authorized scope
make scan TARGET=example.com SCOPE=scope.yaml
A reviewer with only Docker installed can run all of the above. Nothing is installed on the host.
CLI
recon-triage scan --scope scope.yaml --target example.com --out /data/out \
[--enable-nuclei] [--passive-only] [--rate-limit N] [--timeout S]
recon-triage replay --fixtures tests/fixtures --out /data/out [--scope scope.yaml]
recon-triage schema --out schemas
replay runs the full normalize → ground → report path on canned fixtures with
zero network — it's the offline demo and the basis of the test suite.
Scope enforcement (mandatory)
scope.yaml declares in-scope domains + CIDRs and optional exclusions. Anything not
in scope is dropped with a logged warning, never scanned. Out-of-scope rules take
precedence; domain rules match subdomains.
in_scope_domains:
- example.com # also matches api.example.com
in_scope_cidrs:
- 93.184.216.0/24
out_of_scope:
- internal-only.example.com
- 93.184.216.200/30
See scope.example.yaml. Your real scope.yaml is git-ignored.
Running unprivileged & capabilities
The container runs as a non-root app user and defaults to TCP connect scans
(naabu -scan-type connect, nmap -sT), so it works with no added capabilities:
docker run --rm -v "$PWD/scope.yaml:/data/scope.yaml:ro" -v "$PWD/out:/data/out" \
recon-triage:latest scan --scope /data/scope.yaml --target example.com --out /data/out
Optional: for faster SYN scans you may grant raw-socket capabilities. This is never required:
docker run --rm --cap-add=NET_RAW --cap-add=NET_ADMIN ... recon-triage:latest scan ...
Optional LLM triage
Configured purely by environment. If LLM_BASE_URL is unset, the LLM stage is
skipped and a deterministic severity-based ranking is used instead — the report is
always grounded and ranked.
| Env var | Example | Meaning |
|---|---|---|
LLM_BASE_URL |
http://ollama:11434/v1 |
OpenAI-compatible endpoint (enables the stage) |
LLM_MODEL |
qwen2.5:7b-instruct |
model name |
LLM_API_KEY |
not-needed |
dummy ok for local |
The model is instructed to use only the provided ReconReport, cite findings by id,
and is forbidden from inventing CVEs, EDB-IDs, paths, or tools. Output is validated
against the TriageReport schema (one retry on invalid JSON, then deterministic
fallback). Acceptance gate: every suggested_next_step / evidence reference must
point at an identifier present in the input report; anything else is stripped.
Enable a local model via Compose:
docker compose --profile llm up -d ollama
docker compose --profile llm run --rm app-llm \
scan --scope /data/scope.yaml --target example.com --out /data/out
Without the profile, docker compose run --rm app scan ... runs the pipeline LLM-free.
Output
Written to the mounted /data/out volume:
report.json— schema-validReconReport(+TriageReportif triage ran).report.md— operator-facing summary: hosts → services (with versions) → Exploit-DB candidates → nuclei findings → triage priorities.schemas/— exported JSON Schema of the contract.
The unified schema (Pydantic v2) lives in src/recon_triage/schema.py and is the
single source of truth; schemas/*.schema.json is exported on build.
Tool versions (pinned)
| Tool | Version | Source |
|---|---|---|
| subfinder | v2.6.6 |
go install (build arg SUBFINDER_VERSION) |
| dnsx | v1.2.1 |
go install (DNSX_VERSION) |
| naabu | v2.3.1 |
go install (NAABU_VERSION) |
| httpx | v1.6.9 |
go install (HTTPX_VERSION) |
| nuclei | v3.3.5 |
go install (NUCLEI_VERSION) |
| nmap | distro | apk add nmap |
| searchsploit + Exploit-DB | main |
shallow git clone of gitlab.com/exploit-database/exploitdb at build time (pin via build arg EXPLOITDB_REF) |
The Exploit-DB checkout is fetched at build time and shipped inside the image, so
searchsploitneeds no network at runtime. (The Alpineexploitdbpackage is not in every release's stable repo, so the upstream git checkout is used for reproducibility.)
Override any Go tool version at build time, e.g. docker build --build-arg NAABU_VERSION=v2.3.2 .... Base image is Alpine (musl) throughout; all Python
dependencies, including Pydantic v2, ship musllinux wheels so no Debian fallback is
needed.
nuclei templates are not baked into the image — they cache to a mounted volume
(nuclei-templates) at runtime when --enable-nuclei is used.
Development & testing
The test suite is fully offline — schema, every normalizer (fed from committed
fixtures in tests/fixtures/), the Exploit-DB matcher (via an injected searchsploit
stub), scope gating, and the end-to-end replay path. make test runs it inside the
test build stage with zero external calls.
make test # docker: build test stage + run pytest (offline)
make lint # ruff
Project layout
src/recon_triage/
schema.py Pydantic v2 models (single source of truth) + JSON-schema export
scope.py scope load + in/out-of-scope gating
orchestrator.py stage sequencing, host merging, failure isolation, replay path
tools/ one wrapper per tool (native structured output -> schema)
grounding/exploitdb.py searchsploit grounding (never fabricates)
triage/llm.py optional OpenAI-compatible triage + validation + fallback
triage/ranking.py deterministic severity ranking (always available)
report/markdown.py operator-facing report
tests/ offline suite + committed fixtures