fix: resolve 20 security vulnerabilities from comprehensive audit

Critical (1-3):
- Replace hardcoded admin credentials with secure auto-generation (seed.py)
- Enforce SECRET_KEY configuration, fail in production if missing (config.py)
- Add Zip Slip and Zip Bomb protection to all ZIP import services

High/Medium (4-9):
- Add 50MB file size limit and extension whitelist to evidence uploads
- Configure CORS origins via environment variable instead of hardcoded
- Migrate JWT storage from localStorage to HttpOnly cookies (frontend+backend)
- Add rate limiting (5/min) on login endpoint via slowapi
- Replace generic dict payloads with Pydantic schemas (mass assignment)

Medium (10-17):
- Check is_active on login to prevent disabled users from authenticating
- Sanitize exception messages in API responses (system, data_sources)
- Escape LIKE wildcards in all ilike search filters across 8 routers
- Run Docker container as non-root user (appuser)
- Make MINIO_SECURE configurable via environment variable
- Add password complexity policy (12+ chars, upper/lower/digit/special)
- Implement JWT token revocation via in-memory blacklist + reduce TTL to 15min
- Replace xml.etree with defusedxml to prevent Billion Laughs attacks

Low (18-20):
- Add security headers to Nginx (CSP, X-Frame-Options, HSTS-ready, etc.)
- Disable Swagger UI/ReDoc/OpenAPI in production
- Restrict /health endpoint to internal networks via Nginx ACL

Also: rewrite install.sh as interactive wizard for guided deployment,
fix test-from-template validation error (technique_id UUID vs MITRE ID)
This commit is contained in:
2026-02-11 08:56:26 +01:00
parent e7e63161e8
commit 64d64080e0
36 changed files with 1154 additions and 311 deletions

View File

@@ -1,32 +1,80 @@
"""
Seed script — creates the initial admin user if it does not already exist.
On first run the admin credentials are generated securely:
- Username is read from ``ADMIN_USERNAME`` env var (default: ``admin``).
- Password is read from ``ADMIN_PASSWORD`` env var. When the variable is
**not set**, a cryptographically random 16-character password is generated
automatically and printed to the startup logs so the operator can copy it.
Usage:
python -m app.seed
"""
import os
import secrets
import string
from app.auth import hash_password
from app.database import SessionLocal
from app.models.user import User
# Characters for auto-generated passwords (alphanumeric + safe symbols)
_PW_ALPHABET = string.ascii_letters + string.digits + "!@#$%&*-_+"
def _generate_password(length: int = 16) -> str:
"""Return a cryptographically random password of *length* characters."""
return "".join(secrets.choice(_PW_ALPHABET) for _ in range(length))
def seed_admin() -> None:
"""Create the default admin user when it is missing."""
"""Create the initial admin user when it is missing.
Reads ``ADMIN_USERNAME`` and ``ADMIN_PASSWORD`` from the environment.
If ``ADMIN_PASSWORD`` is empty or unset a secure random password is
generated and displayed in the logs.
"""
db = SessionLocal()
try:
existing = db.query(User).filter(User.username == "admin").first()
admin_username = os.environ.get("ADMIN_USERNAME", "admin").strip() or "admin"
existing = db.query(User).filter(User.username == admin_username).first()
if existing:
print("Admin user already exists — skipping.")
print(f"Admin user '{admin_username}' already exists — skipping.")
return
admin_password = os.environ.get("ADMIN_PASSWORD", "").strip()
password_was_generated = False
if not admin_password:
admin_password = _generate_password()
password_was_generated = True
admin = User(
username="admin",
hashed_password=hash_password("admin123"),
username=admin_username,
hashed_password=hash_password(admin_password),
role="admin",
)
db.add(admin)
db.commit()
print("Admin user created successfully.")
# ── Display credentials in startup logs ──────────────────────
print()
print("=" * 60)
print(" AEGIS — Initial Admin User Created")
print("=" * 60)
print(f" Username : {admin_username}")
if password_was_generated:
print(f" Password : {admin_password}")
print()
print(" ** This password was auto-generated because")
print(" ADMIN_PASSWORD was not set in the environment. **")
print(" ** Save it now — it will NOT be shown again. **")
else:
print(" Password : (set via ADMIN_PASSWORD env var)")
print("=" * 60)
print()
finally:
db.close()