Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/api/handlers/alerts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ pub async fn list_alerts(
) -> AppResult<Json<ApiResponse<PaginatedResponse<Alert>>>> {
let alerts = state
.alert_repo
.list(params.severity.as_deref(), params.state.as_deref())
.list(
params.severity.as_deref(),
params.state.as_deref(),
state.seed_mock_data(),
)
.await;

let page = params.page.unwrap_or(1).max(1);
Expand All @@ -50,7 +54,7 @@ pub async fn list_alerts(
pub async fn get_summary(
State(state): State<AppState>,
) -> AppResult<Json<ApiResponse<AlertSummary>>> {
let summary = state.alert_repo.summary().await;
let summary = state.alert_repo.summary(state.seed_mock_data()).await;
Ok(Json(ApiResponse::ok(summary)))
}

Expand Down
10 changes: 10 additions & 0 deletions src/models/alert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub struct Alert {
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_ticket_id: Option<String>,
#[serde(skip)]
pub mock: bool,
}

/// Summary statistics for the dashboard.
Expand Down Expand Up @@ -100,6 +102,7 @@ pub fn seed_alerts() -> Vec<Alert> {
sla_window: Some("15m".into()),
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000002").unwrap(),
Expand All @@ -121,6 +124,7 @@ pub fn seed_alerts() -> Vec<Alert> {
sla_window: Some("30m".into()),
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000003").unwrap(),
Expand All @@ -140,6 +144,7 @@ pub fn seed_alerts() -> Vec<Alert> {
sla_window: None,
source: "certificate-monitor".into(),
external_ticket_id: None,
mock: true,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000004").unwrap(),
Expand All @@ -163,6 +168,7 @@ pub fn seed_alerts() -> Vec<Alert> {
sla_window: None,
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000005").unwrap(),
Expand All @@ -186,6 +192,7 @@ pub fn seed_alerts() -> Vec<Alert> {
sla_window: Some("15m".into()),
source: "verifier".into(),
external_ticket_id: Some("SEC-2024-0042".into()),
mock: true,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000006").unwrap(),
Expand All @@ -205,6 +212,7 @@ pub fn seed_alerts() -> Vec<Alert> {
sla_window: None,
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
]
}
Expand Down Expand Up @@ -289,10 +297,12 @@ mod tests {
sla_window: None,
source: "test".into(),
external_ticket_id: None,
mock: false,
};
let json = serde_json::to_value(&alert).unwrap();
assert!(json.get("type").is_some());
assert!(json.get("alert_type").is_none());
assert!(json.get("mock").is_none());
assert_eq!(json["type"], "cert_expiry");
}

Expand Down
6 changes: 6 additions & 0 deletions src/models/alert_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ impl AlertStore {
sla_window: Some("15m".into()),
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
// WARNING: failed push-mode agent consecutive failures (Acknowledged)
Alert {
Expand All @@ -61,6 +62,7 @@ impl AlertStore {
sla_window: Some("30m".into()),
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
// WARNING: certificate approaching expiry (New)
Alert {
Expand All @@ -81,6 +83,7 @@ impl AlertStore {
sla_window: None,
source: "certificate-monitor".into(),
external_ticket_id: None,
mock: true,
},
// INFO: PCR change detected on healthy push agent (Resolved)
Alert {
Expand All @@ -105,6 +108,7 @@ impl AlertStore {
sla_window: None,
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
// CRITICAL: policy violation on failed pull agent (Under Investigation)
Alert {
Expand All @@ -130,6 +134,7 @@ impl AlertStore {
sla_window: Some("15m".into()),
source: "verifier".into(),
external_ticket_id: Some("SEC-2024-0042".into()),
mock: true,
},
// INFO: clock skew detected (Dismissed)
Alert {
Expand All @@ -150,6 +155,7 @@ impl AlertStore {
sla_window: None,
source: "verifier".into(),
external_ticket_id: None,
mock: true,
},
];

Expand Down
59 changes: 39 additions & 20 deletions src/repository/alert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ use crate::models::alert::{seed_alerts, Alert, AlertSeverity, AlertState, AlertS

#[async_trait]
pub trait AlertRepository: Send + Sync + 'static {
async fn list(&self, severity: Option<&str>, state: Option<&str>) -> Vec<Alert>;
async fn list(
&self,
severity: Option<&str>,
state: Option<&str>,
include_mock: bool,
) -> Vec<Alert>;
async fn get(&self, id: Uuid) -> Option<Alert>;
async fn summary(&self) -> AlertSummary;
async fn summary(&self, include_mock: bool) -> AlertSummary;
async fn acknowledge(&self, id: Uuid) -> Result<(), String>;
async fn investigate(&self, id: Uuid, assigned_to: Option<String>) -> Result<(), String>;
async fn resolve(&self, id: Uuid, resolution: Option<String>) -> Result<(), String>;
Expand All @@ -33,11 +38,19 @@ impl InMemoryAlertRepository {

#[async_trait]
impl AlertRepository for InMemoryAlertRepository {
async fn list(&self, severity: Option<&str>, state: Option<&str>) -> Vec<Alert> {
async fn list(
&self,
severity: Option<&str>,
state: Option<&str>,
include_mock: bool,
) -> Vec<Alert> {
let alerts = self.alerts.read().unwrap();
alerts
.iter()
.filter(|a| {
if !include_mock && a.mock {
return false;
}
if let Some(sev) = severity {
let a_sev = serde_json::to_string(&a.severity).unwrap_or_default();
let a_sev = a_sev.trim_matches('"');
Expand All @@ -63,21 +76,25 @@ impl AlertRepository for InMemoryAlertRepository {
alerts.iter().find(|a| a.id == id).cloned()
}

async fn summary(&self) -> AlertSummary {
async fn summary(&self, include_mock: bool) -> AlertSummary {
let alerts = self.alerts.read().unwrap();
let visible = |a: &&Alert| include_mock || !a.mock;

let critical = alerts
.iter()
.filter(visible)
.filter(|a| a.severity == AlertSeverity::Critical)
.count() as u64;

let warnings = alerts
.iter()
.filter(visible)
.filter(|a| a.severity == AlertSeverity::Warning)
.count() as u64;

let info = alerts
.iter()
.filter(visible)
.filter(|a| a.severity == AlertSeverity::Info)
.count() as u64;

Expand All @@ -86,11 +103,13 @@ impl AlertRepository for InMemoryAlertRepository {

let active_critical = alerts
.iter()
.filter(visible)
.filter(|a| a.severity == AlertSeverity::Critical && is_active(a))
.count() as u64;

let active_warnings = alerts
.iter()
.filter(visible)
.filter(|a| a.severity == AlertSeverity::Warning && is_active(a))
.count() as u64;

Expand Down Expand Up @@ -213,30 +232,30 @@ mod tests {
#[tokio::test]
async fn seed_data_has_expected_alerts() {
let repo = make_repo();
let all = repo.list(None, None).await;
let all = repo.list(None, None, true).await;
assert_eq!(all.len(), 6);
}

#[tokio::test]
async fn filter_by_severity() {
let repo = make_repo();
let critical = repo.list(Some("critical"), None).await;
let critical = repo.list(Some("critical"), None, true).await;
assert_eq!(critical.len(), 2);
let info = repo.list(Some("info"), None).await;
let info = repo.list(Some("info"), None, true).await;
assert_eq!(info.len(), 2);
}

#[tokio::test]
async fn filter_by_state() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
assert_eq!(new_alerts.len(), 2);
}

#[tokio::test]
async fn acknowledge_transitions_new_to_acknowledged() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;

repo.acknowledge(id).await.unwrap();
Expand All @@ -249,7 +268,7 @@ mod tests {
#[tokio::test]
async fn acknowledge_rejects_non_new_state() {
let repo = make_repo();
let acked = repo.list(None, Some("acknowledged")).await;
let acked = repo.list(None, Some("acknowledged"), true).await;
let id = acked[0].id;

let result = repo.acknowledge(id).await;
Expand All @@ -259,7 +278,7 @@ mod tests {
#[tokio::test]
async fn investigate_sets_assignee() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;

repo.investigate(id, Some("analyst@example.com".into()))
Expand All @@ -274,7 +293,7 @@ mod tests {
#[tokio::test]
async fn resolve_sets_resolution_reason() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;

repo.resolve(id, Some("fixed the issue".into()))
Expand All @@ -289,7 +308,7 @@ mod tests {
#[tokio::test]
async fn dismiss_transitions_to_dismissed() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;

repo.dismiss(id).await.unwrap();
Expand All @@ -301,7 +320,7 @@ mod tests {
#[tokio::test]
async fn escalate_increments_count() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;
let before = repo.get(id).await.unwrap().escalation_count;

Expand All @@ -314,7 +333,7 @@ mod tests {
#[tokio::test]
async fn cannot_resolve_already_resolved() {
let repo = make_repo();
let resolved = repo.list(None, Some("resolved")).await;
let resolved = repo.list(None, Some("resolved"), true).await;
let id = resolved[0].id;

let result = repo.resolve(id, None).await;
Expand All @@ -324,7 +343,7 @@ mod tests {
#[tokio::test]
async fn summary_counts_active_alerts() {
let repo = make_repo();
let summary = repo.summary().await;
let summary = repo.summary(true).await;
assert_eq!(summary.critical, 2);
assert_eq!(summary.warnings, 2);
assert_eq!(summary.info, 2);
Expand All @@ -336,7 +355,7 @@ mod tests {
#[tokio::test]
async fn concurrent_escalations_are_serialized() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;
let before = repo.get(id).await.unwrap().escalation_count;

Expand All @@ -359,7 +378,7 @@ mod tests {
#[tokio::test]
async fn concurrent_reads_during_writes() {
let repo = make_repo();
let new_alerts = repo.list(None, Some("new")).await;
let new_alerts = repo.list(None, Some("new"), true).await;
let id = new_alerts[0].id;

let mut handles = Vec::new();
Expand All @@ -368,9 +387,9 @@ mod tests {
for _ in 0..20 {
let repo = repo.clone();
handles.push(tokio::spawn(async move {
let _ = repo.list(None, None).await;
let _ = repo.list(None, None, true).await;
let _ = repo.get(id).await;
let _ = repo.summary().await;
let _ = repo.summary(true).await;
}));
}

Expand Down
Loading