use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use crate::entry::AuditEntry;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum RetentionPolicy {
KeepCount(usize),
KeepDuration(Duration),
KeepAfter(DateTime<Utc>),
}
impl Serialize for RetentionPolicy {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("RetentionPolicy", 2)?;
match self {
RetentionPolicy::KeepCount(n) => {
state.serialize_field("type", "KeepCount")?;
state.serialize_field("value", n)?;
}
RetentionPolicy::KeepDuration(d) => {
state.serialize_field("type", "KeepDuration")?;
state.serialize_field("value", &d.num_seconds())?;
}
RetentionPolicy::KeepAfter(dt) => {
state.serialize_field("type", "KeepAfter")?;
state.serialize_field("value", &dt.to_rfc3339())?;
}
}
state.end()
}
}
impl<'de> Deserialize<'de> for RetentionPolicy {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Tagged {
r#type: String,
value: serde_json::Value,
}
let tagged = Tagged::deserialize(deserializer)?;
match tagged.r#type.as_str() {
"KeepCount" => {
let n = tagged
.value
.as_u64()
.ok_or_else(|| serde::de::Error::custom("KeepCount value must be a number"))?;
Ok(RetentionPolicy::KeepCount(n as usize))
}
"KeepDuration" => {
let secs = tagged.value.as_i64().ok_or_else(|| {
serde::de::Error::custom("KeepDuration value must be seconds")
})?;
Ok(RetentionPolicy::KeepDuration(Duration::seconds(secs)))
}
"KeepAfter" => {
let s = tagged.value.as_str().ok_or_else(|| {
serde::de::Error::custom("KeepAfter value must be an RFC3339 timestamp")
})?;
let dt = chrono::DateTime::parse_from_rfc3339(s)
.map_err(serde::de::Error::custom)?
.with_timezone(&Utc);
Ok(RetentionPolicy::KeepAfter(dt))
}
other => Err(serde::de::Error::custom(format!(
"unknown RetentionPolicy type: {other}"
))),
}
}
}
impl RetentionPolicy {
#[must_use]
pub fn pci_dss() -> Self {
RetentionPolicy::KeepDuration(Duration::days(365))
}
#[must_use]
pub fn hipaa() -> Self {
RetentionPolicy::KeepDuration(Duration::days(6 * 365))
}
#[must_use]
pub fn sox() -> Self {
RetentionPolicy::KeepDuration(Duration::days(7 * 365))
}
#[must_use]
pub fn gdpr(purpose_duration: Duration) -> Self {
RetentionPolicy::KeepDuration(purpose_duration)
}
pub(crate) fn split_index(&self, entries: &[AuditEntry]) -> usize {
match self {
RetentionPolicy::KeepCount(n) => entries.len().saturating_sub(*n),
RetentionPolicy::KeepDuration(duration) => {
let cutoff = Utc::now() - *duration;
Self::first_after(entries, cutoff)
}
RetentionPolicy::KeepAfter(cutoff) => Self::first_after(entries, *cutoff),
}
}
fn first_after(entries: &[AuditEntry], cutoff: DateTime<Utc>) -> usize {
entries
.iter()
.position(|e| e.timestamp() > cutoff)
.unwrap_or(entries.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chain::AuditChain;
use crate::entry::EventSeverity;
fn build_chain(n: usize) -> AuditChain {
let mut chain = AuditChain::new();
for i in 0..n {
chain.append(
EventSeverity::Info,
"src",
format!("action-{i}"),
serde_json::json!({}),
);
}
chain
}
#[test]
fn keep_count_retains_last_n() {
let mut chain = build_chain(10);
let archive = chain.apply_retention(&RetentionPolicy::KeepCount(3));
let archive = archive.unwrap();
assert_eq!(archive.entries.len(), 7);
assert_eq!(chain.len(), 3);
assert!(chain.verify().is_ok());
assert_eq!(chain.entries()[0].prev_hash(), archive.head_hash);
}
#[test]
fn keep_count_larger_than_chain() {
let mut chain = build_chain(5);
let archive = chain.apply_retention(&RetentionPolicy::KeepCount(10));
assert!(archive.is_none());
assert_eq!(chain.len(), 5);
}
#[test]
fn keep_count_zero_archives_all() {
let mut chain = build_chain(5);
let archive = chain.apply_retention(&RetentionPolicy::KeepCount(0));
let archive = archive.unwrap();
assert_eq!(archive.entries.len(), 5);
assert!(chain.is_empty());
}
#[test]
fn keep_after_timestamp() {
let mut chain = build_chain(5);
let cutoff = chain.entries()[2].timestamp();
let archive = chain.apply_retention(&RetentionPolicy::KeepAfter(cutoff));
let archive = archive.unwrap();
assert!(!archive.entries.is_empty());
assert!(chain.verify().is_ok());
for e in chain.entries() {
assert!(e.timestamp() > cutoff);
}
}
#[test]
fn keep_duration_recent() {
let mut chain = build_chain(5);
let archive = chain.apply_retention(&RetentionPolicy::KeepDuration(Duration::hours(1)));
assert!(archive.is_none());
assert_eq!(chain.len(), 5);
}
#[test]
fn retention_on_empty_chain() {
let mut chain = AuditChain::new();
let archive = chain.apply_retention(&RetentionPolicy::KeepCount(5));
assert!(archive.is_none());
}
#[test]
fn retention_preserves_chain_continuity() {
let mut chain = build_chain(10);
let archive = chain
.apply_retention(&RetentionPolicy::KeepCount(5))
.unwrap();
chain.append(EventSeverity::Info, "src", "new", serde_json::json!({}));
assert!(chain.verify().is_ok());
let archived_chain = AuditChain::from_entries(archive.entries);
assert!(archived_chain.verify().is_ok());
}
}