bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
use super::*;
use crate::ops_health_diagnostics::{scoped_audit_events, scoped_notification_events};

impl BucketWarden {
    pub fn event_category_report(
        &mut self,
        principal: &str,
        bucket: Option<&str>,
    ) -> Result<EventCategoryReport, RuntimeError> {
        let (scope, target) = event_scope_and_target(bucket);
        self.require_operator_action(
            principal,
            OperatorAction::ReadAudit,
            &target,
            "ops:GetEventCategoryReport",
        )?;
        self.require_ops_bucket_scope(bucket)?;
        let notification_events = scoped_notification_events(self, bucket);
        let audit_events = scoped_audit_events(self, bucket);
        let mut notification_categories = BTreeMap::new();
        for event in &notification_events {
            *notification_categories
                .entry(notification_category(&event.event_name).to_string())
                .or_insert(0) += 1;
        }
        let mut audit_categories = BTreeMap::new();
        let mut audit_outcomes = BTreeMap::new();
        for event in &audit_events {
            *audit_categories
                .entry(audit_category(&event.action).to_string())
                .or_insert(0) += 1;
            *audit_outcomes
                .entry(format!("{:?}", event.outcome))
                .or_insert(0) += 1;
        }
        let report = EventCategoryReport {
            scope: scope.to_string(),
            target: target.clone(),
            generated_at_epoch_seconds: self.clock_epoch_seconds,
            notification_event_count: notification_events.len(),
            audit_event_count: audit_events.len(),
            notification_categories,
            audit_categories,
            audit_outcomes,
        };
        self.audit.append(
            principal,
            "ops:GetEventCategoryReport",
            &target,
            AuditOutcome::Allowed,
            Some(format!(
                "notification_events={},audit_events={}",
                report.notification_event_count, report.audit_event_count
            )),
        );
        Ok(report)
    }

    pub fn event_compliance_report(
        &mut self,
        principal: &str,
        bucket: Option<&str>,
    ) -> Result<EventComplianceReport, RuntimeError> {
        let (scope, target) = event_scope_and_target(bucket);
        self.require_operator_action(
            principal,
            OperatorAction::ReadAudit,
            &target,
            "ops:GetEventComplianceReport",
        )?;
        self.require_ops_bucket_scope(bucket)?;
        let notification_events = scoped_notification_events(self, bucket);
        let audit_events = scoped_audit_events(self, bucket);
        let report = EventComplianceReport {
            scope: scope.to_string(),
            target: target.clone(),
            generated_at_epoch_seconds: self.clock_epoch_seconds,
            notification_event_count: notification_events.len(),
            audit_event_count: audit_events.len(),
            first_notification_event_id: notification_events
                .first()
                .map(|event| event.event_id.clone()),
            last_notification_event_id: notification_events
                .last()
                .map(|event| event.event_id.clone()),
            first_audit_sequence: audit_events.first().map(|event| event.sequence),
            last_audit_sequence: audit_events.last().map(|event| event.sequence),
            notification_event_ids_contiguous: notification_event_ids_contiguous(
                &notification_events,
            ),
            audit_sequences_contiguous: audit_sequences_contiguous(&audit_events),
            notification_json_lines: notification_events_json_lines(&notification_events),
            audit_json_lines: audit_json_lines(&audit_events),
        };
        self.audit.append(
            principal,
            "ops:GetEventComplianceReport",
            &target,
            AuditOutcome::Allowed,
            Some(format!(
                "notification_events={},audit_events={},notification_contiguous={},audit_contiguous={}",
                report.notification_event_count,
                report.audit_event_count,
                report.notification_event_ids_contiguous,
                report.audit_sequences_contiguous
            )),
        );
        Ok(report)
    }
}

fn event_scope_and_target(bucket: Option<&str>) -> (&'static str, String) {
    match bucket {
        Some(bucket) => ("bucket", bucket.to_string()),
        None => ("runtime", "*".to_string()),
    }
}

