Skip to content

Commit 1abd214

Browse files
committed
Reject reclassifying closed alerts and constrain enums
api_classify() now returns 409 when an alert is no longer 'open' so a resolved alert can't be silently overwritten. Added CHECK constraints on alerts.severity and alerts.status matching the documented enums. Tests: 409-on-closed case plus pure compute_sla() unit tests for the MEDIUM SLA path and unknown-severity skip (no fixture row, so the existing 2/4=50% SLA assertions stay intact). Built with Claude Code and the Claude API (LLM).
1 parent 62a7b9d commit 1abd214

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ def api_classify(alert_id):
209209
alert = cur.fetchone()
210210
if alert is None:
211211
abort(404, description="alert not found")
212+
if alert["status"] != "open":
213+
abort(409, description="alert is already closed")
212214

213215
# Update the alert's status and assignee.
214216
cur.execute(

schema.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ CREATE TABLE alerts (
1212
id SERIAL PRIMARY KEY,
1313
title TEXT NOT NULL,
1414
category TEXT NOT NULL,
15-
severity TEXT NOT NULL,
15+
severity TEXT NOT NULL
16+
CHECK (severity IN ('CRITICAL', 'HIGH', 'MEDIUM', 'LOW')),
1617
source TEXT,
1718
source_ip TEXT,
1819
description TEXT,
1920
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
20-
status TEXT NOT NULL DEFAULT 'open',
21+
status TEXT NOT NULL DEFAULT 'open'
22+
CHECK (status IN ('open', 'true_positive', 'false_positive', 'escalated')),
2123
assigned_to TEXT
2224
);
2325

tests/test_app.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Integration tests for the SOC dashboard API (Flask test client + Postgres)."""
22
import json
3+
from datetime import datetime, timedelta, timezone
4+
5+
from app import compute_sla
36

47

58
def test_pages_render(client):
@@ -101,6 +104,34 @@ def test_classify_rejects_unknown_action(client):
101104
assert resp.status_code == 400
102105

103106

107+
def test_classify_already_closed_returns_409(client):
108+
# alert 1 is already true_positive in the fixtures; reclassifying is rejected.
109+
resp = client.post(
110+
"/api/alerts/1/classify",
111+
data=json.dumps({"analyst": "carol", "action": "classify_tp"}),
112+
content_type="application/json",
113+
)
114+
assert resp.status_code == 409
115+
116+
117+
def test_compute_sla_counts_medium_severity_breach():
118+
# MEDIUM target is 4h; a still-open alert aged 5h breaches SLA.
119+
old = datetime.now(timezone.utc) - timedelta(hours=5)
120+
sla = compute_sla([{"severity": "MEDIUM", "resp": None, "created_at": old}])
121+
assert sla["considered"] == 1
122+
assert sla["breaches"] == 1
123+
assert sla["by_severity"]["MEDIUM"] == 1
124+
125+
126+
def test_compute_sla_skips_unknown_severity():
127+
# An unrecognized severity is silently excluded from SLA accounting.
128+
now = datetime.now(timezone.utc)
129+
sla = compute_sla([{"severity": "BOGUS", "resp": 10, "created_at": now}])
130+
assert sla["considered"] == 0
131+
assert sla["breaches"] == 0
132+
assert sla["breach_rate"] == 0.0
133+
134+
104135
def test_classify_unknown_alert_404(client):
105136
resp = client.post(
106137
"/api/alerts/9999/classify",

0 commit comments

Comments
 (0)