Files
vulncheck/app/models/notification_log.py
T
vulncheck 9ca0be24f0 feat(scans): a sync that dies at login is now a failed run, on screen and by mail
A Wazuh API that refused every login for a day was visible only in the
container log. Every scan-jobs row is per asset, and a sync that fails at
authentication never reaches an asset — so /scans showed a clean history of
COMPLETED runs, the manual button said "Sync Failed — check the logs", and
nobody was told.

Ledger: every sync execution (Wazuh, Nessus, Intune, vCenter, IGEL; button
or scheduler) is one sync_runs row with start, end, status, stats and the
error that stopped it, written from its own session so the record never
depends on the job's transaction. A run where every agent failed the same
way counts as failed, not as "0 triggered". Rows left running by a restart
are closed at startup.

Visible: Scan Jobs gets a Sync Health strip (per source: state, last run,
last success, error) and a Sync Runs table; the dashboard shows a banner
while a source is failed or stale (stale follows the schedule interval, a
weekly Nessus is not stale after six days); the Sync Failed modal shows the
real reason, and the sync endpoints return it instead of "check server
logs".

Mail: a failed run mails the sync-alert recipients (own setting, else the
notification defaults) with the error and the last successful sync, i.e.
how long the coverage gap already is — one mail per source per 24h while
it keeps failing, logged as SYNC_FAILURE in the notification log.
2026-09-03 18:11:24 +02:00

72 lines
2.8 KiB
Python

"""
Notification Log Model
"""
from enum import Enum
from sqlalchemy import (
Column, Integer, String, DateTime, Text,
ForeignKey, Enum as SQLEnum, Index
)
from sqlalchemy.orm import relationship
from app.models.base import Base
class NotificationType(str, Enum):
SLA_BREACH = "sla_breach"
ASSIGNMENT = "assignment"
NEW_VULNERABILITY = "new_vulnerability"
# Actively exploited (CISA KEV / ENISA EUVD) AND present in our inventory —
# its own type so the log can be filtered for the alerts that meant "now".
KEV_ALERT = "kev_alert"
# A sync job (Wazuh/Nessus/Intune/vCenter/IGEL) failed — coverage gap.
SYNC_FAILURE = "sync_failure"
MANUAL = "manual"
class NotificationStatus(str, Enum):
SENT = "sent"
FAILED = "failed"
SUPPRESSED = "suppressed"
class NotificationLog(Base):
__tablename__ = "notification_logs"
id = Column(Integer, primary_key=True, index=True)
# SET NULL, not CASCADE: the log is the record that a notification WAS
# sent, and that stays true after the finding is gone. Without a rule the
# database refused the delete outright — an Intune device removed upstream
# could not be deleted at all once anything had been mailed about it
# (ForeignKeyViolation on notification_logs_vulnerability_id_fkey).
vulnerability_id = Column(Integer, ForeignKey("vulnerabilities.id", ondelete="SET NULL"),
nullable=True, index=True)
# SET NULL for the same reason as vulnerability_id above: the log records
# that a notification WAS sent, which stays true after the asset is gone.
# Without a rule this blocked asset deletion outright — the vulnerability_id
# fix in migration 040 only moved the failure one constraint along.
asset_id = Column(Integer, ForeignKey("assets.id", ondelete="SET NULL"),
nullable=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
notification_type = Column(SQLEnum(NotificationType), nullable=False, index=True)
sent_at = Column(DateTime, nullable=False, index=True)
subject = Column(String(500), nullable=True)
recipient_email = Column(String(255), nullable=True)
status = Column(SQLEnum(NotificationStatus), nullable=False)
message_body = Column(Text, nullable=True)
error_message = Column(Text, nullable=True)
# Relationships
vulnerability = relationship("Vulnerability", foreign_keys=[vulnerability_id])
asset = relationship("Asset", foreign_keys=[asset_id])
user = relationship("User", foreign_keys=[user_id])
def __repr__(self):
return f"<NotificationLog(type='{self.notification_type}', status='{self.status}')>"
Index(
'idx_notif_vuln_type_sent',
NotificationLog.vulnerability_id,
NotificationLog.notification_type,
NotificationLog.sent_at
)