Continuous Attack Surface Management for CISOs.
Surface discovers, monitors and triages the assets and vulnerabilities exposed on the Internet (or on a LAN) for a given perimeter. It runs as a single Docker Compose stack — FastAPI + PostgreSQL + nuclei + nmap — on your own machine or a self-hosted VM. No SaaS, no telemetry, no third party beyond the public scanners Surface queries on your behalf (crt.sh, Shodan, NVD, CISA KEV, upstream nuclei templates).
Part of the CISO Toolbox open-source suite.
Current release: v0.3.1 — 22 scanners, nuclei automatic-scan mode, executive report, weekly email digest, AI triage, screenshots, performance indexes, non-root Docker image. See release notes.
| Pillar | Capability |
|---|---|
| Discovery | CT logs, DNS brute-force, SAN pivoting, CIDR ping sweep, reverse DNS, typosquatting |
| Assessment | nmap (quick/standard/deep), nuclei automatic-scan, TLS cert, TLS grade A-F, Security headers A-F |
| Tech & CVE | Nuclei wappalyzer tech-detect → NVD + EPSS + CISA KEV matching (versioned only) |
| Exposure | Sensitive-file probe (28 paths), JS secret scan, Cloud bucket enum |
| Triage | Bulk triage, dedup with status machine, AI triage (Anthropic/OpenAI), corrective measures |
| Reporting | Executive report (print-to-PDF), Weekly email digest (SMTP), 30-day dashboard timeline |
| Security by design | SSRF guard, DNS-rebinding TOCTOU lock, CSP-strict UI, secret masking, non-root container |
- Certificate Transparency (
ct_logs) — crt.sh query for every TLS certificate ever issued under a root domain. - DNS brute-force (
dns_brute) — 1460+ compound keywords, parallel resolution, wildcard filtering, optional custom wordlist viaSURFACE_DNS_BRUTE_WORDLIST. - SAN pivoting — during the TLS scan of each host, siblings sharing the same certificate are auto-enrolled as monitored assets.
- CIDR ping sweep (
discovery) — nmap host discovery on a range, with auto-enrolment of live IPs (capped at 50 per scan to prevent runaway from large ranges).
| Scanner | Default | Role |
|---|---|---|
nmap_quick |
yes | Top-100 port scan (quick profile) |
nmap_standard |
Top-1000 ports + service/version detection | |
tls |
yes | Cert validity, expiry, SAN discovery, chain, hostname mismatch |
tls_grade |
yes | Protocol/cipher grade A-F (probes TLS 1.0→1.3 + SSL 3.0) |
security_headers |
yes | HSTS / CSP / X-Frame / XCTO / RP / PP grade A-F |
nuclei |
yes | Automatic-scan mode: wappalyzer + matching templates only |
takeover |
yes | 25-fingerprint subdomain takeover detection |
cve_lookup |
yes | NVD 2.0 + EPSS + CISA KEV (from nuclei tech-detect output) |
sensitive_files |
yes | 28-path probe (.git, .env, backup.sql, wp-config, .aws, …) |
js_analysis |
yes | 12 secret patterns grep across 20 × 512 KB JS bundles |
shodan_host |
Shodan /host/{ip} enrichment (1 Shodan credit per lookup) |
|
screenshot |
Chromium headless screenshot (opt-in, Playwright bundled) |
techstack (30-signature HTTP fingerprinting) is still available in the
registry but no longer in the default set — nuclei's wappalyzer provides
broader tech detection (3 000+ signatures) and feeds cve_lookup directly.
| Scanner | Default | Role |
|---|---|---|
email_security |
yes | SPF, DMARC, DKIM, MX analysis |
typosquatting |
yes | 60 lookalike variants (omission, transposition, alt TLDs) |
ct_logs |
yes | crt.sh subdomain discovery |
dns_brute |
yes | Wordlist brute-force with wildcard filter |
tls |
yes | Cert check (domain-level apex probe) |
takeover |
yes | NXDOMAIN + SaaS fingerprint cross-check |
shodan_domain |
Shodan /dns/domain (no credit cost) |
|
cloud_buckets |
S3 / Azure Blob / GCS / DO Spaces bucket enumeration |
| Scanner | Role |
|---|---|
discovery |
nmap ping sweep with host auto-enrolment |
Since v0.3.1 nuclei runs in automatic-scan mode (-as): it probes
the target with wappalyzer first, identifies the tech stack, then
executes only the templates that match the detected technologies.
This drops the request count from ~25 000 (brute-force all templates)
to ~1 500 (relevant ones only), completing a typical host in 3-5 minutes
instead of timing out. Default tuning: rate-limit 150, concurrency 50,
bulk-size 50, timeout 15 s.
Severity overrides — nuclei community templates classify many
security-relevant detections as info. Surface upgrades 35+ template
IDs to a minimum severity that reflects CISO-grade risk assessment:
- EOL software (
msexchange-eol,iis-eol,php-eol, …) → high - Exposed admin panels (
phpmyadmin-panel,adminer-panel, …) → high - NTLM directory leaks, IIS shortname → medium
- GraphQL batching / directive overloading → low
- Deduplication — the same
scanner | type | targetacross rescans merges into one finding with a status machine:new → to_fix → fixedornew → false_positive(with mandatory justification kept for audit). - Bulk triage — classify N findings in one click (false-positive, corrective-measure, or hard delete with cascade).
- AI triage — one-click call to Anthropic or OpenAI with a structured prompt. Button only visible when the AI assistant is configured. API key stays in browser localStorage; the call flies directly from the browser to the LLM provider — the Surface backend never sees the key.
- Corrective measures —
to_fixfindings auto-create a measure with owner, deadline, and status tracked inside Surface. - Full audit trail —
triaged_at,triaged_by, justification preserved indefinitely.
- Executive report — toolbar button renders a print-ready HTML page
in a new tab (Cmd/Ctrl+P → Save as PDF). SQL-aggregated via
GROUP BY+LIMIT— stays fast on large databases. - Weekly email digest —
smtplib-based HTML digest sent once a week to a configurable recipient list. Includes a Send now button. - Screenshots — displayed inline in finding detail view and as
thumbnails on host cards. Loaded lazily via a dedicated
/api/findings/screenshotsendpoint.
- Dashboard v2 — alert banner, 30-day timeline, top exposed hosts, top finding types, scanner health panel, measures burn-down.
- Host grouping — hosts sharing the same resolved IP are grouped into a single card (including IP-literal entries).
- Inline finding detail — clicking a finding in the host view opens its detail (description, evidence, screenshot, triage, AI) without navigating away from the host page.
- Settings accordion — 6 native
<details>sections (Language, AI assistant, Timezone, Nuclei, Shodan, SMTP), exclusive expand. - Timezone picker — 30 IANA zones, browser default.
- FR / EN bilingual — complete interface with in-app methodology and usage guides.
- Vanilla JS frontend — no framework, no bundler, CSP-strict.
- Non-root container — runs as
surface:1000(DockerfileUSERdirective). nmap/nuclei capabilities work without root. - SSRF guard —
_resolve_safe_target()rejects loopback, link-local, cloud metadata, docker-compose siblings. Locks resolved IP against DNS rebinding. Validates SMTP host, every<script src>URL injs_analysis, and all secondary fetch targets. - Email header injection — sender/recipients validated with
parseaddr+ strict regex; CRLF/NUL rejected. - Secret masking — critical/high matches truncated to
abcd…wxyzin the database (never leaked to API or AI triage). - XSS protection — all
innerHTMLescaped viaesc(), AI-generated reference links restricted tohttps?://, digest template useshtml.escape. - ReDoS hardening — JWT and all secret patterns bounded
{10,2000}. - Non-blocking SMTP —
smtplibwrapped inasyncio.to_thread. - CSP-strict —
script-src 'self', no inline, no eval. - Fail-closed auth —
SURFACE_ALLOW_NO_AUTH=1is dev-only. - Rate limiting — per-user sliding window on scan endpoints.
- Keys stored server-side — Shodan, SMTP password never returned in GET responses.
cp .env.example .env
# Edit .env — set DB_PASSWORD, JWT_SECRET, APP_URL, AUTH_TOKEN
docker compose up -d
# → open the URL from APP_URLThe first time you log in, use the value of AUTH_TOKEN from your
.env and any email address — the first user becomes admin
automatically.
For local development only:
SURFACE_ALLOW_NO_AUTH=1 docker compose up -dThis bypasses the fail-closed auth check. Never use in production.
| Var | Required | Default | Description |
|---|---|---|---|
DB_PASSWORD |
yes | — | PostgreSQL password |
JWT_SECRET |
yes | — | JWT signing key — use a long random string |
AUTH_TOKEN |
yes | — | Shared secret for the standalone login endpoint |
APP_URL |
yes | — | Public URL. Cookie Secure flag is set when this starts with https:// |
AUTH_MODE |
no | standalone |
Keep standalone for a local deploy |
SURFACE_NUCLEI_RATE_LIMIT |
no | 150 |
Nuclei requests/sec cap |
SURFACE_NUCLEI_CONCURRENCY |
no | 50 |
Nuclei template concurrency |
SURFACE_DNS_BRUTE_WORDLIST |
no | — | Path to a custom wordlist (must live inside /data/wordlists/ or /app/wordlists/) |
SURFACE_ALLOW_NO_AUTH |
no | — | Set to 1 to disable auth for local dev |
All settings that need runtime changes (Shodan key, SMTP config, nuclei
tuning, timezone) live in the Settings accordion inside the UI and
are persisted in the app_settings table — env vars are for deploy-time
bootstrap only.
See .env.example for a minimal working set.
ghcr.io/cisotoolbox/ciso-surface:v0.3.1
ghcr.io/cisotoolbox/ciso-surface:latest
Multi-arch: linux/amd64, linux/arm64.
Two tag families are published to the same image name:
:vX.Y.Z/:latest— standalone build (public, no suite-only code):vX.Y.Z-suite/:suite— suite build (adds Pilot integration routes)
This README.md describes the standalone build. The suite variant is
consumed by the CISO Toolbox demo-docker deployment.
FastAPI · SQLAlchemy async · PostgreSQL · Alembic · vanilla JS (no
framework) · nuclei · nmap · Playwright + Chromium · dnspython · httpx ·
cryptography · certifi · stdlib smtplib · stdlib ssl
surface/
├── Dockerfile # multi-stage build, non-root, nuclei + playwright
├── docker-compose.yml # FastAPI + PostgreSQL stack
├── .env.example # minimal env var template
├── alembic/ # schema migrations (001..005)
├── src/ # FastAPI backend
│ ├── main.py # app wiring, startup hooks
│ ├── auth.py # JWT + standalone token login
│ ├── models.py # SQLAlchemy models + indexes
│ ├── scanners.py # all scanners + SSRF guard
│ ├── scheduler.py # background recurring scans + weekly digest
│ ├── findings_dedup.py # status-machine dedup logic
│ ├── rate_limit.py # sliding-window rate limiter
│ └── routes/ # FastAPI routers
│ ├── monitored.py # CRUD + scan triggering
│ ├── scans.py # job history, quick scan, nuclei/shodan config
│ ├── findings.py # triage, bulk, import, screenshots
│ ├── measures.py # action plan
│ ├── reports.py # executive report + SMTP digest
│ └── ai.py # AI triage proxy
└── app/ # vanilla JS frontend
├── index.html
├── js/ # Surface app + i18n + shared libs
└── css/ # Surface-specific styles + shared cisotoolbox.css
Security hardening, nuclei refactoring, performance, UX polish:
Security
- Container runs as non-root (
USER surface, uid 1000) javascript:URI blocked in AI-generated reference links (XSS fix)- Status allowlist on
PATCH /internal/measures - Query param allowlist on
GET /findings - Playwright upgraded to 1.51 (was 1.47)
Nuclei refactoring
- Automatic-scan mode (
-as) — wappalyzer first, then matching templates - Default tuning bumped: rate-limit 150, concurrency 50, bulk-size 50
- 35+ severity overrides (EOL→high, panels→medium, NTLM→medium, etc.)
- Target normalized to
host:port(was full URL — broke host detail) cve_lookupreads nucleitech-detectoutput (techstack no longer required)- NVD keyword mapping + version truncation for better CVE matching
- Timeout/error findings downgraded to info,
scan_cleanremoved - Severity filter includes info for tech-detect visibility
Removed
github_enumscanner, GitHub settings section, routes, env vars (will be reimplemented as per-asset scan type in v0.4)SURFACE_ENABLE_SCREENSHOTSenv var (screenshot is opt-in per asset)techstackremoved from defaults (still available in registry)
Performance
- Migration 005: 4 indexes on findings (status+severity, scanner, target, created_at)
- Connection pool: pool_size=20, max_overflow=10
/api/findingsstripspng_b64from list responses/api/findings/screenshotsendpoint for host card thumbnails/api/internal/statsusesGROUP BYinstead of full-table scan- Bulk delete uses batch
DELETE INinstead of N individual deletes
UX
- Screenshot displayed inline in finding detail + thumbnail on host cards
- Inline finding detail in host page (click a finding → see detail without navigating away)
- AI triage button hidden when assistant not configured
- Host grouping fixed: IP-literal hosts now group with hostname aliases
- Playwright + Chromium bundled in Docker image
Six new scanners, executive reporting, email digest, AI triage:
sensitive_files,security_headers,tls_grade,js_analysis,cloud_buckets,github_enum(removed in v0.3.1)- Executive report (client-side print-to-PDF)
- Weekly email digest via stdlib
smtplib - AI-assisted triage (Anthropic / OpenAI)
- Collapsible Settings accordion, timezone picker
- SSRF hardening, secret masking, ReDoS fix, email header injection fix
Async manual scans via BackgroundTask, host grouping by resolved IP (alembic 004), timezone-aware date formatting, scanner tuple-return fix.
Asset tagging + criticality + risk score, scan diff bubbles, tech stack fingerprinting, CVE matching (NVD + EPSS + KEV), reverse cert lookup, per-host scan timeline, HTTP screenshots. Alembic 003.
First public release. Core scanners (nmap, tls, nuclei,
ct_logs, takeover, shodan), SSRF guard, standalone repo
force-reinit.
This repository is synchronized from the CISO Toolbox monorepo via
shared/sync-backend-modules.sh. Contributions that touch code should
be proposed against the monorepo — they will be propagated here on the
next release cut.
See LICENSE at the repo root once published.