An agent for authorized, non-destructive web security testing. It acts like a junior pentester: it orchestrates standard tools (Nuclei, testssl.sh, OWASP ZAP), interprets their output — optionally with Claude — and produces an actionable report in JSON, Markdown and PDF.
Non-negotiable principle: no network request leaves the system without first passing the scope gate. Authorization is earned, not declared — a real domain is only scannable once its ownership has been proven and a signed, in-force consent exists.
🇪🇸 A detailed Spanish version of this document is kept in the git history and available on request.
Most scanners will happily fire at any target you point them at. Centinela is built around the opposite premise: the hard part of ethical security testing is authorization, not scanning. The whole system is designed so that it is structurally impossible to touch a real domain you have not proven you own and consented to test.
- Hard scope gate. The graph always starts at
validate_scope. Without valid authorization it halts before executing a single network node. HttpGuard. A single HTTP client that every outbound request must go through; it enforces the scope allowlist on each call and logs every contact. Anything off-allowlist →OutOfScopeError.- Domain-ownership verification (ACME-style). DNS TXT record or an HTTP file under
/.well-known/, the latter fetched through the guard via a single-use challenge grant. - Tamper-evident consent. A SHA-256 hash over a canonical Rules-of-Engagement document; if the document is altered after consent, the gate invalidates it.
- Non-destructive scanning. Nuclei with
-exclude-tags dos,intrusive,fuzz, rate limiting, timeouts and no out-of-band callbacks by default. - Pluggable analysis. With an API key, Claude consolidates CVSS, OWASP Top 10 and remediation. Without a key, a deterministic raw mode maps only what the scanners actually return and marks everything else as "not available" — it never invents findings.
- Reporting in JSON, Markdown and PDF (cover with authorization reference, severity summary, detail and numbering).
- Append-only audit trail of every decision, tool and host contacted.
The core is a LangGraph state graph:
flowchart LR
START --> validate_scope
validate_scope -->|unauthorized| END
validate_scope -->|ok| recon
recon --> passive_checks
passive_checks -->|active_scan| active_scan
passive_checks -->|intrusive_eligible| dast_scan
passive_checks -->|passive| analyze
active_scan -->|intrusive_eligible| dast_scan
active_scan -->|not intrusive| analyze
dast_scan --> analyze
analyze --> report --> END
| Node | Responsibility |
|---|---|
validate_scope |
Hard gate. Local targets via a self-authorized path; real domains require in-force verification + valid consent + time window. |
recon |
Passive fingerprinting (server, technology, headers) via HttpGuard. |
passive_checks |
Security headers (HSTS, CSP, X-Frame-Options…) and TLS via testssl.sh. |
active_scan |
Nuclei (only if the scope authorizes it). |
dast_scan |
OWASP ZAP in a confined Docker container (dedicated network, memory cap, scope context). Only if intrusive_eligible. |
analyze |
LlmAnalyzer (Claude) or RawAnalyzer (no AI). Consolidates findings from all three layers and deduplicates by (CWE, URL). |
report |
Assembles and persists the report; declares methodology, consent level and limitations. |
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, LangGraph, Anthropic SDK, httpx, dnspython |
| Analysis | Claude (Anthropic) · deterministic raw mode (no AI) |
| Tooling | Nuclei, testssl.sh, OWASP ZAP (official Docker image) |
| PDF report | Jinja2 + WeasyPrint |
| Persistence | PostgreSQL + SQLAlchemy (in-memory by default in dev) |
| Frontend | Next.js 15, TypeScript, Tailwind CSS |
| Dev infra | docker-compose (backend + postgres + Juice Shop) |
backend/app/
├── main.py # FastAPI app
├── config.py # settings (pydantic-settings)
├── graph/ # LangGraph state, orchestrator and nodes (scope, recon, passive, scan, dast, analyze, report)
├── models/ # scope, finding, report, verification, consent, authorization, audit, run
├── repositories/ # data-access layer (append-only audit)
├── tools/ # http_guard, nuclei, testssl, zap, verification, consent, analyzers, pdf, llm
└── templates/ # HTML template for the PDF report
backend/scopes/ # example scopes (local Juice Shop)
backend/scripts/run_scan.py # scan CLI
frontend/ # Next.js 15 (guided flow + report)
docker-compose.yml # backend + postgres + juice-shop
Requirements: Python 3.12+, Node.js 18+ (frontend), Docker (for Juice Shop, Nuclei, testssl.sh, ZAP
and PDF generation). Optional: CENTINELA_ANTHROPIC_API_KEY for Claude analysis — without it, raw
mode is used automatically.
# With docker-compose
docker compose up -d juice-shop postgres
docker compose run --rm backend python scripts/run_scan.py --scope scopes/juice-shop-docker.yaml
# Backend locally
cd backend
pip install -e ".[dev]"
python scripts/run_scan.py --scope scopes/juice-shop.yaml --no-ai # passive + Nuclei, no AI
uvicorn app.main:app --port 8000 # API
# Frontend
cd frontend && npm install && npm run dev # http://localhost:3000 (API on :8000)Key environment variables: CENTINELA_ANTHROPIC_API_KEY (Claude key; raw mode without it),
CENTINELA_NUCLEI_TEMPLATES_DIR, and the CENTINELA_ZAP_* knobs (MEMORY, ATTACK_STRENGTH,
SPIDER_TIMEOUT_MIN, ASCAN_TIMEOUT_MIN) that bound the DAST stage. Secrets are read from the
environment only — never committed.
Real-domain flow (UI): register domain → choose method → publish the challenge → verify → capture authorization + consent → scope becomes authorized → launch scan → download PDF. The UI only guides; the source of truth is the backend gate.
| Method | Path | Description |
|---|---|---|
POST |
/scans |
Launch a background scan |
GET |
/scans/{id} |
Status (running/done/error) and report |
GET |
/scans |
List scans |
GET |
/scopes |
Available scopes (local + real domains) |
POST |
/targets |
Register a domain and return the challenge |
POST |
/targets/{id}/verify |
Verify ownership (DNS/HTTP) |
POST |
/authorizations |
Create the ROE document for a verified domain |
POST |
/authorizations/{id}/consent |
Record consent (hash) |
GET |
/scans/{id}/report.pdf |
Download the PDF report |
cd backend
pytest -q # 65 tests (+1 skip: WeasyPrint smoke on environments without native libs)
ruff check .Tests cover the scope gate, the HttpGuard, verification (mocked DNS/HTTP), consent and its hash
check (including tampering), raw mode, the API, and an end-to-end passive run against a simulated
Juice Shop (Claude mocked, no API calls).
The first end-to-end intrusive-level run against OWASP Juice Shop (raw mode, on an 8 GB dev VPS)
produced 467 deduplicated findings across the passive, Nuclei and ZAP layers — including an
exposed Prometheus /metrics endpoint, a public Swagger spec, CORS misconfiguration (136 resources)
and missing CSP (112 URLs).
Notably, SQLi/XSS did not appear — and the report says so and explains why: under the RAM budget the DAST stage ran ZAP with HtmlUnit and only 2 ascan threads, which cannot drive Juice Shop's Angular SPA the way headless Chrome would. Surfacing those known injections requires ≥ 5 GB free for ZAP + Chrome. Documenting this limitation transparently — rather than hiding it — is a deliberate design goal.
- Modeling authorization as a state machine: ownership verification, tamper-evident consent, and time windows enforced before any network activity — security by construction, not by convention.
- Orchestrating a multi-stage pipeline as a LangGraph state graph with conditional routing.
- Wrapping untrusted, resource-hungry tooling (OWASP ZAP) in a confined Docker sandbox with memory caps, timeouts and a dedicated network.
- Designing an AI-optional system: Claude enriches analysis when available, but a deterministic fallback keeps the product honest and functional without it.
- Writing a report layer that declares its own limitations — a habit that matters as much in security as it does in engineering.
Centinela is designed for authorized security testing. Use it only against systems you own or have explicit, in-force permission to test. Misuse is the sole responsibility of the operator.