Skip to main content

heldar_entry/
retention.rs

1//! The access-control app owns the lifecycle of its own data: it prunes old entry events (and deletes their
2//! evidence frames) on the entry-retention TTL. The kernel's retention sweeper handles only
3//! kernel-owned data (segments, detections, outbox, zone events, sessions, audit, events).
4
5use std::sync::Arc;
6use std::time::Duration;
7
8use chrono::Utc;
9use heldar_kernel::config::Config;
10use sqlx::SqlitePool;
11
12use crate::config::EntryConfig;
13
14pub async fn run(pool: SqlitePool, cfg: Arc<Config>, ecfg: Arc<EntryConfig>) {
15    let mut tick = tokio::time::interval(Duration::from_secs(cfg.retention_interval_s.max(30)));
16    loop {
17        tick.tick().await;
18        if let Err(e) = sweep(&pool, &cfg, &ecfg).await {
19            tracing::error!(error = %e, "entry retention: sweep failed");
20        }
21    }
22}
23
24async fn sweep(pool: &SqlitePool, cfg: &Config, ecfg: &EntryConfig) -> anyhow::Result<()> {
25    let cutoff = Utc::now() - chrono::Duration::days(ecfg.entry_retention_days.max(1));
26    let old: Vec<(String, sqlx::types::Json<serde_json::Value>)> =
27        sqlx::query_as("SELECT id, evidence FROM entry_events WHERE created_at < ?")
28            .bind(cutoff)
29            .fetch_all(pool)
30            .await?;
31    if old.is_empty() {
32        return Ok(());
33    }
34    for (_id, evidence) in &old {
35        if let Some(name) = evidence
36            .0
37            .get("snapshot_path")
38            .and_then(|v| v.as_str())
39            .and_then(|u| u.rsplit('/').next())
40        {
41            let _ = tokio::fs::remove_file(cfg.snapshots_dir.join(name)).await;
42        }
43    }
44    let n = sqlx::query("DELETE FROM entry_events WHERE created_at < ?")
45        .bind(cutoff)
46        .execute(pool)
47        .await?
48        .rows_affected();
49    tracing::info!(
50        deleted = n,
51        "entry retention: pruned old entry events + evidence"
52    );
53    Ok(())
54}