See what attackers see. Use it before they do.
Use it as a red teamer to map external surface fast on targets you're authorised to test. Use it as a defender to see what's leaking out of your own infrastructure: subdomains, exposed ports, dangling CNAMEs, missing TLS, known CVEs, without paying $500-5000/mo for a commercial EASM platform.
OpenEASD wraps the open-source recon tools security teams already use: subfinder, amass, alterx, dnsx, subzy, cloud_enum, naabu, nmap, httpx, gau, katana, nuclei, gitleaks, behind a single web UI with scheduling, alerts, and findings tracking. Twenty-eight tools across DNS/DNSSEC, historical DNS records (passive; A/AAAA/MX a domain has resolved to over time), email (SPF/DMARC/DKIM/MTA-STS/open-relay), TLS, SSH, ports, CVEs, subdomain takeover, ASN/IP-range discovery, historical URLs, cloud assets, exposed secrets in JavaScript, leaked secrets in public GitHub (passive; searches GitHub's code-search API for the org's committed credentials and runs gitleaks over the hits — bring-your-own GitHub token), public-source infrastructure exposure (passive; internal hostnames, cloud buckets, and API endpoints leaked in the org's public GitHub repos), infostealer-log exposure (via Hudson Rock's keyless Cavalier API), data-breach exposure (passive; free XposedOrNot tier out of the box, authoritative with a bring-your-own Have I Been Pwned key), Shodan-sourced exposure (passive; free InternetDB tier out of the box, richer with a bring-your-own Shodan key), lookalike / typosquat domain detection (passive; phishing infrastructure and brand abuse targeting your domain), technology fingerprinting, web hygiene, and CVE prioritisation (EPSS + CISA KEV). Run a passive scan (public-source only, no authorization needed) or an active scan (probes the target, authorization required). Self-hosted, MIT-licensed, one docker run. Results stay on your machine — unless you enable the optional Cloudflare Workers AI analysis (off by default, explicit consent required), which sends finding data to your own Cloudflare account.
Optionally, OpenEASD can rank each scan's findings by exploitability and explain why (a "fix these first" list with per-finding rationale), schedule targeted follow-up scan steps based on what was found, and write plain-language report and alert summaries — using Cloudflare Workers AI with your own account (credentials entered on the AI Analysis page, or via CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN env vars). Off by default; enabling it requires explicit consent in the UI, every call is audit-logged (never the prompt or response contents), and active follow-up scanning still requires the same domain authorization as manual scans.
Built by Rathnakara G N and Ashok S Kamat of Cybersecify, the same tool we run in engagements and on our own infrastructure.
- In-house security engineers and IT-doing-security teams at small-to-mid orgs scanning their own external surface
- Small security consultancies monitoring a handful of clients
- Bug bounty hunters who want a unified view of recon output across programs they're authorised to test
- Solo self-hosters and security learners auditing their own infra
- Enterprise SOCs: no RBAC, SAML, multi-tenant, or Postgres (yet)
- Anyone needing to scan domains they don't own or aren't authorised to test: OpenEASD is intentionally not a "scan-anyone" hosted service. Running it implies you own or have written authorisation for your targets
OpenEASD is a security tool, so it's reasonable to ask whether the tool itself is trustworthy. Here's what we do (and don't do) to make that auditable.
Every external tool we ship is downloaded inside the Docker build from its maintainer's official source. No repackaging, no mirroring:
| Tool | Upstream source |
|---|---|
subfinder, dnsx, naabu, httpx, nuclei, alterx, katana |
github.com/projectdiscovery (signed releases) |
amass |
github.com/owasp-amass/amass (OWASP) |
subzy |
github.com/PentestPad/subzy (Go modules) |
gau |
github.com/lc/gau (Go modules) |
cloud_enum |
github.com/initstring/cloud_enum |
gitleaks |
github.com/gitleaks/gitleaks (MIT, signed releases) |
nmap |
apt-get install nmap (Ubuntu 24.04 official) |
Verbatim install commands are in the Dockerfile; every version is pinned. To verify a binary, pull the same version directly from the upstream URL; it should byte-match what ships in the image.
Every push to main and every vX.Y tag triggers
.github/workflows/ci.yml, which builds
linux/amd64 images and publishes to
ghcr.io/cybersecify/openeasd. The build is reproducible from the
public source; the published image carries:
- SBOM (Software Bill of Materials, SPDX format): every package in
the image, retrievable via
docker buildx imagetools inspect - SLSA provenance attestation: cryptographic proof that the image was built by this repo's GitHub Actions, not swapped or rebuilt elsewhere
# Both images carry their own SBOM + provenance; inspect either target:
docker buildx imagetools inspect ghcr.io/cybersecify/openeasd-web:v2.0.0 --format '{{ json .SBOM }}'
docker buildx imagetools inspect ghcr.io/cybersecify/openeasd-worker:v2.0.0 --format '{{ json .Provenance }}'- No telemetry. OpenEASD doesn't phone home, doesn't check for updates, doesn't send your scan results anywhere.
- No auto-update. The version you pulled is the version that runs.
- No external callbacks during scans: the only network traffic is the scan itself, originating from the tools shipped in the image.
Block all egress at your firewall (except what's needed to reach scan targets) and OpenEASD continues to work.
| Check | Tool | Runs on |
|---|---|---|
| Python SAST | bandit |
Every push + PR |
| Python CVE scan | pip-audit |
Every push + PR |
| Semantic security analysis | CodeQL (Python + JS/TS) | Every push + PR + weekly |
GitHub Actions are pinned to commit SHAs (not floating tags) so a compromised action update can't silently rotate into the build. Dependabot keeps them current on a weekly cadence.
Open security alerts are public on the Security tab; we don't hide gaps.
If you don't trust the published Docker image, build it yourself; every step is in the Dockerfile:
git clone https://github.com/cybersecify/OpenEASD
cd OpenEASD
docker build -t openeasd .- Docker images are not yet cosign-signed. SBOM + SLSA provenance in the manifest covers the "what's in it + who built it" question; cosign would add a separate, externally-verifiable signature. On the roadmap.
For vulnerability reporting, see SECURITY.md.
The scan tools (nuclei and amass especially) are memory-hungry. On a host with too little RAM the kernel will OOM-kill them mid-scan, so scans finish partial or empty.
| RAM | vCPU | Disk | |
|---|---|---|---|
| Minimum | 2 GB | 1 | 5 GB |
| Recommended | 4 GB | 2 | 10 GB |
| 1 GB host | set OPENEASD_LOW_MEMORY=true |
1 | 5 GB |
OpenEASD adapts to the host automatically via OPENEASD_PROFILE (default
auto, detected from RAM):
| Profile | When | Behaviour |
|---|---|---|
low |
< 2 GB (or OPENEASD_LOW_MEMORY=true) |
tools run sequentially, nuclei throttled, amass skips brute — completes on ~1 GB without OOM |
balanced |
2–8 GB | phase-parallel, nuclei -c 25 |
high |
≥ 8 GB | phase-parallel + higher local concurrency and deeper enumeration |
Set -e OPENEASD_PROFILE=low|balanced|high to override auto-detection. On a 1 GB
box, auto-detect picks low (or set it explicitly); a few GB of swap also helps.
On politeness: a bigger box scales local parallelism, not how hard the
target is hit — the per-target request rate stays capped across all profiles, so
high doesn't flood targets or trip their WAFs (which the report flags anyway).
OpenEASD runs as three services (PostgreSQL + web + worker). Set the secrets in
docker-compose.yml (SECRET_KEY, DB_PASSWORD, ALLOWED_HOSTS), then:
docker compose up -d --buildOpen http://localhost:8000 → log in with admin / admin (you'll be forced to set a new password) → add a domain → run a scan. Full env-var reference, update path, Kubernetes manifests, and standalone (no-Docker) install are under Deployment.
- Automated pipeline: 28-tool scan workflow from domain to findings
- Network attack surface scanning: CVEs, TLS/cert issues, SSH config, network protocol vulnerabilities
- CVE prioritisation: EPSS exploit-probability scores + CISA KEV (known-exploited-in-the-wild) flags enrich CVE findings in place, so you triage by real-world risk rather than severity alone
- Dynamic workflows: Create custom scan configurations, enable/disable tools per workflow
- Tool auto-registration: Add new tools with zero core modification
- Live scan progress: Real-time pipeline status with per-tool step tracking
- Scan stop/cancel: Graceful cancellation between tool steps
- Unified findings: All tools write to a single Finding model with lifecycle tracking
- Asset inventory: A persistent, deduplicated view of your attack surface across scans — every subdomain/IP/port/URL with first-seen / last-seen / active-or-gone status; pivot from any asset to its findings and scan history (Assets page +
/api/assets/) - Continuous monitoring: Per-domain rescans on a configurable schedule (6h / 12h / 24h / 48h / weekly)
- Subscan: Re-run specific tools on existing scan assets without full rediscovery
- Reports: CSV and PDF export
- Alerts: Slack and Teams webhooks with configurable severity threshold; test button and alert history in the UI
- Scheduling: One-time, recurring, and daily automated scans
- Domain authorization enforcement: Each domain requires a recorded authorization (owner / written consent / bug bounty) before scans can start. Managed in Django admin; React blocks the Scan button and the API enforces it server-side
- JWT auth: Stateless Bearer token authentication with refresh token rotation
- Forced password change: Default
admin/adminpassword must be changed on first login
── Domain Intelligence ──────────────────────────────────────────────────────
Phase 1 Domain Security - DNS, DNSSEC chain-of-trust, email
(SPF/DMARC/DKIM/MTA-STS/open-relay), RDAP checks
Phase 1 Hudson Rock - Infostealer-log exposure via Hudson Rock's keyless
Cavalier API (aggregate counts only, no plaintext)
Phase 1 GitHub Secrets - Leaked secrets in public GitHub via gitleaks
(passive; BYO GITHUB_TOKEN, redacted before storage)
Phase 1 Typosquat - Lookalike / typosquat domain detection (passive;
registered lookalikes via public DNS — phishing/brand abuse)
Phase 1 Breach Check - Data-breach exposure via XposedOrNot (free/keyless)
or Have I Been Pwned (BYO key); counts only, no PII
Phase 1 DNS History - Historical A/AAAA/MX records via a passive-DNS
dataset (passive; BYO DNS_HISTORY_API_URL)
── Surface Enumeration ─────────────────────────────────────────────────────
Phase 2 Subfinder - Passive subdomain enumeration
Phase 2 Amass - Active subdomain enumeration
Phase 2 Alterx - Subdomain permutation from discovered subdomains
Phase 2 ASN Discovery - Owned ASN/CIDR ranges via amass intel (reports only)
Phase 2 GitHub Org Recon - Infra refs (internal hostnames, cloud buckets, API
endpoints) leaked in the org's public GitHub repos
(passive; official API — keyless, richer with a token)
Phase 3 DNSx - DNS resolution, public IP filtering
Phase 4 Takeover Check - Subdomain takeover detection via subzy
Phase 4 Cloud Assets - Public S3/Azure/GCP bucket enumeration (cloud_enum)
── Port Discovery ───────────────────────────────────────────────────────────
Phase 5 Naabu - TCP port scanning (top 100; CDN edge IPs excluded)
Phase 6 Service Detection - Classify ports as web/non-web via nmap -sV (auto)
── Network Exposure ─────────────────────────────────────────────────────────
Phase 7 Nmap - CVE scanning via NSE vulners (non-web ports)
Phase 7 TLS Checker - Certificate, cipher, and protocol analysis
Phase 7 SSH Checker - SSH configuration audit
Phase 7 Nuclei Network - Network protocol vuln templates (non-web ports)
── Web Exposure ─────────────────────────────────────────────────────────────
Phase 8 httpx - Web probing, URL discovery, technology fingerprinting
Phase 9 Historical URLs - Archived URL discovery via gau
Phase 10 Katana - Deep URL crawl on top of httpx
Phase 11 Nuclei - Web vulnerability scanning (community templates)
Phase 11 Web Checker - Security headers, cookies, CORS analysis
Phase 11 JS Secrets - Hardcoded secrets in JavaScript via gitleaks
── Prioritization ───────────────────────────────────────────────────────────
Phase 12 CVE Intel - Enrich CVE findings with EPSS + CISA KEV
── AI analysis (optional; off by default, bring-your-own Cloudflare) ──────────
Post-scan Triage - Rank findings by exploitability (CISA KEV + EPSS
outrank raw CVSS) with a per-finding rationale, plus
plain-language report and alert summaries
Post-scan Orchestration - A bounded agent may schedule targeted follow-up
subscans based on what was found (still gated by the
same domain authorization as manual scans)
Every scan probe also carries an honest OpenEASD/1.0 user agent (so a target
can allowlist it), and the report flags WAF/edge blocking so an empty result
means "clean", never "silently blocked".
The AI stage runs only when Cloudflare Workers AI credentials are provided
(saved on the AI Analysis page, or CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN
env vars) and an operator enables it with explicit consent. With it off — the
default — the pipeline ends at Phase 12 and nothing leaves your machine. Every AI
call is recorded in an audit log (time, scan, purpose, model, token counts,
finding IDs); prompt and response contents are never stored.
apps/core/ - Infrastructure (never changes)
api/ - Django Ninja API, JWT auth, error handlers
api/tokens/ - BlacklistedToken model (JWT JTI blacklist)
assets/ - Network assets: Subdomain, IPAddress, Port
web_assets/ - Web assets: URL
service_detection/ - Classifies ports as web/non-web (core, always runs)
findings/ - Unified Finding model
scans/ - ScanSession, pipeline orchestrator
workflows/ - Dynamic workflow engine + tool registry
scheduler/ - Scan callables (daily / monitoring sweep / user-schedule sweep / watchdog) driven by DBOS @scheduled workflows
durable/ - DBOS engine, scan/AI workflows, dbos_worker command
notifications/ - Slack/Teams alerts
insights/ - Scan summaries, charts
reports/ - CSV/PDF export
ai/ - AI analysis (Cloudflare Workers AI, BYOK): triage, orchestration, summaries, consent + audit
domains/ - Domain management
dashboard/ - UI home
apps/ - Tool apps (add/remove freely)
domain_security/ - DNS, email, RDAP checks
hudson_rock/ - Infostealer-log exposure (Hudson Rock Cavalier API)
github_secrets/ - Leaked secrets in public GitHub (gitleaks, BYO token)
breach_check/ - Data-breach exposure (XposedOrNot free / HIBP BYO key)
subfinder/ - Passive subdomain enumeration
amass/ - Active subdomain enumeration
alterx/ - Subdomain permutation (from discovered subdomains)
dnsx/ - DNS resolution
takeover_check/ - Subdomain takeover detection (subzy)
naabu/ - Port scanning
nmap/ - CVE scanning (NSE vulners)
tls_checker/ - TLS/cert/cipher analysis
ssh_checker/ - SSH configuration audit
nuclei_network/ - Network protocol vuln scanning
httpx/ - Web probing
historical_urls/ - Archived URL discovery (gau)
katana/ - Deep URL crawl
nuclei/ - Web vulnerability scanning
web_checker/ - Security headers, cookies, CORS
frontend/ - React 19 + Vite 8 SPA
src/pages/ - Page components
src/components/ - Shared UI primitives (Badge, Spinner, Pagination, ConfirmButton)
src/components/ui/ - shadcn/ui primitives (Button, Card, Table, AlertDialog, …)
src/lib/ - queryClient, utils
src/api/client.js - JWT apiFetch wrapper
src/auth.js - localStorage token helpers
OpenEASD runs as three services — PostgreSQL + web + worker. Edit the
secrets in docker-compose.yml (SECRET_KEY, DB_PASSWORD, ALLOWED_HOSTS),
then:
docker compose up -d --builddb— PostgreSQL 17 (app data + the DBOS durable-execution schema)web— the slimopeneasd-webimage (UI/API + synchronous CSV/PDF reports; no scanner tools)worker— theopeneasd-workerimage (DBOS worker + the full scanner matrix,NET_RAW)
Run all three tiers — this is the recommended architecture. Keeping
web,worker, anddbas separate containers is deliberate:
- Privilege separation — the internet-facing
webimage carries no scanner tools and noNET_RAW, so a compromised web surface doesn't inherit the offensive toolkit or raw-socket access (those live only on the worker, which isn't exposed).- Fault isolation — nuclei/amass are memory-hungry; if one is OOM-killed it takes down only a worker, not the UI/API.
- Independent scaling — Postgres has no single-writer lock, so you can run multiple
workerreplicas pulling the same DBOS queue for scan throughput while thewebtier scales separately for HTTP load.Collapsing
web+workerinto one container is possible for a small, trusted, single-user evaluation, but it puts the scanner tools andNET_RAWon the exposed process — not recommended for internet-facing deployments. Keepdbas its own container either way so app replacement never risks data.
Open http://localhost:8000, then log in with admin / admin. You will be forced to set a new password before accessing the app.
Production note:
ALLOWED_HOSTS="*"is fine for evaluation on a private network. For internet-facing deployments, narrow it to your actual hostname or IP (e.g.ALLOWED_HOSTS="scanner.example.com,127.0.0.1") and setCSRF_TRUSTED_ORIGINSif accessing over a domain.
docker compose pull # pulls fresh openeasd-web + openeasd-worker
docker compose up -d # recreates changed containers; the db volume persists all data| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
insecure default | Django secret key; set this in production |
FIELD_ENCRYPTION_KEY |
(derived from SECRET_KEY) |
Optional Fernet key (urlsafe-base64, 32 bytes) encrypting BYOK API keys/webhooks at rest. Set it to decouple secret encryption from SECRET_KEY rotation; changing the effective key makes stored secrets unreadable (re-enter them) |
LOGIN_RATELIMIT_ENABLED / LOGIN_RATELIMIT_MAX_FAILURES / LOGIN_RATELIMIT_WINDOW_SECONDS / LOGIN_RATELIMIT_LOCKOUT_SECONDS |
True / 5 / 900 / 900 |
Brute-force protection on the login endpoint: after N failed logins from an IP within the window, that IP is locked out (429) for the lockout period |
LOGIN_RATELIMIT_TRUST_FORWARDED_FOR |
True |
Key the limiter on the client's X-Forwarded-For (correct behind the mandated TLS proxy). Set False only if running without a trusted proxy — then XFF is ignored (it would be attacker-spoofable) and the raw REMOTE_ADDR is used |
ALLOWED_HOSTS |
localhost,127.0.0.1 |
Comma-separated hostnames (add your server IP/domain) |
CSRF_TRUSTED_ORIGINS |
(none) | Required if accessing via a domain, e.g. https://openeasd.example.com |
DEBUG |
False |
Set True only for local development |
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD |
db / 5432 / openeasd / openeasd / — |
PostgreSQL connection (or set DATABASE_URL) |
DBOS_SCAN_CONCURRENCY |
2 |
Max scans executing at once (Postgres has no single-writer lock) |
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN |
(none) | Optional AI analysis (or enter on the AI Analysis page) |
SLACK_WEBHOOK_URL |
(none) | Slack incoming webhook for scan alerts (can also be set in the Notifications UI) |
MS_TEAMS_WEBHOOK_URL |
(none) | Teams incoming webhook for scan alerts (can also be set in the Notifications UI) |
ALERT_SEVERITY_THRESHOLD |
high |
Minimum severity to trigger alerts; overridden by the Notifications UI setting |
SCAN_DAILY_HOUR |
2 |
Hour for daily scheduled scans (24h, UTC) |
SCAN_DAILY_MINUTE |
0 |
Minute for daily scheduled scans |
REPORT_CTA_URL |
(none) | Optional URL appended to PDF and CSV reports. Renders only when both REPORT_CTA_URL and REPORT_CTA_TEXT are set |
REPORT_CTA_TEXT |
(none) | Optional call-to-action line shown alongside REPORT_CTA_URL in PDF/CSV reports |
Manifests are in k8s/. Requires a cluster with an nginx Ingress controller.
1. Set your secret
Edit k8s/secret.yaml and replace REPLACE_WITH_OPENSSL_RAND_HEX_32 with a real key:
openssl rand -hex 322. Set your domain
Edit k8s/configmap.yaml (ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS) and k8s/ingress.yaml (host) to match your domain.
3. Deploy
kubectl apply -k k8s/4. Update to latest image
kubectl rollout restart deployment/openeasd-web deployment/openeasd-worker -n defaultThree tiers, each its own workload — a default deploy is 3 pods (logs → stdout, no PVC):
| Workload | Command | Resources |
|---|---|---|
openeasd-web Deployment |
init (migrations/admin) → gunicorn (2 workers) |
256Mi–512Mi, no NET_RAW, Service → :8000 |
openeasd-worker Deployment |
manage.py dbos_worker |
512Mi–4Gi, NET_RAW for nmap/naabu, no Service |
openeasd-postgres StatefulSet |
PostgreSQL 17 | 10Gi PVC |
Only the openeasd-web Deployment runs migrations (its initContainer); the openeasd-worker Deployment waits for them via the role-aware entrypoint (OPENEASD_ROLE=worker → migrate --check), so there's no DDL race.
Scaling: each Deployment is
replicas: 1by default. Postgres removes the old single-writer limit, soopeneasd-workerscales independently —kubectl scale deploy/openeasd-worker --replicas=Nadds workers all draining the same DBOS queue, without touching the web tier.
GET /health/ returns {"status": "ok"}, used by K8s readiness and liveness probes (no auth required).
The fastest way to get running on a Linux server or macOS without Docker:
git clone https://github.com/cybersecify/OpenEASD.git
cd OpenEASD
# Linux (installs all deps + systemd services)
sudo ./install.sh
# macOS
./install.sh
# Skip amass if you don't need active subdomain enumeration (saves ~10 min)
sudo ./install.sh --skip-amassThe script installs: Python/uv, Node.js, nmap, ProjectDiscovery tools (subfinder, dnsx, naabu, httpx, nuclei), amass, builds the frontend, generates a .env with a random SECRET_KEY, runs migrations, creates the admin user, grants NET_RAW to nmap/naabu, and sets up systemd services on Linux.
After install, edit .env to add your domain to ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS, then restart:
sudo systemctl restart openeasd-web openeasd-workerOn macOS, start manually:
uv run gunicorn openeasd.wsgi:application --bind 0.0.0.0:8000 --workers 2
uv run manage.py dbos_worker # second terminal (needs PostgreSQL running)# Starts Django (:8001) + the Vite dev server + the DBOS worker together
# (needs PostgreSQL running). See the Makefile for the individual commands.
make dev
# React app at http://localhost:5173Full local-dev guide (prerequisites, first-time setup, CI parity,
troubleshooting): docs/DEVELOPMENT.md.
GitHub Actions runs on every push to main and v* tags:
- pytest: fast test suite (~1,730 tests, excludes the 52 slow DNS/RDAP tests in
test_domain_security.py) - bandit: Python SAST scan
- pip-audit: dependency CVE scan
- Frontend build:
npm ci && npm run build - Docker build: amd64 smoke-build on every push
- Publish to GHCR:
amd64images published on everymainpush and version tags
The REST API is served at /api/ via Django Ninja with JWT Bearer authentication.
- Docs: http://localhost:8000/api/docs (OpenAPI/Swagger UI)
- Auth:
POST /api/token/pair→{access, refresh}tokens - Token refresh:
POST /api/token/refresh
Create a tool app with tool_meta in its AppConfig; no core files to modify:
# apps/my_tool/apps.py
from django.apps import AppConfig
class MyToolConfig(AppConfig):
name = "apps.my_tool"
label = "my_tool"
verbose_name = "My Tool"
tool_meta = {
"label": "My Tool",
"runner": "apps.my_tool.scanner.run_my_tool",
"phase": 6,
"phase_group": "Port Discovery",
"requires": ["naabu"],
"produces_findings": True,
}Then add "apps.my_tool" to INSTALLED_APPS in openeasd/settings/base.py. The tool auto-registers in the workflow system.
apps/my_tool/
apps.py - AppConfig with tool_meta
models.py - Empty (uses core Finding/asset models)
scanner.py - Orchestrator: collect -> analyze -> save
collector.py - Runs binary or probes, returns raw data
analyzer.py - Parses data, builds Finding/asset objects
# Fast tests (excludes slow DNS tests, ~1,730 tests)
uv run pytest tests/ --ignore=tests/unit/test_domain_security.py
# All tests (~1,780 total, incl. the 52 slow DNS/RDAP tests)
uv run pytest tests/Backend:
- Django 5.2 LTS: Web framework
- Django Ninja: REST API with OpenAPI docs
- DBOS: Durable-execution engine — task queue + scheduler, Postgres-backed (crash-resumable scans)
- croniter: Cron parsing for the DBOS user-schedule sweep
- WhiteNoise: Serves static files in production (Docker) with gzip compression
- PostgreSQL (+ psycopg 3): Database — app data and the DBOS checkpoint schema
- paramiko: SSH protocol inspection
- cryptography: X.509 certificate analysis
- xhtml2pdf: PDF report generation
- django-ninja-jwt: JWT auth for the Ninja API (built on PyJWT); access + refresh tokens, blacklist on logout
Frontend:
- React 19 + Vite 8: SPA with hot module replacement
- Tailwind CSS 3 + shadcn/ui: Utility-first styling with Radix UI component primitives
- sonner: Toast notifications
- Vanilla popstate router (no react-router)
MIT License - see LICENSE