use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::types::ScanResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum QuarantineStatus {
Quarantined,
Approved,
Released,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuarantineMetadata {
pub id: Uuid,
pub sandbox_id: Option<String>,
pub agent_id: Option<String>,
pub scan_result: ScanResult,
pub status: QuarantineStatus,
pub quarantined_at: String,
pub approved_by: Option<String>,
pub approved_at: Option<String>,
}
#[derive(Debug)]
pub struct QuarantineStorage {
base_dir: PathBuf,
}
impl QuarantineStorage {
pub fn new(base_dir: impl Into<PathBuf>) -> Self {
Self {
base_dir: base_dir.into(),
}
}
pub fn quarantine(
&self,
artifact: &[u8],
scan_result: ScanResult,
sandbox_id: Option<&str>,
agent_id: Option<&str>,
) -> crate::Result<Uuid> {
let id = Uuid::new_v4();
let entry_dir = self.base_dir.join(id.to_string());
std::fs::create_dir_all(&entry_dir)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine dir: {e}")))?;
std::fs::write(entry_dir.join("artifact.bin"), artifact)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine artifact: {e}")))?;
let metadata = QuarantineMetadata {
id,
sandbox_id: sandbox_id.map(Into::into),
agent_id: agent_id.map(Into::into),
scan_result,
status: QuarantineStatus::Quarantined,
quarantined_at: chrono::Utc::now().to_rfc3339(),
approved_by: None,
approved_at: None,
};
let json = serde_json::to_string_pretty(&metadata)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine metadata: {e}")))?;
std::fs::write(entry_dir.join("metadata.json"), json)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine write: {e}")))?;
tracing::debug!(%id, "artifact quarantined");
Ok(id)
}
pub fn get(&self, id: Uuid) -> crate::Result<QuarantineMetadata> {
let path = self.base_dir.join(id.to_string()).join("metadata.json");
let json = std::fs::read_to_string(&path)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine read {id}: {e}")))?;
serde_json::from_str(&json)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine parse {id}: {e}")))
}
pub fn approve(&self, id: Uuid, approved_by: &str) -> crate::Result<()> {
let mut metadata = self.get(id)?;
metadata.status = QuarantineStatus::Approved;
metadata.approved_by = Some(approved_by.to_owned());
metadata.approved_at = Some(chrono::Utc::now().to_rfc3339());
self.write_metadata(id, &metadata)
}
pub fn reject(&self, id: Uuid) -> crate::Result<()> {
let mut metadata = self.get(id)?;
metadata.status = QuarantineStatus::Rejected;
self.write_metadata(id, &metadata)
}
pub fn list(&self) -> crate::Result<Vec<Uuid>> {
let mut ids = Vec::new();
if !self.base_dir.exists() {
return Ok(ids);
}
let entries = std::fs::read_dir(&self.base_dir)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine list: {e}")))?;
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str()
&& let Ok(id) = Uuid::parse_str(name)
{
ids.push(id);
}
}
Ok(ids)
}
pub fn remove(&self, id: Uuid) -> crate::Result<()> {
let entry_dir = self.base_dir.join(id.to_string());
if entry_dir.exists() {
std::fs::remove_dir_all(&entry_dir).map_err(|e| {
crate::KavachError::ExecFailed(format!("quarantine remove {id}: {e}"))
})?;
}
Ok(())
}
fn write_metadata(&self, id: Uuid, metadata: &QuarantineMetadata) -> crate::Result<()> {
let path = self.base_dir.join(id.to_string()).join("metadata.json");
let json = serde_json::to_string_pretty(metadata)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine write: {e}")))?;
std::fs::write(&path, json)
.map_err(|e| crate::KavachError::ExecFailed(format!("quarantine write: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scanning::types::{ScanVerdict, Severity};
fn make_scan_result() -> ScanResult {
ScanResult {
verdict: ScanVerdict::Block,
findings: vec![],
worst_severity: Severity::Critical,
}
}
#[test]
fn quarantine_and_get() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
let id = storage
.quarantine(
b"secret data",
make_scan_result(),
Some("sb-1"),
Some("agent-1"),
)
.unwrap();
let metadata = storage.get(id).unwrap();
assert_eq!(metadata.status, QuarantineStatus::Quarantined);
assert_eq!(metadata.sandbox_id.as_deref(), Some("sb-1"));
}
#[test]
fn approve_workflow() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
let id = storage
.quarantine(b"data", make_scan_result(), None, None)
.unwrap();
storage.approve(id, "admin@example.com").unwrap();
let metadata = storage.get(id).unwrap();
assert_eq!(metadata.status, QuarantineStatus::Approved);
assert_eq!(metadata.approved_by.as_deref(), Some("admin@example.com"));
assert!(metadata.approved_at.is_some());
}
#[test]
fn reject_workflow() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
let id = storage
.quarantine(b"data", make_scan_result(), None, None)
.unwrap();
storage.reject(id).unwrap();
let metadata = storage.get(id).unwrap();
assert_eq!(metadata.status, QuarantineStatus::Rejected);
}
#[test]
fn list_entries() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
let id1 = storage
.quarantine(b"a", make_scan_result(), None, None)
.unwrap();
let id2 = storage
.quarantine(b"b", make_scan_result(), None, None)
.unwrap();
let ids = storage.list().unwrap();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&id1));
assert!(ids.contains(&id2));
}
#[test]
fn remove_entry() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
let id = storage
.quarantine(b"data", make_scan_result(), None, None)
.unwrap();
storage.remove(id).unwrap();
assert!(storage.get(id).is_err());
assert!(storage.list().unwrap().is_empty());
}
#[test]
fn list_empty() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
assert!(storage.list().unwrap().is_empty());
}
#[test]
fn get_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let storage = QuarantineStorage::new(dir.path());
assert!(storage.get(Uuid::new_v4()).is_err());
}
}