1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::crypto::{hash, Hash, PublicKey, Sig};
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14pub struct EventId(pub Hash);
15
16impl EventId {
17 pub fn as_hash(&self) -> &Hash {
19 &self.0
20 }
21}
22
23impl std::fmt::Display for EventId {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{}", self.0)
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct ActorId {
32 pub key: PublicKey,
34 pub name: Option<String>,
36 pub kind: ActorKind,
38}
39
40impl ActorId {
41 pub fn new(key: PublicKey, kind: ActorKind) -> Self {
43 Self {
44 key,
45 name: None,
46 kind,
47 }
48 }
49
50 pub fn with_name(mut self, name: impl Into<String>) -> Self {
52 self.name = Some(name.into());
53 self
54 }
55
56 pub fn id(&self) -> Hash {
58 self.key.id()
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum ActorKind {
66 User,
68 System,
70 Agent,
72 Integration,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct ResourceId {
79 pub kind: ResourceKind,
81 pub id: String,
83 pub parent: Option<Box<ResourceId>>,
85}
86
87impl ResourceId {
88 pub fn new(kind: ResourceKind, id: impl Into<String>) -> Self {
90 Self {
91 kind,
92 id: id.into(),
93 parent: None,
94 }
95 }
96
97 pub fn with_parent(mut self, parent: ResourceId) -> Self {
99 self.parent = Some(Box::new(parent));
100 self
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum ResourceKind {
108 Repository,
110 Commit,
112 Branch,
114 Tag,
116 PullRequest,
118 Issue,
120 File,
122 User,
124 Organization,
126 Credential,
128 Config,
130 Document,
132 Other,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case")]
139pub enum EventType {
140 RepoCreated,
143 RepoDeleted,
145 RepoTransferred,
147 RepoVisibilityChanged,
149
150 Push { force: bool, commits: u32 },
153 BranchCreated,
155 BranchDeleted,
157 BranchProtectionChanged,
159 TagCreated,
161 TagDeleted,
163
164 PullRequestOpened,
167 PullRequestMerged,
169 PullRequestClosed,
171 ReviewSubmitted { verdict: ReviewVerdict },
173 IssueOpened,
175 IssueClosed,
177
178 AccessGranted { permission: String },
181 AccessRevoked,
183 Login { method: String },
185 Logout,
187 LoginFailed { reason: String },
189 MfaConfigured,
191
192 AgentAction {
195 action: String,
196 reasoning: Option<String>,
197 },
198 AgentAuthorized { scope: Vec<String> },
200 AgentRevoked,
202
203 DataExportRequested,
206 DataExportCompleted,
208 DataDeletionRequested,
210 DataDeletionCompleted,
212 ConsentGiven { purpose: String },
214 ConsentRevoked { purpose: String },
216
217 ConfigChanged { key: String },
220 ReleasePublished { version: String },
222 BackupCreated,
224 SecurityScan { findings: u32 },
226
227 AgentAccountability {
233 event_label: String,
235 summary: String,
237 },
238
239 Custom { name: String },
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247pub enum ReviewVerdict {
248 Approved,
249 ChangesRequested,
250 Commented,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum Outcome {
257 Success,
259 Failure { reason: String },
261 Denied { reason: String },
263 Pending,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269pub struct Attestation {
270 pub attester: PublicKey,
272 pub signature: Sig,
274 pub chain: Vec<AttestationLink>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct AttestationLink {
281 pub attester: PublicKey,
282 pub signature: Sig,
283 #[serde(with = "chrono::serde::ts_milliseconds")]
284 pub timestamp: DateTime<Utc>,
285}
286
287impl Attestation {
288 pub fn new(attester: PublicKey, signature: Sig) -> Self {
290 Self {
291 attester,
292 signature,
293 chain: Vec::new(),
294 }
295 }
296
297 pub fn verify(&self, canonical_bytes: &[u8]) -> Result<()> {
299 self.attester.verify(canonical_bytes, &self.signature)?;
300
301 let mut prev_sig_bytes = self.signature.to_bytes().to_vec();
304 for link in &self.chain {
305 link.attester.verify(&prev_sig_bytes, &link.signature)?;
306 prev_sig_bytes = link.signature.to_bytes().to_vec();
307 }
308
309 Ok(())
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315pub struct AuditEvent {
316 #[serde(with = "chrono::serde::ts_milliseconds")]
318 pub event_time: DateTime<Utc>,
319
320 pub event_type: EventType,
322
323 pub actor: ActorId,
325
326 pub resource: ResourceId,
328
329 pub outcome: Outcome,
331
332 pub metadata: Vec<u8>,
334
335 pub attestation: Attestation,
337}
338
339impl AuditEvent {
340 pub fn builder() -> AuditEventBuilder {
342 AuditEventBuilder::default()
343 }
344
345 pub fn canonical_bytes(&self) -> Vec<u8> {
347 let signable = SignableEvent {
349 event_time: self.event_time,
350 event_type: &self.event_type,
351 actor: &self.actor,
352 resource: &self.resource,
353 outcome: &self.outcome,
354 metadata: &self.metadata,
355 };
356 bincode::serialize(&signable).expect("serialization should not fail")
358 }
359
360 pub fn metadata_json(&self) -> Option<serde_json::Value> {
362 if self.metadata.is_empty() {
363 return None;
364 }
365 serde_json::from_slice(&self.metadata).ok()
366 }
367
368 pub fn has_metadata(&self) -> bool {
370 !self.metadata.is_empty()
371 }
372
373 pub fn id(&self) -> EventId {
375 EventId(hash(&self.canonical_bytes()))
376 }
377
378 pub fn attester(&self) -> &PublicKey {
380 &self.attestation.attester
381 }
382
383 pub fn signature(&self) -> &Sig {
385 &self.attestation.signature
386 }
387
388 pub fn verification_tuple(&self) -> (&PublicKey, Vec<u8>, &Sig) {
393 (self.attester(), self.canonical_bytes(), self.signature())
394 }
395
396 pub fn validate(&self) -> Result<()> {
398 self.attestation.verify(&self.canonical_bytes())?;
400
401 let now = Utc::now();
403 if self.event_time > now + chrono::Duration::minutes(5) {
404 return Err(Error::invalid_event("event_time is in the future"));
405 }
406
407 Ok(())
408 }
409}
410
411#[derive(Serialize)]
413struct SignableEvent<'a> {
414 #[serde(with = "chrono::serde::ts_milliseconds")]
415 event_time: DateTime<Utc>,
416 event_type: &'a EventType,
417 actor: &'a ActorId,
418 resource: &'a ResourceId,
419 outcome: &'a Outcome,
420 metadata: &'a [u8],
421}
422
423#[derive(Default)]
425pub struct AuditEventBuilder {
426 event_time: Option<DateTime<Utc>>,
427 event_type: Option<EventType>,
428 actor: Option<ActorId>,
429 resource: Option<ResourceId>,
430 outcome: Option<Outcome>,
431 metadata: Vec<u8>,
432}
433
434impl AuditEventBuilder {
435 pub fn event_time(mut self, time: DateTime<Utc>) -> Self {
437 self.event_time = Some(time);
438 self
439 }
440
441 pub fn now(mut self) -> Self {
443 self.event_time = Some(Utc::now());
444 self
445 }
446
447 pub fn event_type(mut self, event_type: EventType) -> Self {
449 self.event_type = Some(event_type);
450 self
451 }
452
453 pub fn actor(mut self, actor: ActorId) -> Self {
455 self.actor = Some(actor);
456 self
457 }
458
459 pub fn resource(mut self, resource: ResourceId) -> Self {
461 self.resource = Some(resource);
462 self
463 }
464
465 pub fn outcome(mut self, outcome: Outcome) -> Self {
467 self.outcome = Some(outcome);
468 self
469 }
470
471 pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
473 self.metadata = serde_json::to_vec(&metadata).unwrap_or_default();
474 self
475 }
476
477 pub fn metadata_bytes(mut self, bytes: Vec<u8>) -> Self {
479 self.metadata = bytes;
480 self
481 }
482
483 pub fn sign(self, key: &crate::crypto::SecretKey) -> Result<AuditEvent> {
485 let event_time = self
486 .event_time
487 .ok_or_else(|| Error::invalid_event("missing event_time"))?;
488 let event_type = self
489 .event_type
490 .ok_or_else(|| Error::invalid_event("missing event_type"))?;
491 let actor = self
492 .actor
493 .ok_or_else(|| Error::invalid_event("missing actor"))?;
494 let resource = self
495 .resource
496 .ok_or_else(|| Error::invalid_event("missing resource"))?;
497 let outcome = self.outcome.unwrap_or(Outcome::Success);
498
499 let signable = SignableEvent {
501 event_time,
502 event_type: &event_type,
503 actor: &actor,
504 resource: &resource,
505 outcome: &outcome,
506 metadata: &self.metadata,
507 };
508 let canonical = bincode::serialize(&signable)?;
509 let signature = key.sign(&canonical);
510
511 Ok(AuditEvent {
512 event_time,
513 event_type,
514 actor,
515 resource,
516 outcome,
517 metadata: self.metadata,
518 attestation: Attestation::new(key.public_key(), signature),
519 })
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::crypto::SecretKey;
527
528 fn test_key() -> SecretKey {
529 SecretKey::generate()
530 }
531
532 #[test]
533 fn test_event_creation_and_validation() {
534 let key = test_key();
535 let actor = ActorId::new(key.public_key(), ActorKind::User).with_name("alice");
536 let resource = ResourceId::new(ResourceKind::Repository, "myrepo");
537
538 let event = AuditEvent::builder()
539 .now()
540 .event_type(EventType::Push {
541 force: false,
542 commits: 3,
543 })
544 .actor(actor)
545 .resource(resource)
546 .outcome(Outcome::Success)
547 .sign(&key)
548 .unwrap();
549
550 assert!(event.validate().is_ok());
551 }
552
553 #[test]
554 fn test_event_id_is_deterministic() {
555 let key = test_key();
556 let time = Utc::now();
557 let actor = ActorId::new(key.public_key(), ActorKind::System);
558 let resource = ResourceId::new(ResourceKind::Config, "settings");
559
560 let event1 = AuditEvent::builder()
561 .event_time(time)
562 .event_type(EventType::ConfigChanged {
563 key: "theme".into(),
564 })
565 .actor(actor.clone())
566 .resource(resource.clone())
567 .sign(&key)
568 .unwrap();
569
570 let event2 = AuditEvent::builder()
571 .event_time(time)
572 .event_type(EventType::ConfigChanged {
573 key: "theme".into(),
574 })
575 .actor(actor)
576 .resource(resource)
577 .sign(&key)
578 .unwrap();
579
580 assert_eq!(event1.id(), event2.id());
581 }
582
583 #[test]
584 fn test_tampered_event_fails_validation() {
585 let key = test_key();
586 let actor = ActorId::new(key.public_key(), ActorKind::User);
587 let resource = ResourceId::new(ResourceKind::Repository, "myrepo");
588
589 let mut event = AuditEvent::builder()
590 .now()
591 .event_type(EventType::RepoDeleted)
592 .actor(actor)
593 .resource(resource)
594 .sign(&key)
595 .unwrap();
596
597 event.outcome = Outcome::Failure {
599 reason: "tampered".into(),
600 };
601
602 assert!(event.validate().is_err());
604 }
605
606 #[test]
607 fn test_bincode_roundtrip() {
608 let key = test_key();
609 let actor = ActorId::new(key.public_key(), ActorKind::User);
610 let resource = ResourceId::new(ResourceKind::Repository, "myrepo");
611
612 let event = AuditEvent::builder()
613 .now()
614 .event_type(EventType::Push {
615 force: false,
616 commits: 1,
617 })
618 .actor(actor)
619 .resource(resource)
620 .sign(&key)
621 .unwrap();
622
623 let bytes = bincode::serialize(&event).expect("serialize should work");
625 println!("Serialized event size: {} bytes", bytes.len());
626
627 let restored: AuditEvent = bincode::deserialize(&bytes).expect("deserialize should work");
629
630 assert_eq!(event.id(), restored.id());
631 assert!(restored.validate().is_ok());
632 }
633
634 #[test]
635 fn test_datetime_bincode() {
636 use chrono::{DateTime, Utc};
637 use serde::{Deserialize, Serialize};
638
639 #[derive(Serialize, Deserialize, Debug)]
640 struct TestTime {
641 #[serde(with = "chrono::serde::ts_milliseconds")]
642 time: DateTime<Utc>,
643 }
644
645 let t = TestTime { time: Utc::now() };
646 let bytes = bincode::serialize(&t).expect("serialize");
647 let restored: TestTime = bincode::deserialize(&bytes).expect("deserialize");
648 let diff = (t.time - restored.time).num_milliseconds().abs();
651 assert!(diff <= 1, "times should be within 1ms");
652 }
653
654 #[test]
655 fn test_actor_bincode() {
656 let key = test_key();
657 let actor = ActorId::new(key.public_key(), ActorKind::User).with_name("alice");
658
659 let bytes = bincode::serialize(&actor).expect("serialize");
660 println!("ActorId size: {} bytes", bytes.len());
661 let restored: ActorId = bincode::deserialize(&bytes).expect("deserialize");
662 assert_eq!(actor.id(), restored.id());
663 }
664
665 #[test]
666 fn test_attestation_bincode() {
667 let key = test_key();
668 let sig = key.sign(b"test");
669 let att = Attestation::new(key.public_key(), sig);
670
671 let bytes = bincode::serialize(&att).expect("serialize");
672 println!("Attestation size: {} bytes", bytes.len());
673 let restored: Attestation = bincode::deserialize(&bytes).expect("deserialize");
674 assert_eq!(att.attester.as_bytes(), restored.attester.as_bytes());
675 }
676
677 #[test]
678 fn test_event_type_bincode() {
679 let et = EventType::Push {
680 force: true,
681 commits: 5,
682 };
683 let bytes = bincode::serialize(&et).expect("serialize");
684 println!("EventType size: {} bytes", bytes.len());
685 let restored: EventType = bincode::deserialize(&bytes).expect("deserialize");
686 assert_eq!(et, restored);
687 }
688
689 #[test]
690 fn test_resource_bincode() {
691 let r = ResourceId::new(ResourceKind::Repository, "myrepo");
692 let bytes = bincode::serialize(&r).expect("serialize");
693 println!("ResourceId size: {} bytes", bytes.len());
694 let restored: ResourceId = bincode::deserialize(&bytes).expect("deserialize");
695 assert_eq!(r.id, restored.id);
696 }
697
698 #[test]
699 fn test_outcome_bincode() {
700 let o = Outcome::Success;
701 let bytes = bincode::serialize(&o).expect("serialize");
702 println!("Outcome size: {} bytes", bytes.len());
703 let restored: Outcome = bincode::deserialize(&bytes).expect("deserialize");
704 assert_eq!(o, restored);
705 }
706
707 #[test]
708 fn test_minimal_struct_bincode() {
709 use crate::crypto::Sig;
710 use serde::{Deserialize, Serialize};
711
712 #[derive(Serialize, Deserialize, Debug)]
714 struct TestWithSig {
715 data: u64,
716 sig: Sig,
717 }
718
719 let key = test_key();
720 let sig = key.sign(b"test");
721 let t = TestWithSig { data: 42, sig };
722
723 let bytes = bincode::serialize(&t).expect("serialize");
724 println!("TestWithSig size: {} bytes", bytes.len());
725 let _restored: TestWithSig = bincode::deserialize(&bytes).expect("deserialize");
726 }
727
728 #[test]
729 fn test_vec_attestation_link_bincode() {
730 let chain: Vec<AttestationLink> = vec![];
731 let bytes = bincode::serialize(&chain).expect("serialize");
732 println!("Empty chain size: {} bytes", bytes.len());
733 let restored: Vec<AttestationLink> = bincode::deserialize(&bytes).expect("deserialize");
734 assert_eq!(chain.len(), restored.len());
735 }
736
737 #[test]
738 fn test_full_struct_bincode() {
739 use chrono::{DateTime, Utc};
740 use serde::{Deserialize, Serialize};
741
742 #[derive(Serialize, Deserialize, Debug)]
744 struct TestEvent {
745 #[serde(with = "chrono::serde::ts_milliseconds")]
746 event_time: DateTime<Utc>,
747 event_type: EventType,
748 actor: ActorId,
749 resource: ResourceId,
750 outcome: Outcome,
751 metadata: Vec<u8>,
752 attestation: Attestation,
753 }
754
755 let key = test_key();
756 let sig = key.sign(b"test");
757
758 let t = TestEvent {
759 event_time: Utc::now(),
760 event_type: EventType::Push {
761 force: false,
762 commits: 1,
763 },
764 actor: ActorId::new(key.public_key(), ActorKind::User),
765 resource: ResourceId::new(ResourceKind::Repository, "myrepo"),
766 outcome: Outcome::Success,
767 metadata: vec![],
768 attestation: Attestation::new(key.public_key(), sig),
769 };
770
771 let bytes = bincode::serialize(&t).expect("serialize");
772 println!("TestEvent size: {} bytes", bytes.len());
773 let _restored: TestEvent = bincode::deserialize(&bytes).expect("deserialize");
774 }
775}