use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const AUDIT_EVENT_SCHEMA: &str = "tsafe.audit_event.v1";
pub const AUDIT_EVENT_DATASCHEMA: &str = "https://schemas.tsafe.dev/tsafe.audit_event.v1.json";
pub const CLOUDEVENTS_SPECVERSION: &str = "1.0";
pub const DEFAULT_CLOUDEVENT_SOURCE: &str = "urn:tsafe:local";
pub const DEFAULT_PROVENANCE_REPO: &str = "tsafe";
pub const DEFAULT_PROVENANCE_PRODUCER: &str = "tsafe-cli";
pub const DEFAULT_PROVENANCE_KIND_EXECUTION: &str = "execution";
pub const DEFAULT_PROVENANCE_KIND_OTHER: &str = "other";
pub const EVENT_SCAN_COMPLETED: &str = "dev.tsafe.scan.completed.v1";
pub const EVENT_SCAN_FAILED: &str = "dev.tsafe.scan.failed.v1";
pub const EVENT_CONTRACT_CREATED: &str = "dev.tsafe.contract.created.v1";
pub const EVENT_CONTRACT_REJECTED: &str = "dev.tsafe.contract.rejected.v1";
pub const EVENT_ENFORCE_STARTED: &str = "dev.tsafe.enforce.started.v1";
pub const EVENT_ENFORCE_COMPLETED: &str = "dev.tsafe.enforce.completed.v1";
pub const EVENT_ENFORCE_REJECTED: &str = "dev.tsafe.enforce.rejected.v1";
pub const EVENT_AUDIT_RENDERED: &str = "dev.tsafe.audit.rendered.v1";
pub const EVENT_AUDIT_FAILED: &str = "dev.tsafe.audit.failed.v1";
pub const LEGACY_AUDIT_EVENT_SCHEMA: &str = "algol.audit_event.v1";
pub const LEGACY_AUDIT_EVENT_DATASCHEMA: &str =
"https://schemas.algol.dev/algol.audit_event.v1.json";
pub const LEGACY_CLOUDEVENT_SOURCE: &str = "urn:algol:local";
pub const LEGACY_PROVENANCE_REPO: &str = "algol";
pub const LEGACY_PROVENANCE_PRODUCER: &str = "algol-cli";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AuditOutcome {
Success,
Failure,
Rejected,
}
impl AuditOutcome {
pub fn as_str(&self) -> &'static str {
match self {
Self::Success => "success",
Self::Failure => "failure",
Self::Rejected => "rejected",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditActor {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditResource {
pub kind: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditArtifactRef {
pub schema: String,
pub path: String,
pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditTouchedResource {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id_ref: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuditEvent {
pub schema: String,
pub operation: String,
pub time: DateTime<Utc>,
pub actor: AuditActor,
pub resource: AuditResource,
pub outcome: AuditOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason_code: Option<String>,
pub correlation_id: String,
pub idempotency_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(default)]
pub artifact_refs: Vec<AuditArtifactRef>,
#[serde(default)]
pub touched_resources: Vec<AuditTouchedResource>,
pub redaction: String,
}
impl AuditEvent {
pub fn validation_errors(&self) -> Vec<String> {
let mut errors = Vec::new();
if !is_supported_audit_event_schema(&self.schema) {
errors.push(format!("unsupported schema {}", self.schema));
}
if self.operation.trim().is_empty() {
errors.push("operation must not be empty".to_string());
}
if self.actor.kind.trim().is_empty() {
errors.push("actor.kind must not be empty".to_string());
}
if self.resource.kind.trim().is_empty() {
errors.push("resource.kind must not be empty".to_string());
}
if self.resource.id.trim().is_empty() {
errors.push("resource.id must not be empty".to_string());
}
if self.correlation_id.trim().is_empty() {
errors.push("correlation_id must not be empty".to_string());
}
if self.idempotency_key.trim().is_empty() {
errors.push("idempotency_key must not be empty".to_string());
}
if self.redaction != "blake3" && self.redaction != "sha256" {
errors.push(format!(
"unsupported redaction {}; expected blake3 (or sha256 during compat window)",
self.redaction
));
}
validate_artifact_refs(&self.artifact_refs, &mut errors);
validate_touched_resources(&self.touched_resources, &mut errors);
validate_no_obvious_raw_sensitive_values(self, &mut errors);
errors
}
pub fn ensure_valid(&self) -> Result<(), String> {
let errors = self.validation_errors();
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("; "))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CloudEvent {
pub specversion: String,
pub id: String,
pub source: String,
#[serde(rename = "type")]
pub event_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub datacontenttype: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dataschema: Option<String>,
pub correlationid: String,
pub provenancerepo: String,
pub provenanceproducer: String,
pub provenanceversion: String,
pub provenancekind: String,
#[serde(alias = "algoloperation")]
pub tsafeoperation: String,
#[serde(alias = "algoloutcome")]
pub tsafeoutcome: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "algolartifacthash"
)]
pub tsafeartifacthash: Option<String>,
pub data: AuditEvent,
}
impl CloudEvent {
pub fn validation_errors(&self) -> Vec<String> {
let mut errors = Vec::new();
if self.specversion != CLOUDEVENTS_SPECVERSION {
errors.push(format!("unsupported specversion {}", self.specversion));
}
if self.id.trim().is_empty() {
errors.push("id must not be empty".to_string());
}
if self.source.trim().is_empty() {
errors.push("source must not be empty".to_string());
}
if self.event_type.trim().is_empty() {
errors.push("type must not be empty".to_string());
}
if self.correlationid.trim().is_empty() {
errors.push("correlationid must not be empty".to_string());
}
if let Some(dataschema) = &self.dataschema {
if dataschema != AUDIT_EVENT_DATASCHEMA && dataschema != LEGACY_AUDIT_EVENT_DATASCHEMA {
errors.push(format!("unsupported dataschema {dataschema}"));
}
}
if self.tsafeoperation.trim().is_empty() {
errors.push("tsafeoperation must not be empty".to_string());
}
if self.tsafeoutcome.trim().is_empty() {
errors.push("tsafeoutcome must not be empty".to_string());
}
if self.provenancerepo.trim().is_empty() {
errors.push("provenancerepo must not be empty".to_string());
}
if self.provenanceproducer.trim().is_empty() {
errors.push("provenanceproducer must not be empty".to_string());
}
if self.provenanceversion.trim().is_empty() {
errors.push("provenanceversion must not be empty".to_string());
}
if self.provenancekind.trim().is_empty() {
errors.push("provenancekind must not be empty".to_string());
}
errors.extend(
self.data
.validation_errors()
.into_iter()
.map(|error| format!("data.{error}")),
);
if self.correlationid != self.data.correlation_id {
errors.push("correlationid must match data.correlation_id".to_string());
}
if self.tsafeoperation != self.data.operation {
errors.push("tsafeoperation must match data.operation".to_string());
}
if self.tsafeoutcome != self.data.outcome.as_str() {
errors.push("tsafeoutcome must match data.outcome".to_string());
}
validate_no_obvious_raw_sensitive_values(self, &mut errors);
errors
}
pub fn ensure_valid(&self) -> Result<(), String> {
let errors = self.validation_errors();
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("; "))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnforceCompletedInput {
pub event_id: String,
pub source: String,
pub time: DateTime<Utc>,
pub subject: String,
pub actor: AuditActor,
pub resource: AuditResource,
pub correlation_id: String,
pub idempotency_key: String,
pub fingerprint: Option<String>,
pub artifact_refs: Vec<AuditArtifactRef>,
pub touched_resources: Vec<AuditTouchedResource>,
pub artifact_hash: Option<String>,
pub tsafe_attest_version: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LifecycleEventInput {
pub event_type: String,
pub operation: String,
pub outcome: AuditOutcome,
pub reason_code: Option<String>,
pub event_id: String,
pub source: String,
pub time: DateTime<Utc>,
pub subject: String,
pub actor: AuditActor,
pub resource: AuditResource,
pub correlation_id: String,
pub idempotency_key: String,
pub fingerprint: Option<String>,
pub artifact_refs: Vec<AuditArtifactRef>,
pub touched_resources: Vec<AuditTouchedResource>,
pub artifact_hash: Option<String>,
pub tsafe_attest_version: String,
pub provenancekind: String,
}
pub fn build_enforce_completed_audit_event(input: &EnforceCompletedInput) -> AuditEvent {
AuditEvent {
schema: AUDIT_EVENT_SCHEMA.to_string(),
operation: "enforce.completed".to_string(),
time: input.time,
actor: input.actor.clone(),
resource: input.resource.clone(),
outcome: AuditOutcome::Success,
reason_code: None,
correlation_id: input.correlation_id.clone(),
idempotency_key: input.idempotency_key.clone(),
fingerprint: input.fingerprint.clone(),
artifact_refs: input.artifact_refs.clone(),
touched_resources: input.touched_resources.clone(),
redaction: "blake3".to_string(),
}
}
pub fn project_enforce_completed_cloudevent(input: &EnforceCompletedInput) -> CloudEvent {
let data = build_enforce_completed_audit_event(input);
CloudEvent {
specversion: CLOUDEVENTS_SPECVERSION.to_string(),
id: input.event_id.clone(),
source: if input.source.trim().is_empty() {
DEFAULT_CLOUDEVENT_SOURCE.to_string()
} else {
input.source.clone()
},
event_type: EVENT_ENFORCE_COMPLETED.to_string(),
time: Some(input.time),
subject: Some(input.subject.clone()),
datacontenttype: Some("application/json".to_string()),
dataschema: Some(AUDIT_EVENT_DATASCHEMA.to_string()),
correlationid: input.correlation_id.clone(),
provenancerepo: DEFAULT_PROVENANCE_REPO.to_string(),
provenanceproducer: DEFAULT_PROVENANCE_PRODUCER.to_string(),
provenanceversion: input.tsafe_attest_version.clone(),
provenancekind: DEFAULT_PROVENANCE_KIND_EXECUTION.to_string(),
tsafeoperation: data.operation.clone(),
tsafeoutcome: data.outcome.as_str().to_string(),
tsafeartifacthash: input.artifact_hash.clone(),
data,
}
}
pub fn build_lifecycle_audit_event(input: &LifecycleEventInput) -> AuditEvent {
AuditEvent {
schema: AUDIT_EVENT_SCHEMA.to_string(),
operation: input.operation.clone(),
time: input.time,
actor: input.actor.clone(),
resource: input.resource.clone(),
outcome: input.outcome.clone(),
reason_code: input.reason_code.clone(),
correlation_id: input.correlation_id.clone(),
idempotency_key: input.idempotency_key.clone(),
fingerprint: input.fingerprint.clone(),
artifact_refs: input.artifact_refs.clone(),
touched_resources: input.touched_resources.clone(),
redaction: "blake3".to_string(),
}
}
pub fn project_lifecycle_cloudevent(input: &LifecycleEventInput) -> CloudEvent {
let data = build_lifecycle_audit_event(input);
CloudEvent {
specversion: CLOUDEVENTS_SPECVERSION.to_string(),
id: input.event_id.clone(),
source: if input.source.trim().is_empty() {
DEFAULT_CLOUDEVENT_SOURCE.to_string()
} else {
input.source.clone()
},
event_type: input.event_type.clone(),
time: Some(input.time),
subject: Some(input.subject.clone()),
datacontenttype: Some("application/json".to_string()),
dataschema: Some(AUDIT_EVENT_DATASCHEMA.to_string()),
correlationid: input.correlation_id.clone(),
provenancerepo: DEFAULT_PROVENANCE_REPO.to_string(),
provenanceproducer: DEFAULT_PROVENANCE_PRODUCER.to_string(),
provenanceversion: input.tsafe_attest_version.clone(),
provenancekind: if input.provenancekind.trim().is_empty() {
DEFAULT_PROVENANCE_KIND_OTHER.to_string()
} else {
input.provenancekind.clone()
},
tsafeoperation: data.operation.clone(),
tsafeoutcome: data.outcome.as_str().to_string(),
tsafeartifacthash: input.artifact_hash.clone(),
data,
}
}
pub fn is_supported_audit_event_schema(schema: &str) -> bool {
schema == AUDIT_EVENT_SCHEMA || schema == LEGACY_AUDIT_EVENT_SCHEMA
}
fn validate_artifact_refs(items: &[AuditArtifactRef], errors: &mut Vec<String>) {
for item in items {
if item.schema.trim().is_empty() {
errors.push("artifact_refs schema must not be empty".to_string());
}
if item.path.trim().is_empty() {
errors.push("artifact_refs path must not be empty".to_string());
}
if !is_supported_hash(&item.hash) {
errors.push(format!(
"artifact_refs {} hash must be a blake3 hash (or sha256 during compat window)",
item.path
));
}
}
}
fn validate_touched_resources(items: &[AuditTouchedResource], errors: &mut Vec<String>) {
for item in items {
if item.kind.trim().is_empty() {
errors.push("touched_resources kind must not be empty".to_string());
}
if item.name_ref.is_none() && item.id_ref.is_none() {
errors.push(format!(
"touched_resources {} must include name_ref or id_ref",
item.kind
));
}
if let Some(name_ref) = &item.name_ref {
if !is_supported_hash(name_ref) {
errors.push(format!(
"touched_resources {} name_ref must be a blake3 hash (or sha256 during compat window)",
item.kind
));
}
}
if let Some(id_ref) = &item.id_ref {
if !is_supported_hash(id_ref) {
errors.push(format!(
"touched_resources {} id_ref must be a blake3 hash (or sha256 during compat window)",
item.kind
));
}
}
}
}
fn validate_no_obvious_raw_sensitive_values<T: Serialize>(value: &T, errors: &mut Vec<String>) {
let Ok(value) = serde_json::to_value(value) else {
errors.push("event must serialize for sensitive value validation".to_string());
return;
};
find_obvious_sensitive_value(&value, "$", errors);
}
fn find_obvious_sensitive_value(value: &Value, path: &str, errors: &mut Vec<String>) {
match value {
Value::Object(map) => {
for (key, nested) in map {
let next_path = format!("{path}.{key}");
if let Value::String(text) = nested {
if is_obvious_raw_sensitive_value(key, text) {
errors.push(format!(
"{next_path} appears to contain a raw sensitive value"
));
}
}
find_obvious_sensitive_value(nested, &next_path, errors);
}
}
Value::Array(items) => {
for (index, nested) in items.iter().enumerate() {
find_obvious_sensitive_value(nested, &format!("{path}[{index}]"), errors);
}
}
Value::String(text) => {
if contains_known_secret_sample(text) {
errors.push(format!("{path} appears to contain a raw sensitive value"));
}
}
_ => {}
}
}
fn is_obvious_raw_sensitive_value(key: &str, value: &str) -> bool {
if contains_known_secret_sample(value) {
return true;
}
let key = key.to_ascii_lowercase();
let key_suggests_secret_value =
key == "value" || key.ends_with("_value") || key.contains("secret_value");
key_suggests_secret_value && !is_safe_reference_or_redaction(value)
}
fn contains_known_secret_sample(value: &str) -> bool {
let lowered = value.to_ascii_lowercase();
lowered.contains("super-secret-value")
|| lowered.contains("-----begin private key-----")
|| lowered.starts_with("sk-")
|| lowered.starts_with("ghp_")
}
fn is_safe_reference_or_redaction(value: &str) -> bool {
value == "****"
|| value.contains("****")
|| is_supported_hash(value)
|| value.starts_with("blake3:")
|| value.starts_with("sha256:")
|| value.starts_with("redacted:")
}
fn is_supported_hash(value: &str) -> bool {
is_blake3_hash(value) || is_sha256_hash(value)
}
fn is_blake3_hash(value: &str) -> bool {
let Some(hex) = value.strip_prefix("blake3:") else {
return false;
};
hex.len() == 64 && hex.chars().all(|char| char.is_ascii_hexdigit())
}
fn is_sha256_hash(value: &str) -> bool {
let Some(hex) = value.strip_prefix("sha256:") else {
return false;
};
hex.len() == 64 && hex.chars().all(|char| char.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
fn blake3_test_hash() -> String {
"blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string()
}
fn input() -> EnforceCompletedInput {
EnforceCompletedInput {
event_id: "01HXEVENT000000000000000001".to_string(),
source: DEFAULT_CLOUDEVENT_SOURCE.to_string(),
time: "2026-05-21T00:00:00Z".parse().unwrap(),
subject: blake3_test_hash(),
actor: AuditActor {
kind: "local-user".to_string(),
id_hash: Some(blake3_test_hash()),
},
resource: AuditResource {
kind: "run".to_string(),
id: blake3_test_hash(),
},
correlation_id: "01HXCORRELATION000000000001".to_string(),
idempotency_key: "01HXIDEMPOTENCY00000000001".to_string(),
fingerprint: Some("3f4a1c00000000000000000000000000".to_string()),
artifact_refs: vec![AuditArtifactRef {
schema: "tsafe.run.v1".to_string(),
path: "tsafe-run.json".to_string(),
hash: blake3_test_hash(),
}],
touched_resources: vec![AuditTouchedResource {
kind: "env".to_string(),
name_ref: Some(blake3_test_hash()),
id_ref: None,
}],
artifact_hash: Some(blake3_test_hash()),
tsafe_attest_version: "1.2.0".to_string(),
}
}
#[test]
fn events_enforce_completed_projection_validates() {
let event = project_enforce_completed_cloudevent(&input());
assert_eq!(event.event_type, EVENT_ENFORCE_COMPLETED);
assert_eq!(event.source, DEFAULT_CLOUDEVENT_SOURCE);
assert_eq!(event.provenancerepo, "tsafe");
assert_eq!(event.provenanceproducer, "tsafe-cli");
assert!(
event.validation_errors().is_empty(),
"{:?}",
event.validation_errors()
);
}
#[test]
fn events_validation_rejects_missing_correlation() {
let mut event = build_enforce_completed_audit_event(&input());
event.correlation_id.clear();
assert!(event
.validation_errors()
.iter()
.any(|error| error.contains("correlation_id")));
}
#[test]
fn events_validation_rejects_raw_secret_sample() {
let mut event = build_enforce_completed_audit_event(&input());
event.reason_code = Some("super-secret-value".to_string());
assert!(event
.validation_errors()
.iter()
.any(|error| error.contains("raw sensitive value")));
}
#[test]
fn cloudevent_compat_legacy_extension_attrs_deserialise() {
let blob = serde_json::json!({
"specversion": "1.0",
"id": "01HXEVENT000000000000000099",
"source": LEGACY_CLOUDEVENT_SOURCE,
"type": "dev.algol.enforce.completed.v1",
"time": "2026-05-20T00:00:00Z",
"subject": blake3_test_hash(),
"datacontenttype": "application/json",
"dataschema": LEGACY_AUDIT_EVENT_DATASCHEMA,
"correlationid": "01HXCORR00000000000099",
"provenancerepo": LEGACY_PROVENANCE_REPO,
"provenanceproducer": LEGACY_PROVENANCE_PRODUCER,
"provenanceversion": "0.1.0",
"provenancekind": "execution",
"algoloperation": "enforce.completed",
"algoloutcome": "success",
"algolartifacthash": blake3_test_hash(),
"data": {
"schema": LEGACY_AUDIT_EVENT_SCHEMA,
"operation": "enforce.completed",
"time": "2026-05-20T00:00:00Z",
"actor": {"kind": "local-user"},
"resource": {"kind": "run", "id": blake3_test_hash()},
"outcome": "success",
"correlation_id": "01HXCORR00000000000099",
"idempotency_key": "01HXIDEM00000000000099",
"redaction": "sha256",
}
});
let parsed: CloudEvent = serde_json::from_value(blob).expect("legacy json parses");
assert_eq!(parsed.tsafeoperation, "enforce.completed");
assert_eq!(parsed.tsafeoutcome, "success");
assert!(parsed.tsafeartifacthash.is_some());
assert!(
parsed.ensure_valid().is_ok(),
"legacy cloudevent rejected: {:?}",
parsed.validation_errors()
);
}
}