Architecture¶
Suspicious is a set of containerised services coordinated by a Django REST API and a Celery worker.
Services¶
| Service | Role |
|---|---|
suspicious |
Django REST API (Gunicorn, port 9020) |
suspicious_celery |
Celery beat + worker (background jobs, cortex sync, case finalisation) |
suspicious_ui |
React/Vite frontend (Nginx, port 9021) |
db_suspicious |
MariaDB 12 (primary; opt-in replica via R6 router) |
redis_broker / redis_cache |
Valkey 9 — Celery broker + Django cache |
elasticsearch |
Search and indexing |
rustfs |
S3-compatible object storage (artifacts/attachments) |
cortex |
Analyzer execution engine (YARA, AI, sandbox) |
chromadb |
Vector DB for semantic similarity search |
feeder |
IMAP poller that auto-ingests emails (container email_feeder) |
traefik |
Reverse proxy with TLS termination |
tempo / grafana |
Optional OpenTelemetry trace store + dashboards (manual setup under deployment/docker/monitoring/) |
Request / Analysis flow¶
- A user submits an email/file/URL/IP/hash via the web UI, API, or the email feeder's IMAP polling.
- Django creates a
Case, then dispatches Cortex analyzers viaCortexJob.run_analyzer— each call writes anAnalyzerReportand aCaseAnalyzerJobledger row atomically (case_id ↔ cortex_job_id). - Cortex runs analyzers asynchronously and POSTs to
/api/cortex/webhook/(HMAC-signed, jobId-deduped) as each job finishes. - The webhook view looks up the case via a single indexed read on
CaseAnalyzerJob, then enqueuesprocess_cortex_job(case_id, job_id)on Celery; the task takes a per-case Redis lock, updates the ledger, and callsfinalise_caseonce all pending jobs are non-pending. finalise_caseaggregates scores (Safe / Inconclusive / Suspicious / Dangerous), pushes to TheHive/MISP if configured, queries ChromaDB for similar past cases, notifies the reporter by SMTP, and updates dashboard KPIs.- Celery beat runs
update_ongoing_casesevery 300s as a webhook fallback, andfail_stale_jobsevery 600s to auto-fail jobs pending beyond the stale-job timeout.
URL analysis planning¶
URLs extracted from a reported email are not all dispatched blindly. At
dispatch time (step 2, mail path only), plan_url_analysis collapses equivalent
URLs by canonical key, reuses recent results across cases within
reuse_ttl_days, and caps analysis per registered domain to the most
interesting URLs. Every extracted URL is still recorded — capped ones are marked
skipped and stay analyzable on demand via
POST /api/submissions/<id>/urls/<url_id>/analyze/. The planner is fail-open
(any error → analyze the URL) and kill-switched by url_analysis.enabled.
Direct single-URL submission bypasses it. See
Observable Processors.
See Components for each part in detail, and the API Reference for endpoints.