Files
vulncheck/app/models/notification_log.py
T
vulncheck 5436745d0e fix: give the remaining asset foreign keys a delete rule
Migration 040 fixed the two constraints pointing at vulnerabilities, which was
only half the chain — I checked what referenced findings and not what
referenced assets. So the delete got one step further and failed on the next
constraint instead:

    ForeignKeyViolation: update or delete on table "assets" violates foreign
    key constraint "notification_logs_asset_id_fkey"

This time every foreign key into the delete path was enumerated. Three had no
rule, each gets the one that fits: the notification log keeps its row with the
reference cleared (it records that a mail WAS sent, which stays true), while
scans and findings belong to their asset, are NOT NULL, and go with it. The
asset_groups association is included even though SQLAlchemy clears it itself —
the database should not depend on the application getting the order right.

Verified no foreign key into assets, vulnerabilities or scans is left without
a rule.
2026-08-10 15:37:03 +02:00

67 lines
2.5 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"
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
)