Files
vulncheck/app/db_init.py
T
vulncheck fa5351a703 fix(kev): the alert mail was still a two-catalog feature
VulnCheck KEV shipped as a third exploited-catalog source in f77c741 and the
dashboard reads it, but the immediate-alert mail never did: db_init seeded
kev_alert_sources as "cisa,euvd" on the reasoning that a ~4x larger catalog
should be an opt-in, and there is no UI to opt in with. An exploited CVE that
only VulnCheck listed sat open on an active asset and produced no mail.

Catalog size is not what gates a mail — an OPEN finding on an ACTIVE asset is.
Whichever catalog names it, an exploited CVE on our own machine is worth the
alert, and VulnCheck usually lists days before CISA (lead_days is computed
from exactly that gap), which is the window an immediate alert exists for.

Seed is now all three. Migration 047 rewrites the value on installs still
carrying the untouched old default; anything an operator changed by hand is
left alone — a default correction, not a policy override.

The plumbing was already there: get_kev_catalog / _normalize_sources /
the {{sources}} label line all handle vulncheck. Only the seed was in the way.
Two stale docstrings naming just CISA and ENISA fixed with it.

First run after upgrade sends a backlog, not a storm: MAX_ALERTS_PER_MAIL
caps it at 25 and the job is hourly.
2026-08-29 18:03:15 +02:00

118 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import logging
from sqlalchemy.orm import Session
from app.models.user import User, UserRole
from app.models.setting import Setting
from app.auth.jwt_handler import hash_password, get_password_strength
logger = logging.getLogger(__name__)
def _ensure_default_setting(db: Session, key: str, default_value: str, description: str) -> None:
"""Create a setting row only if it does not already exist."""
existing = db.query(Setting).filter(Setting.key == key).first()
if existing:
return
db.add(Setting(key=key, value=default_value, description=description))
def create_initial_data(db: Session):
"""
Creates initial data like the default admin user if it doesn't exist.
Also seeds default enrichment toggles.
"""
try:
# Seed enrichment defaults (no-op if already present)
try:
_ensure_default_setting(
db,
"enrichment_epss_enabled",
"true",
"Enable EPSS (FIRST.org) score enrichment"
)
_ensure_default_setting(
db,
"enrichment_kev_enabled",
"true",
"Enable CISA KEV (Known Exploited Vulnerabilities) enrichment"
)
_ensure_default_setting(
db,
"enrichment_euvd_enabled",
"true",
"Enable ENISA EUVD enrichment (EU exploited/critical vulnerabilities)"
)
_ensure_default_setting(
db,
"kev_alert_enabled",
"true",
"Email immediately when an actively exploited CVE (CISA KEV / "
"ENISA EUVD / VulnCheck KEV) has open findings on an active asset"
)
_ensure_default_setting(
db,
"kev_alert_sources",
"cisa,euvd,vulncheck",
"KEV sources used for immediate alerting (cisa, euvd, vulncheck). "
"All three by default: catalog size is not what decides a mail — "
"an OPEN finding on an ACTIVE asset is, and an exploited CVE "
"sitting on our own machine is worth the mail whichever catalog "
"listed it. VulnCheck usually lists days ahead of CISA, which is "
"the whole point of an immediate alert. Set to a shorter list to "
"narrow it."
)
db.commit()
except Exception as e:
logger.warning(f"Could not seed enrichment settings: {e}")
db.rollback()
# Check if any admin exists
admin_exists = db.query(User).filter(User.role == UserRole.ADMIN).count() > 0
if admin_exists:
logger.info("️ Admin user already exists. Skipping default admin creation.")
return
# Default admin (simple onboarding)
if os.getenv("DISABLE_DEFAULT_ADMIN", "false").lower() == "true":
logger.warning(
"⚠️ No admin user exists and default admin is disabled. "
"Set DEFAULT_ADMIN_* to initialize."
)
return
default_username = os.getenv("DEFAULT_ADMIN_USERNAME", "admin")
default_email = os.getenv("DEFAULT_ADMIN_EMAIL", "admin@vulnmanager.local")
default_password = os.getenv("DEFAULT_ADMIN_PASSWORD")
if not default_password:
logger.error(
"❌ No admin user exists and DEFAULT_ADMIN_PASSWORD is not set. "
"Refusing to create admin with insecure default. "
"Set DEFAULT_ADMIN_PASSWORD env var or use POST /auth/setup-admin "
"with SETUP_ADMIN_TOKEN to bootstrap."
)
return
password_check = get_password_strength(default_password)
if not password_check["is_valid"]:
logger.error(
"❌ DEFAULT_ADMIN_PASSWORD does not meet strength requirements: %s. "
"Refusing to create admin user.",
", ".join(password_check["feedback"])
)
return
admin_user = User(
username=default_username,
email=default_email,
password_hash=hash_password(default_password),
role=UserRole.ADMIN,
is_active=True,
is_verified=True,
)
db.add(admin_user)
db.commit()
logger.info("✅ Default admin user created: %s", default_username)
except Exception as e:
logger.error(f"❌ Error creating initial data: {e}")
db.rollback()