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
52 changes: 52 additions & 0 deletions src/api/handlers/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use crate::state::AppState;
pub struct KeylimeSettings {
pub verifier_url: String,
pub registrar_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed_mock_data: Option<bool>,
}

/// GET /api/settings/keylime -- return current Registrar/Verifier URLs.
Expand All @@ -25,6 +27,11 @@ pub async fn get_keylime(
let settings = KeylimeSettings {
verifier_url: kl.verifier_url().to_string(),
registrar_url: kl.registrar_url().to_string(),
seed_mock_data: if state.seed_mock_data() {
Some(true)
} else {
None
},
};
Ok(Json(ApiResponse::ok(settings)))
}
Expand All @@ -50,11 +57,24 @@ pub async fn update_keylime(

let new_client = KeylimeClient::new(config)?;
state.swap_keylime(new_client);

if let Some(seed) = body.seed_mock_data {
state.set_seed_mock_data(seed);
if seed {
state.alert_repo.seed_if_empty().await;
}
}

state.persist_settings();

let settings = KeylimeSettings {
verifier_url: body.verifier_url,
registrar_url: body.registrar_url,
seed_mock_data: if state.seed_mock_data() {
Some(true)
} else {
None
},
};
Ok(Json(ApiResponse::ok(settings)))
}
Expand Down Expand Up @@ -256,11 +276,43 @@ mod tests {
let settings = KeylimeSettings {
verifier_url: "http://v:3000".into(),
registrar_url: "http://r:3001".into(),
seed_mock_data: None,
};
let json = serde_json::to_string(&settings).unwrap();
let deserialized: KeylimeSettings = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.verifier_url, "http://v:3000");
assert_eq!(deserialized.registrar_url, "http://r:3001");
assert_eq!(deserialized.seed_mock_data, None);
}

#[test]
fn keylime_settings_seed_mock_data_roundtrip() {
let settings = KeylimeSettings {
verifier_url: "http://v:3000".into(),
registrar_url: "http://r:3001".into(),
seed_mock_data: Some(true),
};
let json = serde_json::to_string(&settings).unwrap();
let deserialized: KeylimeSettings = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.seed_mock_data, Some(true));
}

#[test]
fn keylime_settings_omits_seed_mock_data_when_none() {
let settings = KeylimeSettings {
verifier_url: "http://v:3000".into(),
registrar_url: "http://r:3001".into(),
seed_mock_data: None,
};
let json = serde_json::to_string(&settings).unwrap();
assert!(!json.contains("seed_mock_data"));
}

#[test]
fn keylime_settings_deserializes_without_seed_field() {
let json = r#"{"verifier_url":"http://v:3000","registrar_url":"http://r:3001"}"#;
let deserialized: KeylimeSettings = serde_json::from_str(json).unwrap();
assert_eq!(deserialized.seed_mock_data, None);
}

