"""Application configuration for the Aegis MITRE ATT&CK Coverage Platform. Loads settings from environment variables and ``.env`` files via ``pydantic-settings``. Validates critical secrets at import time and raises ``RuntimeError`` (production) or issues a ``UserWarning`` (development) when unsafe defaults are detected. """ # Import os import os # Import secrets import secrets # Import warnings import warnings # Import BaseSettings from pydantic_settings from pydantic_settings import BaseSettings # --------------------------------------------------------------------------- # Detect environment: "production" when AEGIS_ENV or common indicators are set # --------------------------------------------------------------------------- _is_production = os.environ.get("AEGIS_ENV", "").lower() == "production" # Define class Settings class Settings(BaseSettings): """Application settings loaded from environment variables and .env file.""" # Assign DATABASE_URL = "postgresql://postgres:postgres@postgres:5432/attackdb" DATABASE_URL: str = "postgresql://postgres:postgres@postgres:5432/attackdb" # ── Security ────────────────────────────────────────────────────── # SECRET_KEY has NO safe default. In development a random key is # generated at startup (tokens invalidate on restart — acceptable # for local dev). In production it MUST be supplied via env/.env # so tokens survive restarts. SECRET_KEY: str = "" # Assign ALGORITHM = "HS256" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 # 8 hours — /auth/refresh extends active sessions # ── Redis ───────────────────────────────────────────────────────── REDIS_URL: str = "redis://redis:6379/0" # Logical DB indices on the same Redis instance (PATH in URL is overridden). REDIS_TOKEN_BLACKLIST_DB: int = 1 # Assign REDIS_CACHE_DB = 2 REDIS_CACHE_DB: int = 2 # ── CORS ───────────────────────────────────────────────────────── # Comma-separated list of allowed origins, or a JSON array. # In dev this defaults to common local ports; in production set it # to the actual frontend domain(s). CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173" # ── MinIO / S3 ─────────────────────────────────────────────────── MINIO_ENDPOINT: str = "minio:9000" # Public hostname used in presigned URLs returned to browsers. # In production set this to :9000 (or a public FQDN) so # the browser can reach MinIO directly. Defaults to MINIO_ENDPOINT. MINIO_PUBLIC_ENDPOINT: str = "" MINIO_ACCESS_KEY: str = "minioadmin" # Assign MINIO_SECRET_KEY = "minioadmin" MINIO_SECRET_KEY: str = "minioadmin" # Assign MINIO_BUCKET = "evidence" MINIO_BUCKET: str = "evidence" # Assign MINIO_SECURE = False # True → use HTTPS to connect to MinIO MINIO_SECURE: bool = False # True → use HTTPS to connect to MinIO # ── Re-testing ─────────────────────────────────────────────────── MAX_RETEST_COUNT: int = 3 # maximum automatic retests per original test # ── Jira Integration ──────────────────────────────────────────── JIRA_ENABLED: bool = False # Assign JIRA_URL = "" JIRA_URL: str = "" # Assign JIRA_USERNAME = "" JIRA_USERNAME: str = "" # Assign JIRA_API_TOKEN = "" JIRA_API_TOKEN: str = "" # Assign JIRA_IS_CLOUD = True JIRA_IS_CLOUD: bool = True # Assign JIRA_DEFAULT_PROJECT = "" JIRA_DEFAULT_PROJECT: str = "" JIRA_ISSUE_TYPE_TEST: str = "Task" # tests (campaign or standalone) JIRA_ISSUE_TYPE_CAMPAIGN: str = "Epic" # campaigns (under Initiative) # Jira custom field ID for "Start date" — Jira Cloud team-managed: customfield_10015 # Override with the correct field ID for your Jira instance if different. JIRA_START_DATE_FIELD: str = "customfield_10015" # ── Tempo Integration ───────────────────────────────────────────── TEMPO_ENABLED: bool = False # Assign TEMPO_API_TOKEN = "" TEMPO_API_TOKEN: str = "" # Assign TEMPO_API_VERSION = 4 TEMPO_API_VERSION: int = 4 # Assign TEMPO_DEFAULT_WORK_TYPE = "Red Team" TEMPO_DEFAULT_WORK_TYPE: str = "Red Team" # Tempo API base URL — use https://api.eu.tempo.io/4 for EU workspaces. # Can also be set via system_configs key "tempo.base_url" at runtime. TEMPO_BASE_URL: str = "" # empty → falls back to https://api.tempo.io/4 # ── OSINT / Intelligence ──────────────────────────────────────── NVD_API_KEY: str = "" # optional; increases NVD rate limit from 5/30s to 50/30s # Assign STALE_THRESHOLD_DAYS = 365 # days before coverage is considered stale STALE_THRESHOLD_DAYS: int = 365 # days before coverage is considered stale # ── Reporting ───────────────────────────────────────────────────── REPORT_TEMPLATES_DIR: str = "app/templates/reports" # Assign REPORT_OUTPUT_DIR = "/tmp/aegis_reports" REPORT_OUTPUT_DIR: str = "/tmp/aegis_reports" # Assign COMPANY_NAME = "Organization" COMPANY_NAME: str = "Organization" # Assign COMPANY_LOGO_PATH = "app/templates/reports/assets/logo.png" COMPANY_LOGO_PATH: str = "app/templates/reports/assets/logo.png" # ── Email / SMTP ────────────────────────────────────────────────── SMTP_ENABLED: bool = False SMTP_HOST: str = "" SMTP_PORT: int = 587 SMTP_USERNAME: str = "" SMTP_PASSWORD: str = "" SMTP_FROM_EMAIL: str = "aegis@company.com" SMTP_USE_TLS: bool = True PLATFORM_URL: str = "http://localhost:5173" # base URL for links in emails # ── Scoring weights (must sum to 100) ──────────────────────────── SCORING_WEIGHT_TESTS: int = 40 # Assign SCORING_WEIGHT_DETECTION_RULES = 25 SCORING_WEIGHT_DETECTION_RULES: int = 25 # Assign SCORING_WEIGHT_D3FEND = 15 SCORING_WEIGHT_D3FEND: int = 15 # Assign SCORING_WEIGHT_RECENCY = 10 SCORING_WEIGHT_RECENCY: int = 10 # Assign SCORING_WEIGHT_SEVERITY = 10 SCORING_WEIGHT_SEVERITY: int = 10 # Legacy env names (mapped in scoring_config_service) SCORING_WEIGHT_FRESHNESS: int = 10 # Assign SCORING_WEIGHT_PLATFORM_DIVERSITY = 10 SCORING_WEIGHT_PLATFORM_DIVERSITY: int = 10 # Define class Config class Config: """Pydantic BaseSettings configuration — load from .env file.""" # Assign env_file = ".env" env_file = ".env" # Assign settings = Settings() settings = Settings() # --------------------------------------------------------------------------- # Post-init validation for SECRET_KEY # --------------------------------------------------------------------------- _UNSAFE_SECRETS = { # Literal argument value "", # Literal argument value "change-me-in-production", # Literal argument value "change-me-in-production-use-a-long-random-string", } # Check: settings.SECRET_KEY in _UNSAFE_SECRETS if settings.SECRET_KEY in _UNSAFE_SECRETS: # Check: _is_production if _is_production: # Raise RuntimeError raise RuntimeError( # Literal argument value "CRITICAL: SECRET_KEY is not configured. " # Literal argument value "Set a strong random value (>= 32 chars) via the SECRET_KEY " # Literal argument value "environment variable or in your .env file before running in " # Literal argument value "production. Example: openssl rand -hex 32" ) # Development: auto-generate an ephemeral key and warn settings.SECRET_KEY = secrets.token_hex(32) # Call warnings.warn() warnings.warn( # Literal argument value "SECRET_KEY was not set — using an auto-generated ephemeral key. " # Literal argument value "JWT tokens will be invalidated on every restart. " # Literal argument value "Set SECRET_KEY in your environment for persistent sessions.", # Keyword argument: stacklevel stacklevel=2, ) # --------------------------------------------------------------------------- # SEC-002: Reject default credentials in production # --------------------------------------------------------------------------- if _is_production: # Assign _DEFAULT_CREDS = { _DEFAULT_CREDS = { ("MINIO_ACCESS_KEY", settings.MINIO_ACCESS_KEY, "minioadmin"), ("MINIO_SECRET_KEY", settings.MINIO_SECRET_KEY, "minioadmin"), } # Iterate over _DEFAULT_CREDS for name, current, default in _DEFAULT_CREDS: # Check: current == default if current == default: # Raise RuntimeError raise RuntimeError( f"CRITICAL: {name} is using the default value '{default}'. " f"Set a strong value via the {name} environment variable " f"before running in production." )