use std::path::Path;
use std::time::SystemTime;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use super::surfaces::DeterministicSurfaces;
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attestation {
pub version: String,
pub timestamp: String,
pub policy: String,
pub surfaces: DeterministicSurfaces,
pub determinism_score: f64,
pub security_level: u8,
pub seeds: HashMap<String, u64>,
pub digests: HashMap<String, String>,
pub sbom: Option<Sbom>,
pub provenance: Option<Provenance>,
pub coverage: Option<CoverageMap>,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sbom {
pub format: String,
pub version: String,
pub dependencies: Vec<Dependency>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version: String,
pub license: Option<String>,
pub source: String,
pub sha256: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Provenance {
pub builder: BuilderIdentity,
pub invocation: BuildInvocation,
pub materials: Vec<Material>,
pub metadata: BuildMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuilderIdentity {
pub name: String,
pub version: String,
pub digest: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildInvocation {
pub command: Vec<String>,
pub env: HashMap<String, String>,
pub working_dir: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Material {
pub uri: String,
pub digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildMetadata {
pub started_at: String,
pub finished_at: Option<String>,
pub duration_secs: Option<f64>,
pub exit_code: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageMap {
pub total_lines: u64,
pub covered_lines: u64,
pub coverage_pct: f64,
pub files: Vec<FileCoverage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileCoverage {
pub path: String,
pub total_lines: u64,
pub covered_lines: u64,
pub coverage_pct: f64,
}
impl Attestation {
pub fn new(surfaces: &DeterministicSurfaces, policy: &str) -> Self {
let mut seeds = HashMap::new();
if let super::surfaces::TimeMode::Frozen(seed) = surfaces.time {
seeds.insert("time".to_string(), seed);
}
if let super::surfaces::RngMode::Seeded(seed) = surfaces.rng {
seeds.insert("rng".to_string(), seed);
}
let security_level = match policy {
"Locked" => 100,
"Permissive" => 30,
_ => 50,
};
Self {
version: "1.0".to_string(),
timestamp: format_timestamp(SystemTime::now()),
policy: policy.to_string(),
surfaces: surfaces.clone(),
determinism_score: surfaces.determinism_score(),
security_level,
seeds,
digests: HashMap::new(),
sbom: None,
provenance: None,
coverage: None,
metadata: HashMap::new(),
}
}
pub fn add_digest(&mut self, name: impl Into<String>, digest: impl Into<String>) {
self.digests.insert(name.into(), digest.into());
}
pub fn set_sbom(&mut self, sbom: Sbom) {
self.sbom = Some(sbom);
}
pub fn set_provenance(&mut self, provenance: Provenance) {
self.provenance = Some(provenance);
}
pub fn set_coverage(&mut self, coverage: CoverageMap) {
self.coverage = Some(coverage);
}
pub fn add_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
self.metadata.insert(key.into(), value);
}
pub fn policy(&self) -> &str {
&self.policy
}
pub fn export(&self, path: impl AsRef<Path>) -> Result<()> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn import(path: impl AsRef<Path>) -> Result<Self> {
let json = std::fs::read_to_string(path)?;
let attestation = serde_json::from_str(&json)?;
Ok(attestation)
}
pub fn verify(&self) -> Result<bool> {
Ok(true)
}
pub fn reproducibility_instructions(&self) -> Result<String> {
let mut instructions = String::new();
instructions.push_str("# Reproducibility Instructions\n\n");
instructions.push_str(&format!("Policy: {}\n", self.policy));
instructions.push_str(&format!("Determinism Score: {:.2}\n", self.determinism_score));
instructions.push_str(&format!("Security Level: {}\n\n", self.security_level));
instructions.push_str("## Seeds\n\n");
for (key, value) in &self.seeds {
instructions.push_str(&format!("- {}: {}\n", key, value));
}
instructions.push_str("\n## Configuration\n\n");
let surfaces_json = serde_json::to_string_pretty(&self.surfaces)
.map_err(|e| Error::new(&format!("Failed to serialize surfaces: {}", e)))?;
instructions.push_str(&format!("```json\n{}\n```\n", surfaces_json));
Ok(instructions)
}
}
fn format_timestamp(time: SystemTime) -> String {
let duration = time.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let nanos = duration.subsec_nanos();
format!("{}.{:09}Z", secs, nanos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_attestation_creation() {
let surfaces = DeterministicSurfaces::deterministic(42);
let attestation = Attestation::new(&surfaces, "Locked");
assert_eq!(attestation.policy, "Locked");
assert_eq!(attestation.determinism_score, 1.0);
assert_eq!(attestation.security_level, 100);
assert!(attestation.seeds.contains_key("time"));
assert!(attestation.seeds.contains_key("rng"));
}
#[test]
fn test_attestation_export_import() {
use tempfile::TempDir;
let surfaces = DeterministicSurfaces::deterministic(42);
let attestation = Attestation::new(&surfaces, "Locked");
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("attestation.json");
attestation.export(&path).unwrap();
let imported = Attestation::import(&path).unwrap();
assert_eq!(attestation.policy, imported.policy);
assert_eq!(attestation.determinism_score, imported.determinism_score);
}
#[test]
fn test_reproducibility_instructions() {
let surfaces = DeterministicSurfaces::deterministic(42);
let attestation = Attestation::new(&surfaces, "Locked");
let instructions = attestation.reproducibility_instructions().unwrap();
assert!(instructions.contains("Policy: Locked"));
assert!(instructions.contains("Determinism Score: 1.00"));
assert!(instructions.contains("time: 42"));
assert!(instructions.contains("rng: 42"));
}
}