fn notification_category(event_name: &str) -> &'static str {
    if event_name.starts_with("s3:ObjectCreated:") || event_name.starts_with("s3:ObjectRemoved:") {
        "data"
    } else if event_name.starts_with("s3:ObjectRestore:") {
        "restore"
    } else if event_name.starts_with("s3:Replication:") {
        "replication"
    } else if event_name.starts_with("s3:LifecycleExpiration:") {
        "lifecycle"
    } else if event_name.starts_with("s3:ObjectLock:") {
        "retention"
    } else {
        "other"
    }
}

fn audit_category(action: &str) -> &'static str {
    if action.starts_with("auth:") {
        "auth"
    } else if action.contains("Policy") {
        "policy"
    } else if action.contains("Retention")
        || action.contains("LegalHold")
        || action.contains("ObjectLock")
    {
        "retention"
    } else if action.starts_with("ops:") || action.contains("Bucket") {
        "admin"
    } else if action.starts_with("s3:") {
        "data"
    } else {
        "other"
    }
}

fn notification_event_ids_contiguous(events: &[&NotificationEvent]) -> bool {
    let Some(first) = events.first() else {
        return true;
    };
    let Some(mut expected) = first
        .event_id
        .strip_prefix('e')
        .and_then(|value| value.parse::<u64>().ok())
    else {
        return false;
    };
    for event in events {
        let Some(current) = event
            .event_id
            .strip_prefix('e')
            .and_then(|value| value.parse::<u64>().ok())
        else {
            return false;
        };
        if current != expected {
            return false;
        }
        expected += 1;
    }
    true
}

fn audit_sequences_contiguous(events: &[&AuditEvent]) -> bool {
    let Some(first) = events.first() else {
        return true;
    };
    let mut expected = first.sequence;
    for event in events {
        if event.sequence != expected {
            return false;
        }
        expected += 1;
    }
    true
}

fn notification_events_json_lines(events: &[&NotificationEvent]) -> String {
    events
        .iter()
        .map(|event| {
            format!(
                "{{\"event_id\":\"{}\",\"event_name\":\"{}\",\"bucket\":\"{}\",\"key\":\"{}\",\"version_id\":\"{}\",\"rule_id\":\"{}\",\"target_kind\":\"{}\",\"target_arn\":\"{}\",\"event_time_epoch_seconds\":{}}}",
                escape_json(&event.event_id),
                escape_json(&event.event_name),
                escape_json(&event.bucket),
                escape_json(&event.key),
                escape_json(&event.version_id),
                escape_json(&event.rule_id),
                escape_json(&event.target_kind),
                escape_json(&event.target_arn),
                event.event_time_epoch_seconds
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn audit_json_lines(events: &[&AuditEvent]) -> String {
    events
        .iter()
        .map(|event| {
            let detail = event
                .detail
                .as_ref()
                .map(|detail| format!("\"{}\"", escape_json(detail)))
                .unwrap_or_else(|| "null".to_string());
            format!(
                "{{\"sequence\":{},\"subject\":\"{}\",\"action\":\"{}\",\"resource\":\"{}\",\"outcome\":\"{:?}\",\"detail\":{}}}",
                event.sequence,
                escape_json(&event.subject),
                escape_json(&event.action),
                escape_json(&event.resource),
                event.outcome,
                detail
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn escape_json(value: &str) -> String {
    let mut escaped = String::with_capacity(value.len());
    for character in value.chars() {
        match character {
            '"' => escaped.push_str("\\\""),
            '\\' => escaped.push_str("\\\\"),
            '\n' => escaped.push_str("\\n"),
            '\r' => escaped.push_str("\\r"),
            '\t' => escaped.push_str("\\t"),
            '\u{08}' => escaped.push_str("\\b"),
            '\u{0c}' => escaped.push_str("\\f"),
            character if character.is_control() => {
                escaped.push_str(&format!("\\u{:04x}", character as u32));
            }
            character => escaped.push(character),
        }
    }
    escaped
}