feat: add notification alert rules

This commit is contained in:
Christian Krakau-Louis
2026-05-22 22:55:49 +02:00
parent 9d0375569b
commit 05b687b525
9 changed files with 703 additions and 10 deletions
@@ -21,6 +21,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import require_admin_auth
from app.models.setting import Setting
from app.services.alert_rules import evaluate_alert_rules, send_current_alerts
from app.services.notifications import send_notification
router = APIRouter()
@@ -127,6 +128,55 @@ SETTING_DEFAULTS: List[Dict[str, Any]] = [
"value_type": "string",
"category": "notifications",
},
{
"key": "notifications.alert_new_sources_enabled",
"value": "true",
"description": "Alert when a new sending source appears in recent DMARC reports",
"value_type": "boolean",
"category": "notifications",
},
{
"key": "notifications.alert_compliance_drop_enabled",
"value": "true",
"description": "Alert when DMARC compliance drops by the configured percentage points",
"value_type": "boolean",
"category": "notifications",
},
{
"key": "notifications.alert_compliance_drop_points",
"value": "10",
"description": "Minimum compliance-rate drop, in percentage points, before alerting",
"value_type": "integer",
"category": "notifications",
},
{
"key": "notifications.alert_failure_threshold_enabled",
"value": "true",
"description": "Alert when DMARC failures exceed the configured daily threshold",
"value_type": "boolean",
"category": "notifications",
},
{
"key": "notifications.alert_failure_threshold_count",
"value": "100",
"description": "Minimum failed message count in the last day before alerting",
"value_type": "integer",
"category": "notifications",
},
{
"key": "notifications.alert_missing_reports_enabled",
"value": "true",
"description": "Alert when a monitored domain has not received recent DMARC reports",
"value_type": "boolean",
"category": "notifications",
},
{
"key": "notifications.alert_missing_reports_days",
"value": "2",
"description": "Number of days without DMARC reports before alerting",
"value_type": "integer",
"category": "notifications",
},
]
# Keys whose values should be redacted in GET responses (treated as secrets)
@@ -216,6 +266,19 @@ class NotificationTestResponse(BaseModel):
error: Optional[str] = None
class AlertRulesResponse(BaseModel):
"""Current alert-rule evaluation response."""
alerts: List[Dict[str, Any]]
class AlertNotificationResponse(BaseModel):
"""Alert-rule evaluation plus notification delivery status."""
alerts: List[Dict[str, Any]]
notification: Dict[str, Any]
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@@ -261,6 +324,33 @@ async def test_notification_settings(
return result.to_dict()
@router.get("/notifications/alerts", response_model=AlertRulesResponse)
async def evaluate_notification_alerts(
db: Session = Depends(get_db),
_auth: dict = Depends(require_admin_auth),
) -> AlertRulesResponse:
"""Evaluate enabled notification alert rules against current DMARC data."""
_seed_defaults(db)
return {"alerts": evaluate_alert_rules(db)}
@router.post("/notifications/alerts/send", response_model=AlertNotificationResponse)
async def send_notification_alerts(
db: Session = Depends(get_db),
_auth: dict = Depends(require_admin_auth),
) -> AlertNotificationResponse:
"""Evaluate current alert rules and send a notification summary when needed."""
_seed_defaults(db)
result = send_current_alerts(db)
notification = result["notification"]
if result["alerts"] and not notification.get("success"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=result,
)
return result
@router.get("/{key:path}", response_model=SettingResponse)
async def get_setting(
key: str,