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.
72 lines
2.8 KiB
Python
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
|
|
)
|