Architecture
How FestIn is built, in one pass.
Two systems, one package
flowchart LR
subgraph scanner["Scanner CLI (festin scan)"]
A[domains / file] --> B[crawler
httpx]
B --> C[DNS discovery
dnspython]
B --> P[permutations]
P --> D[prober
S3 / GCS / Azure]
D --> E[secret analysis
rules engine]
E --> O[exports
csv / sarif / jsonl]
E --> S[state / checkpoint]
end
flowchart TB
subgraph svc["Dashboard service (festin-serve)"]
UI[SPA
static/] --> API[router.py
/api/v1]
API --> MW[JWT middleware]
API --> DB[(database.py
aiosqlite)]
API --> SCH[scheduler.py
10s loop]
API --> TASK[background task
scan_runner]
SCH --> TASK
TASK --> DB
end
Package layout
festin/
├── cli.py typer CLI: scan / serve / version
├── scan_runner.py scan pipeline (also called by the service)
├── s3.py bucket probing across cloud providers
├── secrets.py secret-detection rules
├── permutations.py bucket-name permutation engine
├── analysis.py object content analysis
├── exports.py csv / sarif / jsonl writers
├── checkpoint.py resume support
├── state.py state files (enables diffing)
├── ratelimit.py probe rate control
├── models.py shared dataclasses (ScanResult, Finding, S3Bucket)
│
└── service/
├── serve.py app factory, middleware wiring, entrypoint
├── router.py all REST endpoints (FestinRouter)
├── auth.py bcrypt + JWT + middleware
├── database.py aiosqlite layer (schema + CRUD)
├── scheduler.py periodic scan loop + orchestration
├── queues.py in-memory queue (Redis path unwired)
└── static/ the SPA (vanilla JS, hash routing)
Two HTTP servers live in the repo
festin/api.py is a legacy read-only state-file server. The real dashboard is festin/service/serve.py. Don't confuse them.
Request lifecycle (scan trigger)
sequenceDiagram
actor U as UI/curl
participant R as router.py
participant DB as database.py
participant T as _execute_scan (task)
participant S as scan_runner
U->>R: POST /scans/run-scan {domains, project_id}
R->>DB: find/create domain, create_scan (status=pending)
R-->>U: 202 {scan_id, job_id}
R->>T: asyncio.create_task
T->>DB: update status=running
T->>S: run_scan(namespace, domains)
S-->>T: ScanResult(buckets, findings)
T->>DB: persist_scan_results (real rows)
T->>DB: update status=completed
Note over U: SPA polls /scans/{id} every 4s
until terminal status
Authentication flow
sequenceDiagram
participant C as client
participant MW as JWTMiddleware
participant H as handler
participant A as AuthService
C->>MW: request + Bearer token
MW->>MW: verify signature + expiry
alt valid token
MW->>H: request["user"] = username
H->>A: role check for mutations (_admin_guard)
else no token + /auth/register
MW->>H: anonymous pass-through
Note over H: handler decides:
user_count==0 → admin bootstrap
else 401
else no token, any other path
MW-->>C: 401
end
The invariant: the middleware identifies, the handler authorizes. /auth/register is never exempted from the middleware — exempting it made valid admins look anonymous (a real bug; see ADR #4).
Data model
erDiagram
PROJECTS ||--o{ DOMAINS : contains
DOMAINS ||--o{ SCANS : has
SCANS ||--o{ FINDINGS : yields
SCANS ||--o{ BUCKETS : finds
USERS }o--o{ PROJECTS : "access (UI-level)"
| Table | Key columns |
|---|---|
projects | id, name (unique), description |
domains | id, domain_name, project_id, enabled |
scans | id, domain_id, project_id, status, buckets_found, findings_count, started_at, finished_at |
findings | id, scan_id, bucket, object, rule, severity, line, match (redacted) |
buckets | id, scan_id, name, objects_count |
scheduled_scans | id, domain, interval_minutes |
users | id, username, password_hash (bcrypt), role |
Migrations are idempotent: CREATE TABLE IF NOT EXISTS + guarded ALTER TABLE inside Database.migrate() (executescript).
Scaling story
The deliberate seams for growth (all inside festin/service/):
- Database — swap
Databaseinternals for asyncpg/Postgres. Callers never touch raw connections. - Scan execution — replace the in-process
asyncio.create_taskinrun-scanwith a queue consumer;queues.pyalready sketches the Redis path. - Scheduler — extract to its own deployment; it only needs the DB.
See HA patterns for how these compose.