use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub const ATTEST_CONTRACT_SCHEMA: &str = "tsafe.contract.v1";
pub const LEGACY_ATTEST_CONTRACT_SCHEMA: &str = "algol.contract.v1";
pub const REDACTION_BLAKE3: &str = "blake3";
pub const REDACTION_SHA256_LEGACY: &str = "sha256";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestContractPolicy {
pub default_env: String,
pub allow_safe_baseline: bool,
pub redaction: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestAllowedEnv {
pub name: String,
pub source: String,
pub required: bool,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestDeniedEnv {
pub name: String,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestContract {
pub schema: String,
pub repo_path: String,
pub repo_commit: Option<String>,
pub created_at: DateTime<Utc>,
pub created_by: String,
pub command: Vec<String>,
pub policy: AttestContractPolicy,
pub allowed_env: Vec<AttestAllowedEnv>,
pub denied_env: Vec<AttestDeniedEnv>,
pub safe_baseline_env: Vec<String>,
pub source_scan: Option<String>,
}
impl AttestContract {
pub fn validation_errors(&self) -> Vec<String> {
let mut errors = Vec::new();
if !is_supported_contract_schema(&self.schema) {
errors.push(format!("unsupported schema {}", self.schema));
}
if self.repo_path.trim().is_empty() {
errors.push("repo_path must not be empty".to_string());
}
if self.command.is_empty() || self.command.iter().any(|part| part.trim().is_empty()) {
errors.push("command must contain non-empty argv entries".to_string());
}
if self.policy.default_env != "deny" {
errors.push(format!(
"unsupported policy.default_env {}; MVP requires deny",
self.policy.default_env
));
}
if self.policy.redaction != REDACTION_BLAKE3
&& self.policy.redaction != REDACTION_SHA256_LEGACY
{
errors.push(format!(
"unsupported policy.redaction {}; expected blake3 (or sha256 during compat window)",
self.policy.redaction
));
}
validate_allowed_env(&self.allowed_env, &mut errors);
validate_denied_env(&self.denied_env, &mut errors);
validate_safe_baseline(&self.safe_baseline_env, &mut errors);
errors
}
pub fn ensure_valid(&self) -> Result<(), String> {
let errors = self.validation_errors();
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("; "))
}
}
}
pub fn is_supported_contract_schema(schema: &str) -> bool {
schema == ATTEST_CONTRACT_SCHEMA || schema == LEGACY_ATTEST_CONTRACT_SCHEMA
}
pub fn is_supported_source_uri(source: &str) -> bool {
source.starts_with("literal://demo/")
|| source
.strip_prefix("env://")
.is_some_and(|name| !name.trim().is_empty())
}
fn validate_allowed_env(allowed_env: &[AttestAllowedEnv], errors: &mut Vec<String>) {
let mut names = BTreeSet::new();
for item in allowed_env {
if item.name.trim().is_empty() {
errors.push("allowed_env name must not be empty".to_string());
}
if !names.insert(item.name.clone()) {
errors.push(format!("duplicate allowed_env {}", item.name));
}
if !is_supported_source_uri(&item.source) {
errors.push(format!(
"unsupported source for {}: {}; MVP supports literal://demo/* and env://*",
item.name, item.source
));
}
if item.reason.trim().is_empty() {
errors.push(format!(
"allowed_env {} reason must not be empty",
item.name
));
}
}
}
fn validate_denied_env(denied_env: &[AttestDeniedEnv], errors: &mut Vec<String>) {
let mut names = BTreeSet::new();
for item in denied_env {
if item.name.trim().is_empty() {
errors.push("denied_env name must not be empty".to_string());
}
if !names.insert(item.name.clone()) {
errors.push(format!("duplicate denied_env {}", item.name));
}
if item.reason.trim().is_empty() {
errors.push(format!("denied_env {} reason must not be empty", item.name));
}
}
}
fn validate_safe_baseline(safe_baseline_env: &[String], errors: &mut Vec<String>) {
let mut names = BTreeSet::new();
for name in safe_baseline_env {
if name.trim().is_empty() {
errors.push("safe_baseline_env name must not be empty".to_string());
}
if !names.insert(name.clone()) {
errors.push(format!("duplicate safe_baseline_env {name}"));
}
if is_sensitive_baseline_name(name) {
errors.push(format!(
"safe_baseline_env {name} is sensitive and cannot be baseline-inherited"
));
}
}
}
fn is_sensitive_baseline_name(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
upper.contains("SECRET")
|| upper.contains("TOKEN")
|| upper.contains("CREDENTIAL")
|| upper.contains("CREDS")
|| upper.contains("PASSWORD")
|| upper.contains("PASSWD")
|| upper.contains("PRIVATE_KEY")
|| upper.ends_with("_KEY")
|| upper.ends_with("_PWD")
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_contract() -> AttestContract {
AttestContract {
schema: ATTEST_CONTRACT_SCHEMA.to_string(),
repo_path: ".".to_string(),
repo_commit: None,
created_at: Utc::now(),
created_by: "model-test".to_string(),
command: vec!["true".to_string()],
policy: AttestContractPolicy {
default_env: "deny".to_string(),
allow_safe_baseline: true,
redaction: REDACTION_BLAKE3.to_string(),
},
allowed_env: vec![AttestAllowedEnv {
name: "DATABASE_URL".to_string(),
source: "literal://demo/DATABASE_URL".to_string(),
required: true,
reason: "test".to_string(),
}],
denied_env: vec![AttestDeniedEnv {
name: "AWS_SECRET_ACCESS_KEY".to_string(),
reason: "test".to_string(),
}],
safe_baseline_env: vec!["PATH".to_string()],
source_scan: None,
}
}
#[test]
fn contract_rejects_empty_and_blank_command_entries() {
let mut empty = valid_contract();
empty.command.clear();
assert!(empty
.validation_errors()
.iter()
.any(|error| error.contains("command must contain")));
let mut blank = valid_contract();
blank.command = vec!["true".to_string(), " ".to_string()];
assert!(blank
.validation_errors()
.iter()
.any(|error| error.contains("command must contain")));
}
#[test]
fn contract_rejects_invalid_denied_env_entries() {
let mut contract = valid_contract();
contract.denied_env = vec![
AttestDeniedEnv {
name: "".to_string(),
reason: "test".to_string(),
},
AttestDeniedEnv {
name: "GH_TOKEN".to_string(),
reason: "".to_string(),
},
AttestDeniedEnv {
name: "GH_TOKEN".to_string(),
reason: "duplicate".to_string(),
},
];
let errors = contract.validation_errors().join("; ");
assert!(errors.contains("denied_env name must not be empty"));
assert!(errors.contains("denied_env GH_TOKEN reason must not be empty"));
assert!(errors.contains("duplicate denied_env GH_TOKEN"));
}
#[test]
fn contract_rejects_each_sensitive_baseline_name_pattern() {
for name in [
"APP_SECRET",
"API_TOKEN",
"DATABASE_PASSWORD",
"DB_PASSWD",
"SSH_PRIVATE_KEY",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_GHA_CREDS_PATH",
"AWS_ACCESS_KEY",
"DB_PWD",
] {
let mut contract = valid_contract();
contract.safe_baseline_env = vec![name.to_string()];
let errors = contract.validation_errors().join("; ");
assert!(
errors.contains("is sensitive and cannot be baseline-inherited"),
"{name} was not rejected: {errors}"
);
}
}
#[test]
fn contract_rejects_unsupported_source_uri() {
let mut contract = valid_contract();
contract.allowed_env = vec![AttestAllowedEnv {
name: "DATABASE_URL".to_string(),
source: "https://example.com/secrets/db".to_string(),
required: true,
reason: "test".to_string(),
}];
let errors = contract.validation_errors().join("; ");
assert!(errors.contains("unsupported source for DATABASE_URL"));
}
#[test]
fn contract_rejects_unsupported_default_env_policy() {
let mut contract = valid_contract();
contract.policy.default_env = "allow".to_string();
let errors = contract.validation_errors().join("; ");
assert!(errors.contains("unsupported policy.default_env allow"));
}
#[test]
fn contract_accepts_a_well_formed_artifact() {
let contract = valid_contract();
assert!(
contract.ensure_valid().is_ok(),
"valid_contract() should pass validation: {:?}",
contract.validation_errors()
);
}
#[test]
fn contract_accepts_legacy_schema_and_redaction_during_compat() {
let mut contract = valid_contract();
contract.schema = LEGACY_ATTEST_CONTRACT_SCHEMA.to_string();
contract.policy.redaction = REDACTION_SHA256_LEGACY.to_string();
assert!(
contract.ensure_valid().is_ok(),
"legacy schema + sha256 redaction must remain valid: {:?}",
contract.validation_errors()
);
}
#[test]
fn is_supported_source_uri_accepts_mvp_schemes_and_rejects_others() {
assert!(is_supported_source_uri("literal://demo/FOO"));
assert!(is_supported_source_uri("env://FOO"));
assert!(!is_supported_source_uri("env://"));
assert!(!is_supported_source_uri("env:// "));
assert!(!is_supported_source_uri("https://example.com"));
assert!(!is_supported_source_uri("vault://path"));
}
}