Skip to main content

kaptein_viewmodel/
audit.rs

1//! The single write-audit record: one format, two consumers.
2//!
3//! Used by both the local audit log and the incident-timeline export. Records
4//! **operations, not values** — secrets are never persisted here. Fully `serde`-
5//! serializable so it can cross the `serve`/gRPC-Web boundary and be exported.
6
7use serde::{Deserialize, Serialize};
8
9/// A reference to a Kubernetes resource.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct ResourceRef {
12    pub group: String,
13    pub kind: String,
14    pub namespace: String,
15    pub name: String,
16}
17
18/// The operation that was performed. `McpToolCall` is intentionally **absent**: MCP is a
19/// *transport*, captured in `AuditEvent::source`, not a distinct operation. An agent that
20/// scales a deployment logs `Operation::Scale` with `source: Surface::Mcp`.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum Operation {
23    // Read operations (governance requires visibility into reads, not just writes).
24    List,
25    Describe,
26    Logs,
27    Diagnose,
28    // Write operations.
29    Delete,
30    Scale,
31    Restart,
32    Cordon,
33    Drain,
34    Evict,
35    Exec,
36    PortForward,
37    /// A GitOps write path action (branch + PR), not an API-server write.
38    GitPrOpened,
39    /// An operator viewed (unmasked) a secret — the single most audit-relevant event for
40    /// a tool that masks secrets by default.
41    SecretViewed,
42}
43
44/// The outcome of a write attempt.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum Outcome {
47    Applied,
48    DryRun,
49    Rejected,
50}
51
52/// Which projection initiated the action. MCP is a *source*, not an operation.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum Source {
56    Tui,
57    Gui,
58    Browser,
59    Mcp,
60    Headless,
61}
62
63/// The actor who performed the operation. An agent has its **own** identity, so agent
64/// actions are distinguishable from human actions (ADR-0007, ADR-0010).
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct Actor {
67    pub kind: ActorKind,
68    pub name: String,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum ActorKind {
74    Human,
75    Agent,
76}
77
78/// A single audit record. Serialized with `serde`; the same shape feeds the incident
79/// timeline export (one format, two consumers).
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct AuditEvent {
82    /// Unix epoch milliseconds — a typed instant, not a pre-formatted string, so it
83    /// sorts and localizes correctly.
84    pub timestamp: i64,
85    pub actor: Actor,
86    /// Cluster/context id — never a secret.
87    pub context: String,
88    pub operation: Operation,
89    pub target: ResourceRef,
90    pub outcome: Outcome,
91    /// Which projection initiated the action (MCP is a source, not an operation).
92    pub source: Source,
93    /// Identifies the debugging session / multi-step agent invocation, so the incident
94    /// timeline can group related events.
95    pub session_id: String,
96    /// Recorded break-glass justification (required for the break-glass guardrail to be
97    /// a complete control).
98    pub reason: Option<String>,
99    /// Who initiated the session on the agent's behalf (an agent acts under its own
100    /// ServiceAccount, but the audit question is still "who asked").
101    pub on_behalf_of: Option<String>,
102}