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.
67 lines
2.5 KiB
Python
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
|
|
)
|