#[test]
Expand Down
12 changes: 11 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ async fn main() -> anyhow::Result<()> {
.or_else(|| std::env::var("KEYLIME_REGISTRAR_URL").ok())
.unwrap_or_else(|| "http://localhost:3001".to_string());

let seed_mock_data = persisted
.as_ref()
.and_then(|s| s.seed_mock_data)
.unwrap_or(false);

let mtls = persisted.and_then(|s| s.mtls);

let observation_interval_secs: u64 = std::env::var("OBSERVATION_INTERVAL_SECS")
Expand All @@ -66,7 +71,11 @@ async fn main() -> anyhow::Result<()> {
let db = SqliteDb::connect(&url).await?;
db.init_schema().await?;
tracing::info!("SQLite database connected: {url}");
db.repositories()
let repos = db.repositories();
if seed_mock_data {
repos.alert.seed_if_empty().await;
}
repos
}
Ok(url) => {
tracing::warn!("Unsupported DATABASE_URL scheme: {url} — using in-memory repos");
Expand Down Expand Up @@ -100,6 +109,7 @@ async fn main() -> anyhow::Result<()> {
repos.audit,
cache,
config_path,
seed_mock_data,
);

let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(());
Expand Down
135 changes: 134 additions & 1 deletion src/models/alert.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use chrono::{DateTime, Utc};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

Expand Down Expand Up @@ -76,6 +76,139 @@ pub struct AlertSummary {
pub active_warnings: u64,
}

pub fn seed_alerts() -> Vec<Alert> {
let now = Utc::now();

vec![
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000001").unwrap(),
alert_type: AlertType::AttestationFailure,
severity: AlertSeverity::Critical,
description: "Agent attestation failed: quote verification returned INVALID — \
PCR values do not match expected policy"
.into(),
affected_agents: vec!["a1b2c3d4-0000-1111-2222-333344445555".into()],
state: AlertState::New,
created_timestamp: now - Duration::minutes(45),
acknowledged_timestamp: None,
assigned_to: None,
investigation_notes: None,
root_cause: None,
resolution: None,
auto_resolved: false,
escalation_count: 0,
sla_window: Some("15m".into()),
source: "verifier".into(),
external_ticket_id: None,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000002").unwrap(),
alert_type: AlertType::AttestationFailure,
severity: AlertSeverity::Warning,
description: "Push-mode agent has 3 consecutive attestation failures — \
evidence submission timeout"
.into(),
affected_agents: vec!["b2c3d4e5-a1b0-8765-4321-fedcba987654".into()],
state: AlertState::Acknowledged,
created_timestamp: now - Duration::hours(2),
acknowledged_timestamp: Some(now - Duration::hours(1)),
assigned_to: Some("operator@example.com".into()),
investigation_notes: None,
root_cause: None,
resolution: None,
auto_resolved: false,
escalation_count: 0,
sla_window: Some("30m".into()),
source: "verifier".into(),
external_ticket_id: None,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000003").unwrap(),
alert_type: AlertType::CertExpiry,
severity: AlertSeverity::Warning,
description: "EK certificate expires in 28 days — renewal recommended".into(),
affected_agents: vec!["d432fbb3-d2f1-4a97-9ef7-75bd81c00000".into()],
state: AlertState::New,
created_timestamp: now - Duration::hours(6),
acknowledged_timestamp: None,
assigned_to: None,
investigation_notes: None,
root_cause: None,
resolution: None,
auto_resolved: false,
escalation_count: 0,
sla_window: None,
source: "certificate-monitor".into(),
external_ticket_id: None,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000004").unwrap(),
alert_type: AlertType::PcrChange,
severity: AlertSeverity::Info,
description: "PCR-14 value changed after kernel update — \
verified as legitimate change"
.into(),
affected_agents: vec!["f7e6d5c4-b3a2-9180-7654-321098765432".into()],
state: AlertState::Resolved,
created_timestamp: now - Duration::hours(26),
acknowledged_timestamp: Some(now - Duration::hours(25)),
assigned_to: Some("admin@example.com".into()),
investigation_notes: Some(
"Kernel updated from 6.1.0 to 6.1.5 — PCR change expected".into(),
),
root_cause: Some("Planned kernel update".into()),
resolution: Some("Policy updated to reflect new kernel measurements".into()),
auto_resolved: false,
escalation_count: 0,
sla_window: None,
source: "verifier".into(),
external_ticket_id: None,
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000005").unwrap(),
alert_type: AlertType::PolicyViolation,
severity: AlertSeverity::Critical,
description: "IMA policy violation: unauthorized binary /usr/local/bin/suspect \
executed on agent"
.into(),
affected_agents: vec!["a1b2c3d4-0000-1111-2222-333344445555".into()],
state: AlertState::UnderInvestigation,
created_timestamp: now - Duration::hours(1),
acknowledged_timestamp: Some(now - Duration::minutes(50)),
assigned_to: Some("security-team@example.com".into()),
investigation_notes: Some(
"Binary hash does not match any known package. Escalated to security team.".into(),
),
root_cause: None,
resolution: None,
auto_resolved: false,
escalation_count: 1,
sla_window: Some("15m".into()),
source: "verifier".into(),
external_ticket_id: Some("SEC-2024-0042".into()),
},
Alert {
id: Uuid::parse_str("a0000001-0000-4000-8000-000000000006").unwrap(),
alert_type: AlertType::ClockSkew,
severity: AlertSeverity::Info,
description: "Clock skew of 2.3s detected between agent and verifier".into(),
affected_agents: vec!["c5d6e7f8-a9b0-4321-8765-abcdef012345".into()],
state: AlertState::Dismissed,
created_timestamp: now - Duration::hours(48),
acknowledged_timestamp: Some(now - Duration::hours(47)),
assigned_to: None,
investigation_notes: None,
root_cause: None,
resolution: Some("NTP sync corrected the drift — false positive".into()),
auto_resolved: false,
escalation_count: 0,
sla_window: None,
source: "verifier".into(),
external_ticket_id: None,
},
]
}

/// Notification for external channels (FR-010).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Notification {
Expand Down
Loading