Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
113 lines
3.3 KiB
Rust
113 lines
3.3 KiB
Rust
//! Audit log record types. One JSON object per line, hash-chained by the exact bytes of each line.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{CallId, DataClass, DenyReason, Hash32, SessionId, Timestamp};
|
|
|
|
pub use crate::chain::{ChainFailure, ChainReport, ChainVerifier, Location, TornTail};
|
|
|
|
// JSON: {"outcome":"denied","reason":"no_grant"} ; the tag sits beside the fields
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
|
|
pub enum DecisionRecord {
|
|
Allowed {},
|
|
Ask {},
|
|
Denied { reason: DenyReason },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ApprovalAnswer {
|
|
Approved,
|
|
Refused,
|
|
Expired,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ResultStatus {
|
|
Result,
|
|
Failed,
|
|
}
|
|
|
|
// JSON: {"type":"decision","session":"…",…} ; the tag sits beside the fields
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
|
pub enum AuditEvent {
|
|
Decision {
|
|
session: SessionId,
|
|
call: CallId,
|
|
tool: String,
|
|
arguments: String,
|
|
outcome: DecisionRecord,
|
|
grant: Option<String>,
|
|
grant_sha256: Option<Hash32>,
|
|
taint: DataClass,
|
|
untrusted: bool,
|
|
},
|
|
Approval {
|
|
session: SessionId,
|
|
call: CallId,
|
|
decision: u64,
|
|
answer: ApprovalAnswer,
|
|
by: Option<String>,
|
|
post: Option<String>,
|
|
reason: Option<String>,
|
|
outcome: DecisionRecord,
|
|
grant: Option<String>,
|
|
grant_sha256: Option<Hash32>,
|
|
taint: DataClass,
|
|
untrusted: bool,
|
|
},
|
|
Result {
|
|
session: SessionId,
|
|
call: CallId,
|
|
decision: u64,
|
|
status: ResultStatus,
|
|
class: DataClass,
|
|
untrusted: bool,
|
|
truncated: bool,
|
|
bytes: u64,
|
|
sha256: Hash32,
|
|
taint_after: DataClass,
|
|
},
|
|
Recovery {
|
|
torn_bytes: u64,
|
|
torn_sha256: Hash32,
|
|
},
|
|
AcceptedBreak {
|
|
file: String,
|
|
line: u64,
|
|
last_good: Hash32,
|
|
},
|
|
}
|
|
|
|
// JSON: {"seq":0,"time":"…","prev":"…","event":{"type":"decision",…}} ; `event` is a nested object.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct AuditRecord {
|
|
pub seq: u64,
|
|
pub time: Timestamp,
|
|
pub prev: Hash32,
|
|
pub event: AuditEvent,
|
|
}
|
|
|
|
/// True if `name` is an audit log file: `YYYY-MM-DD.jsonl`, ASCII digits with `-` at positions 4
|
|
/// and 7, a month from 01 to 12 and a day from 01 to 31. `brokerd` and `bxctl audit verify` both
|
|
/// decide with this, so they read the same files.
|
|
pub fn is_audit_log_name(name: &str) -> bool {
|
|
let Some(date) = name.strip_suffix(".jsonl") else {
|
|
return false;
|
|
};
|
|
let shaped = date.len() == 10
|
|
&& date.bytes().enumerate().all(|(i, b)| match i {
|
|
4 | 7 => b == b'-',
|
|
_ => b.is_ascii_digit(),
|
|
});
|
|
if !shaped {
|
|
return false;
|
|
}
|
|
let number = |range: std::ops::Range<usize>| date.get(range).and_then(|s| s.parse::<u8>().ok());
|
|
matches!(number(5..7), Some(1..=12)) && matches!(number(8..10), Some(1..=31))
|
|
}
|