use tsafe_core::{
audit::AuditEntry,
events::{key_ref, CloudEvent},
};
macro_rules! schema_str {
($path:literal) => {
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../", $path))
};
}
fn validator_for_schema(schema_text: &str) -> jsonschema::Validator {
let schema_value: serde_json::Value =
serde_json::from_str(schema_text).expect("schema file must be valid JSON");
jsonschema::validator_for(&schema_value).expect("schema must be a valid JSON Schema")
}
fn assert_validates(validator: &jsonschema::Validator, event: &CloudEvent, label: &str) {
let value = serde_json::to_value(event).expect("CloudEvent must serialise to JSON");
let errors: Vec<String> = validator
.iter_errors(&value)
.map(|e| format!("{e}"))
.collect();
assert!(
errors.is_empty(),
"{label}: schema validation failed with {} error(s):\n{}",
errors.len(),
errors.join("\n")
);
}
#[test]
fn secret_set_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/secret/secret-event.v1.schema.json"
));
let entry = AuditEntry::success("prod", "set", Some("DB_PASSWORD"));
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "secret set");
}
#[test]
fn secret_get_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/secret/secret-event.v1.schema.json"
));
let entry = AuditEntry::success("dev", "get", Some("API_KEY"));
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "secret get");
}
#[test]
fn secret_delete_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/secret/secret-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "delete", Some("OLD_TOKEN"));
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "secret delete");
}
#[test]
fn secret_rotation_due_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/secret/secret-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "rotate-due", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "secret rotation_due");
}
#[test]
fn session_unlocked_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/session/session-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "unlock", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "session unlocked");
}
#[test]
fn vault_created_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/vault/vault-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "init", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "vault created (init)");
}
#[test]
fn vault_rotated_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/vault/vault-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "rotate", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "vault rotated");
}
#[test]
fn vault_exported_event_validates_against_vault_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/vault/vault-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "export", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "vault exported");
}
#[test]
fn vault_exec_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/vault/vault-event.v1.schema.json"
));
let entry = AuditEntry::success("work", "exec", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "vault exec (no authority)");
}
#[test]
fn sync_pull_completed_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/sync/sync-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "kv-pull", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "sync pull completed (kv-pull)");
}
#[test]
fn sync_vault_pull_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/sync/sync-event.v1.schema.json"
));
let entry = AuditEntry::success("work", "vault-pull", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "sync pull completed (vault-pull)");
}
#[test]
fn share_published_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/share/share-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "share-once", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "share published");
}
#[test]
fn share_consumed_event_validates_against_schema() {
let validator = validator_for_schema(schema_str!(
"contracts/events/share/share-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "receive-once", None);
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "share consumed");
}
#[test]
fn browser_event_gap_hand_constructed_payload_validates() {
let validator = validator_for_schema(schema_str!(
"contracts/events/browser/browser-event.v1.schema.json"
));
let data = serde_json::json!({
"audit_id": "00000000-0000-4000-8000-000000000001",
"operation": "browser-phishing-blocked",
"key_ref": null,
"status": "success",
"message": null
});
let event = CloudEvent::new(
"tsafe/browser",
"com.tsafe.browser.phishing_blocked.v1",
data,
);
assert_validates(
&validator,
&event,
"browser phishing_blocked (hand-constructed)",
);
}
#[test]
fn authority_exec_shape_matches_runtime() {
use tsafe_core::{
audit::{AuditContext, AuditExecContext},
contracts::{AuthorityContract, AuthorityNetworkPolicy, AuthorityTrust},
rbac::RbacProfile,
};
let validator = validator_for_schema(schema_str!(
"contracts/events/vault/vault-event.v1.schema.json"
));
let contract = AuthorityContract {
name: "deploy".into(),
profile: Some("main".into()),
namespace: Some("infra".into()),
access_profile: RbacProfile::ReadOnly,
allowed_secrets: vec!["API_KEY".into(), "DB_PASSWORD".into()],
required_secrets: vec!["DB_PASSWORD".into()],
allowed_targets: vec!["deploy.sh".into()],
trust: AuthorityTrust::Hardened,
network: AuthorityNetworkPolicy::Restricted,
};
let entry = AuditEntry::success("main", "exec", None).with_context(AuditContext::from_exec(
AuditExecContext::from_contract(&contract)
.with_target("/scripts/deploy.sh")
.with_injected_secrets(["DB_PASSWORD"])
.with_missing_required_secrets(["API_KEY"])
.with_dropped_env_names(["OPENAI_API_KEY"])
.with_target_evaluation(&contract.evaluate_target(Some("/scripts/deploy.sh"))),
));
let event = CloudEvent::from_audit(&entry);
let exec = &event.data["authority"]["exec"];
assert!(!exec.is_null(), "authority.exec must be present");
assert_eq!(exec["contract_name"], "deploy");
assert_eq!(exec["trust_level"], "hardened");
assert_eq!(exec["network"], "restricted");
assert_validates(&validator, &event, "vault exec with authority (C7-fix)");
}
#[test]
fn no_plaintext_secret_in_serialised_events_all_families() {
let sensitive_key = "PROD_STRIPE_SECRET_KEY";
let sensitive_value = "sk_live_super_secret_plaintext_value_9999";
let families: &[(&str, &str, Option<&str>)] = &[
("secret", "set", Some(sensitive_key)),
("secret", "get", Some(sensitive_key)),
("secret", "delete", Some(sensitive_key)),
("session", "unlock", None),
("vault", "init", None),
("sync", "kv-pull", None),
("share", "share-once", None),
];
for (family, operation, key) in families {
let entry = AuditEntry::success("prod", operation, *key);
let event = CloudEvent::from_audit(&entry);
let serialised = serde_json::to_string(&event)
.unwrap_or_else(|e| panic!("{family}/{operation}: serialise failed: {e}"));
assert!(
!serialised.contains(sensitive_key),
"{family}/{operation}: plaintext key name '{sensitive_key}' leaked into event JSON"
);
assert!(
!serialised.contains(sensitive_value),
"{family}/{operation}: plaintext secret value leaked into event JSON"
);
if let Some(k) = key {
let v: serde_json::Value = serde_json::from_str(&serialised).unwrap();
let kr = v["data"]["key_ref"].as_str().unwrap_or_else(|| {
panic!("{family}/{operation}: key_ref must be a string when key is present")
});
assert_eq!(
kr.len(),
64,
"{family}/{operation}: key_ref must be 64-char SHA-256 hex"
);
assert_eq!(
kr,
key_ref("prod", k),
"{family}/{operation}: key_ref must match SHA-256(profile:key)"
);
}
}
}
#[test]
fn key_ref_is_opaque_in_schema_validated_event() {
let validator = validator_for_schema(schema_str!(
"contracts/events/secret/secret-event.v1.schema.json"
));
let entry = AuditEntry::success("main", "set", Some("GITHUB_TOKEN"));
let event = CloudEvent::from_audit(&entry);
assert_validates(&validator, &event, "key_ref opaqueness check");
let serialised = serde_json::to_string(&event).unwrap();
assert!(
!serialised.contains("GITHUB_TOKEN"),
"plaintext key name must not appear anywhere in a schema-validated event"
);
let value: serde_json::Value = serde_json::from_str(&serialised).unwrap();
let kr = value["data"]["key_ref"].as_str().unwrap();
assert_eq!(kr, key_ref("main", "GITHUB_TOKEN"));
assert_eq!(kr.len(), 64);
}