use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::crypto::{hash, Hash, PublicKey, Sig};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct EventId(pub Hash);
impl EventId {
pub fn as_hash(&self) -> &Hash {
&self.0
}
}
impl std::fmt::Display for EventId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActorId {
pub key: PublicKey,
pub name: Option<String>,
pub kind: ActorKind,
}
impl ActorId {
pub fn new(key: PublicKey, kind: ActorKind) -> Self {
Self {
key,
name: None,
kind,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn id(&self) -> Hash {
self.key.id()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActorKind {
User,
System,
Agent,
Integration,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceId {
pub kind: ResourceKind,
pub id: String,
pub parent: Option<Box<ResourceId>>,
}
impl ResourceId {
pub fn new(kind: ResourceKind, id: impl Into<String>) -> Self {
Self {
kind,
id: id.into(),
parent: None,
}
}
pub fn with_parent(mut self, parent: ResourceId) -> Self {
self.parent = Some(Box::new(parent));
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceKind {
Repository,
Commit,
Branch,
Tag,
PullRequest,
Issue,
File,
User,
Organization,
Credential,
Config,
Document,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
RepoCreated,
RepoDeleted,
RepoTransferred,
RepoVisibilityChanged,
Push { force: bool, commits: u32 },
BranchCreated,
BranchDeleted,
BranchProtectionChanged,
TagCreated,
TagDeleted,
PullRequestOpened,
PullRequestMerged,
PullRequestClosed,
ReviewSubmitted { verdict: ReviewVerdict },
IssueOpened,
IssueClosed,
AccessGranted { permission: String },
AccessRevoked,
Login { method: String },
Logout,
LoginFailed { reason: String },
MfaConfigured,
AgentAction {
action: String,
reasoning: Option<String>,
},
AgentAuthorized { scope: Vec<String> },
AgentRevoked,
DataExportRequested,
DataExportCompleted,
DataDeletionRequested,
DataDeletionCompleted,
ConsentGiven { purpose: String },
ConsentRevoked { purpose: String },
ConfigChanged { key: String },
ReleasePublished { version: String },
BackupCreated,
SecurityScan { findings: u32 },
AgentAccountability {
event_label: String,
summary: String,
},
Custom { name: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewVerdict {
Approved,
ChangesRequested,
Commented,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Outcome {
Success,
Failure { reason: String },
Denied { reason: String },
Pending,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
pub attester: PublicKey,
pub signature: Sig,
pub chain: Vec<AttestationLink>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttestationLink {
pub attester: PublicKey,
pub signature: Sig,
#[serde(with = "chrono::serde::ts_milliseconds")]
pub timestamp: DateTime<Utc>,
}
impl Attestation {
pub fn new(attester: PublicKey, signature: Sig) -> Self {
Self {
attester,
signature,
chain: Vec::new(),
}
}
pub fn verify(&self, canonical_bytes: &[u8]) -> Result<()> {
self.attester.verify(canonical_bytes, &self.signature)?;
let mut prev_sig_bytes = self.signature.to_bytes().to_vec();
for link in &self.chain {
link.attester.verify(&prev_sig_bytes, &link.signature)?;
prev_sig_bytes = link.signature.to_bytes().to_vec();
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditEvent {
#[serde(with = "chrono::serde::ts_milliseconds")]
pub event_time: DateTime<Utc>,
pub event_type: EventType,
pub actor: ActorId,
pub resource: ResourceId,
pub outcome: Outcome,
pub metadata: Vec<u8>,
pub attestation: Attestation,
}
impl AuditEvent {
pub fn builder() -> AuditEventBuilder {
AuditEventBuilder::default()
}
pub fn canonical_bytes(&self) -> Vec<u8> {
let signable = SignableEvent {
event_time: self.event_time,
event_type: &self.event_type,
actor: &self.actor,
resource: &self.resource,
outcome: &self.outcome,
metadata: &self.metadata,
};
bincode::serialize(&signable).expect("serialization should not fail")
}
pub fn metadata_json(&self) -> Option<serde_json::Value> {
if self.metadata.is_empty() {
return None;
}
serde_json::from_slice(&self.metadata).ok()
}
pub fn has_metadata(&self) -> bool {
!self.metadata.is_empty()
}
pub fn id(&self) -> EventId {
EventId(hash(&self.canonical_bytes()))
}
pub fn attester(&self) -> &PublicKey {
&self.attestation.attester
}
pub fn signature(&self) -> &Sig {
&self.attestation.signature
}
pub fn verification_tuple(&self) -> (&PublicKey, Vec<u8>, &Sig) {
(self.attester(), self.canonical_bytes(), self.signature())
}
pub fn validate(&self) -> Result<()> {
self.attestation.verify(&self.canonical_bytes())?;
let now = Utc::now();
if self.event_time > now + chrono::Duration::minutes(5) {
return Err(Error::invalid_event("event_time is in the future"));
}
Ok(())
}
}
#[derive(Serialize)]
struct SignableEvent<'a> {
#[serde(with = "chrono::serde::ts_milliseconds")]
event_time: DateTime<Utc>,
event_type: &'a EventType,
actor: &'a ActorId,
resource: &'a ResourceId,
outcome: &'a Outcome,
metadata: &'a [u8],
}
#[derive(Default)]
pub struct AuditEventBuilder {
event_time: Option<DateTime<Utc>>,
event_type: Option<EventType>,
actor: Option<ActorId>,
resource: Option<ResourceId>,
outcome: Option<Outcome>,
metadata: Vec<u8>,
}
impl AuditEventBuilder {
pub fn event_time(mut self, time: DateTime<Utc>) -> Self {
self.event_time = Some(time);
self
}
pub fn now(mut self) -> Self {
self.event_time = Some(Utc::now());
self
}
pub fn event_type(mut self, event_type: EventType) -> Self {
self.event_type = Some(event_type);
self
}
pub fn actor(mut self, actor: ActorId) -> Self {
self.actor = Some(actor);
self
}
pub fn resource(mut self, resource: ResourceId) -> Self {
self.resource = Some(resource);
self
}
pub fn outcome(mut self, outcome: Outcome) -> Self {
self.outcome = Some(outcome);
self
}
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = serde_json::to_vec(&metadata).unwrap_or_default();
self
}
pub fn metadata_bytes(mut self, bytes: Vec<u8>) -> Self {
self.metadata = bytes;
self
}
pub fn sign(self, key: &crate::crypto::SecretKey) -> Result<AuditEvent> {
let event_time = self
.event_time
.ok_or_else(|| Error::invalid_event("missing event_time"))?;
let event_type = self
.event_type
.ok_or_else(|| Error::invalid_event("missing event_type"))?;
let actor = self
.actor
.ok_or_else(|| Error::invalid_event("missing actor"))?;
let resource = self
.resource
.ok_or_else(|| Error::invalid_event("missing resource"))?;
let outcome = self.outcome.unwrap_or(Outcome::Success);
let signable = SignableEvent {
event_time,
event_type: &event_type,
actor: &actor,
resource: &resource,
outcome: &outcome,
metadata: &self.metadata,
};
let canonical = bincode::serialize(&signable)?;
let signature = key.sign(&canonical);
Ok(AuditEvent {
event_time,
event_type,
actor,
resource,
outcome,
metadata: self.metadata,
attestation: Attestation::new(key.public_key(), signature),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::SecretKey;
fn test_key() -> SecretKey {
SecretKey::generate()
}
#[test]
fn test_event_creation_and_validation() {
let key = test_key();
let actor = ActorId::new(key.public_key(), ActorKind::User).with_name("alice");
let resource = ResourceId::new(ResourceKind::Repository, "myrepo");
let event = AuditEvent::builder()
.now()
.event_type(EventType::Push {
force: false,
commits: 3,
})
.actor(actor)
.resource(resource)
.outcome(Outcome::Success)
.sign(&key)
.unwrap();
assert!(event.validate().is_ok());
}
#[test]
fn test_event_id_is_deterministic() {
let key = test_key();
let time = Utc::now();
let actor = ActorId::new(key.public_key(), ActorKind::System);
let resource = ResourceId::new(ResourceKind::Config, "settings");
let event1 = AuditEvent::builder()
.event_time(time)
.event_type(EventType::ConfigChanged {
key: "theme".into(),
})
.actor(actor.clone())
.resource(resource.clone())
.sign(&key)
.unwrap();
let event2 = AuditEvent::builder()
.event_time(time)
.event_type(EventType::ConfigChanged {
key: "theme".into(),
})
.actor(actor)
.resource(resource)
.sign(&key)
.unwrap();
assert_eq!(event1.id(), event2.id());
}
#[test]
fn test_tampered_event_fails_validation() {
let key = test_key();
let actor = ActorId::new(key.public_key(), ActorKind::User);
let resource = ResourceId::new(ResourceKind::Repository, "myrepo");
let mut event = AuditEvent::builder()
.now()
.event_type(EventType::RepoDeleted)
.actor(actor)
.resource(resource)
.sign(&key)
.unwrap();
event.outcome = Outcome::Failure {
reason: "tampered".into(),
};
assert!(event.validate().is_err());
}
#[test]
fn test_bincode_roundtrip() {
let key = test_key();
let actor = ActorId::new(key.public_key(), ActorKind::User);
let resource = ResourceId::new(ResourceKind::Repository, "myrepo");
let event = AuditEvent::builder()
.now()
.event_type(EventType::Push {
force: false,
commits: 1,
})
.actor(actor)
.resource(resource)
.sign(&key)
.unwrap();
let bytes = bincode::serialize(&event).expect("serialize should work");
println!("Serialized event size: {} bytes", bytes.len());
let restored: AuditEvent = bincode::deserialize(&bytes).expect("deserialize should work");
assert_eq!(event.id(), restored.id());
assert!(restored.validate().is_ok());
}
#[test]
fn test_datetime_bincode() {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct TestTime {
#[serde(with = "chrono::serde::ts_milliseconds")]
time: DateTime<Utc>,
}
let t = TestTime { time: Utc::now() };
let bytes = bincode::serialize(&t).expect("serialize");
let restored: TestTime = bincode::deserialize(&bytes).expect("deserialize");
let diff = (t.time - restored.time).num_milliseconds().abs();
assert!(diff <= 1, "times should be within 1ms");
}
#[test]
fn test_actor_bincode() {
let key = test_key();
let actor = ActorId::new(key.public_key(), ActorKind::User).with_name("alice");
let bytes = bincode::serialize(&actor).expect("serialize");
println!("ActorId size: {} bytes", bytes.len());
let restored: ActorId = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(actor.id(), restored.id());
}
#[test]
fn test_attestation_bincode() {
let key = test_key();
let sig = key.sign(b"test");
let att = Attestation::new(key.public_key(), sig);
let bytes = bincode::serialize(&att).expect("serialize");
println!("Attestation size: {} bytes", bytes.len());
let restored: Attestation = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(att.attester.as_bytes(), restored.attester.as_bytes());
}
#[test]
fn test_event_type_bincode() {
let et = EventType::Push {
force: true,
commits: 5,
};
let bytes = bincode::serialize(&et).expect("serialize");
println!("EventType size: {} bytes", bytes.len());
let restored: EventType = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(et, restored);
}
#[test]
fn test_resource_bincode() {
let r = ResourceId::new(ResourceKind::Repository, "myrepo");
let bytes = bincode::serialize(&r).expect("serialize");
println!("ResourceId size: {} bytes", bytes.len());
let restored: ResourceId = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(r.id, restored.id);
}
#[test]
fn test_outcome_bincode() {
let o = Outcome::Success;
let bytes = bincode::serialize(&o).expect("serialize");
println!("Outcome size: {} bytes", bytes.len());
let restored: Outcome = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(o, restored);
}
#[test]
fn test_minimal_struct_bincode() {
use crate::crypto::Sig;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct TestWithSig {
data: u64,
sig: Sig,
}
let key = test_key();
let sig = key.sign(b"test");
let t = TestWithSig { data: 42, sig };
let bytes = bincode::serialize(&t).expect("serialize");
println!("TestWithSig size: {} bytes", bytes.len());
let _restored: TestWithSig = bincode::deserialize(&bytes).expect("deserialize");
}
#[test]
fn test_vec_attestation_link_bincode() {
let chain: Vec<AttestationLink> = vec![];
let bytes = bincode::serialize(&chain).expect("serialize");
println!("Empty chain size: {} bytes", bytes.len());
let restored: Vec<AttestationLink> = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(chain.len(), restored.len());
}
#[test]
fn test_full_struct_bincode() {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct TestEvent {
#[serde(with = "chrono::serde::ts_milliseconds")]
event_time: DateTime<Utc>,
event_type: EventType,
actor: ActorId,
resource: ResourceId,
outcome: Outcome,
metadata: Vec<u8>,
attestation: Attestation,
}
let key = test_key();
let sig = key.sign(b"test");
let t = TestEvent {
event_time: Utc::now(),
event_type: EventType::Push {
force: false,
commits: 1,
},
actor: ActorId::new(key.public_key(), ActorKind::User),
resource: ResourceId::new(ResourceKind::Repository, "myrepo"),
outcome: Outcome::Success,
metadata: vec![],
attestation: Attestation::new(key.public_key(), sig),
};
let bytes = bincode::serialize(&t).expect("serialize");
println!("TestEvent size: {} bytes", bytes.len());
let _restored: TestEvent = bincode::deserialize(&bytes).expect("deserialize");
}
}