A self-hosted threat intelligence aggregation platform for MSP operators. ThreatPilot pulls from external threat intel feeds (CISA KEV, security RSS), normalizes the data, and presents a daily-updated dashboard showing active CVEs, patch recommendations, threat actor activity, and security news signal analysis.
- CVE Tracking — Ingests CISA Known Exploited Vulnerabilities (KEV) feed with severity scoring, CVSS, and EPSS data
- Patch Queue — Automatically generates prioritized patch recommendations for your monitored software inventory
- News Aggregation — Pulls from security RSS feeds (Bleeping Computer, Krebs on Security, Qualys Blog) with signal rating
- Monitored Apps — Define your software stack; CVEs are automatically matched against it
- Daily Reports — Generates PDF digests of the previous 24 hours of CVEs and news
- Scheduled Jobs — Feed fetching, report generation, and cleanup run automatically via APScheduler
- Dark UI — Bootstrap 5 dark theme with Chart.js dashboards and HTMX-powered interactions
| Layer | Technology |
|---|---|
| Framework | Flask 3.x |
| ORM | Flask-SQLAlchemy / SQLAlchemy 2.0 |
| Migrations | Flask-Migrate / Alembic |
| Database | MySQL 8.0 |
| Auth | Flask-Login + Werkzeug |
| Scheduler | APScheduler |
| HTTP Client | httpx |
| RSS Parsing | feedparser |
| PDF Reports | WeasyPrint |
| Frontend | Jinja2 + HTMX + Chart.js + Bootstrap 5 |
- Docker 24+
- Docker Compose v2 (included with Docker Desktop)
- Git
git clone https://github.com/jly-engineer/threat_intelligence_app.git threatpilot
cd threatpilotpython3 -c "import secrets; print(secrets.token_hex(32))"cp .env.example .envOpen .env and set the required values:
# Required — use a long random string (e.g. openssl rand -hex 32)
SECRET_KEY=your-secret-key-here
# Required — password for the built-in admin account
DEFAULT_ADMIN_PASSWORD=your-admin-password
# MySQL credentials
# DB_ROOT_PASSWORD and DB_PASSWORD should be strong random strings.
# Docker Compose creates the MySQL database using these values on first run,
# so there is no pre-existing database to match — just generate and paste.
# Example: python3 -c "import secrets; print(secrets.token_hex(16))"
DB_ROOT_PASSWORD=your-random-root-password
DB_PASSWORD=your-random-db-password
# DB_USER and DB_NAME can stay as defaults — they are labels, not secrets.
DB_USER=threatpilot
DB_NAME=threatpilot
# Optional — your local timezone
SCHEDULER_TIMEZONE=America/Indiana/Indianapolis
# Optional — increases NVD API rate limit
# Free key at https://nvd.nist.gov/developers/request-an-api-key
NVD_API_KEY=
DATABASE_URLis constructed automatically by Docker Compose from theDB_*vars above and does not need to be set manually.
docker compose run --rm app flask db init
docker compose run --rm app flask db migrate -m "initial"docker compose up -dThis will:
- Pull the MySQL 8.0 image
- Build the ThreatPilot app image
- Wait for the database to be ready
- Run
flask db upgradeto apply migrations - Seed default feeds, admin user, and settings
- Start the app on port 8082
Open your browser to http://localhost:8082 and log in with:
- Username:
admin - Password: the value you set for
DEFAULT_ADMIN_PASSWORD
The following feeds are seeded automatically on first run:
| Feed | Type | Source |
|---|---|---|
| CISA KEV | API | cisa.gov |
| Bleeping Computer | RSS | bleepingcomputer.com |
| Krebs on Security | RSS | krebsonsecurity.com |
| Qualys Blog | RSS | blog.qualys.com |
Additional feeds can be added from the Feed Sources page in the app.
| Job | Schedule | Description |
|---|---|---|
fetch_all_feeds |
Daily 06:00 | Pull all enabled feed sources |
generate_daily_report |
Daily 07:00 | Build and save the PDF digest |
cleanup_old_news |
Weekly Sunday | Delete news items older than 90d |
# View live logs
docker compose logs -f app
# Stop the stack
docker compose down
# Stop and wipe all data (destructive)
docker compose down -v
# Rebuild after a code update
docker compose up -d --build
# Open a shell inside the app container
docker compose exec app bash
# Manually apply database migrations
docker compose exec app flask db upgrade| Volume | Contents |
|---|---|
db_data |
MySQL database files |
reports_output |
Generated PDF reports |
backups |
Database backups |
Volumes survive docker compose down. Use docker compose down -v only for a clean slate.
git pull
docker compose up -d --buildThe entrypoint runs flask db upgrade automatically on every startup, so migrations are applied on upgrade with no additional steps.
| Port | Service |
|---|---|
| 8082 | ThreatPilot web UI |
| 3306 | MySQL (internal only) |
Port 8081 is reserved for Profit Pilot and is not used by this application.
If the app starts but crashes with Table 'threatpilot.users' doesn't exist, the database migrations did not run. This usually means the db_data volume has stale state from a previously failed startup. Wipe it and restart:
docker compose down -v
docker compose up -d --buildYou should see Running upgrade -> 5aee1ee15443 in the logs before gunicorn starts. If you don't, the migration scripts are not reaching the container — verify migrations/versions/ is not excluded in .dockerignore.
The MariaDB container does not use SSL. If the mysql CLI in the entrypoint fails with a TLS error, ensure the --ssl=FALSE flag is present in entrypoint.sh:
mysql -h "${DB_HOST}" -uroot -p"${DB_ROOT_PASSWORD}" --ssl=FALSE <<SQLCheck logs for the root cause:
docker compose logs --tail=50 appThe most common causes are a missing or weak SECRET_KEY, a bad DATABASE_URL, or a migration failure. The entrypoint exits with a non-zero code on migration failure, which triggers Docker's restart: unless-stopped policy.
MIT