use serde::{Deserialize, Serialize};
use super::FileRecord;
pub const NOTE_FORMAT: &str = "openmoat-provenance-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NoteFormat {
#[serde(rename = "openmoat-provenance-v1")]
V1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuditHead {
pub event: i64,
pub hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EventHash {
pub event: i64,
pub hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Note {
pub format: NoteFormat,
pub commit: String,
pub head: AuditHead,
pub events: Vec<EventHash>,
pub digest: String,
pub files: Vec<FileRecord>,
}
impl Note {
#[must_use]
pub fn digest_input(&self) -> Vec<u8> {
let mut out = Vec::new();
let mut field = |text: &str| {
out.extend_from_slice(text.len().to_string().as_bytes());
out.push(b':');
out.extend_from_slice(text.as_bytes());
out.push(b'\n');
};
field(NOTE_FORMAT);
field(&self.commit);
field(&self.head.event.to_string());
field(&self.head.hash);
for row in &self.events {
field(&row.event.to_string());
field(&row.hash);
}
for file in &self.files {
field(&file.path);
field(&file.host);
field(&file.session);
field(&file.event.to_string());
field(&file.ts_ms.to_string());
field(file.evidence.as_str());
let lines: Vec<String> = file
.lines
.iter()
.map(|r| format!("{}-{}", r.start, r.end))
.collect();
field(&lines.join(","));
match &file.untrusted {
Some(u) => {
field(u.kind.as_str());
field(&u.source);
}
None => field("-"),
}
}
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Discrepancy {
Omitted {
path: String,
host: String,
session: String,
},
Changed {
path: String,
host: String,
session: String,
what: &'static str,
},
}
#[must_use]
pub fn compare(claimed: &[FileRecord], found: &[FileRecord]) -> Vec<Discrepancy> {
let mut out = Vec::new();
let same = |a: &FileRecord, b: &FileRecord| {
a.path == b.path && a.host == b.host && a.session == b.session
};
for now in found {
let Some(note) = claimed.iter().find(|c| same(c, now)) else {
out.push(Discrepancy::Omitted {
path: now.path.clone(),
host: now.host.clone(),
session: now.session.clone(),
});
continue;
};
let what = if note.event != now.event || note.ts_ms != now.ts_ms {
Some("the write it cites")
} else if note.evidence != now.evidence || note.lines != now.lines {
Some("how the write is tied to the file")
} else if note.untrusted.as_ref().map(|u| u.kind) != now.untrusted.as_ref().map(|u| u.kind)
{
Some("whether the session had read untrusted content")
} else {
None
};
if let Some(what) = what {
out.push(Discrepancy::Changed {
path: now.path.clone(),
host: now.host.clone(),
session: now.session.clone(),
what,
});
}
}
out
}