use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use tsafe_core::attest_contract::AttestContract;
use tsafe_core::run_evidence::RunEvidence;
use crate::model::ScanReport;
use crate::redact;
use crate::scan as scanner;
pub fn audit(run_path: &Path, output: Option<&Path>) -> Result<String> {
let json = fs::read_to_string(run_path)
.with_context(|| format!("read run evidence: {}", run_path.display()))?;
let run: RunEvidence = serde_json::from_str(&json).context("parse run evidence")?;
run.ensure_valid()
.map_err(|errors| anyhow::anyhow!("invalid run evidence: {errors}"))?;
let contract = load_contract(run_path, &run).ok();
let contract_available = contract.is_some();
let scan = contract
.as_ref()
.and_then(|contract| load_scan(run_path, contract).ok());
let context = AuditContext {
contract_available,
scan: scan.as_ref(),
};
let report = render_markdown(&run, &context);
if let Some(output) = output {
ensure_parent_dir(output)?;
fs::write(output, &report).with_context(|| format!("write audit: {}", output.display()))?;
}
Ok(report)
}
pub fn print_report(report: &str) {
println!("{report}");
}
struct AuditContext<'a> {
contract_available: bool,
scan: Option<&'a ScanReport>,
}
#[cfg(test)]
impl AuditContext<'_> {
fn missing() -> Self {
Self {
contract_available: false,
scan: None,
}
}
}
fn render_markdown(run: &RunEvidence, context: &AuditContext<'_>) -> String {
let mut report = String::new();
let result = if run.process.exit_code == 0 {
"Passed"
} else {
"Failed"
};
report.push_str("# tsafe attest audit\n");
report.push_str("## Summary\n");
report.push_str(&format!("- Command: `{}`\n", run.command.join(" ")));
report.push_str(&format!(
"- Commit: `{}`\n",
run.repo_commit.as_deref().unwrap_or("unknown")
));
report.push_str(&format!("- Started: `{}`\n", run.started_at));
report.push_str(&format!("- Result: {result}\n"));
report.push_str(&format!(
"- Contract enforced: {}\n",
yes_no(run.result.contract_enforced)
));
report.push_str(&format!(
"- Parent environment variables: {}\n",
run.environment.parent_env_count
));
report.push_str(&format!(
"- Child environment variables: {}\n",
run.environment.child_env_count
));
report.push_str(&format!(
"- Sensitive ambient secrets denied: {}\n",
run.environment.sensitive_env_denied.len()
));
report.push_str(&format!(
"- Sensitive ambient vars inherited by child: {}\n",
sensitive_ambient_inherited_count(run)
));
report.push_str(&format!(
"- Environment authority exposure after enforcement: {}\n",
environment_authority_exposure(run)
));
report.push_str(&format!(
"- Declared secrets injected: {}\n",
run.environment.secrets_injected.len()
));
report.push_str(&format!(
"- Repo risk before enforcement: {}\n",
run.result.risk_delta.before_score
));
report.push_str(&format!(
"- Residual repo risk after env enforcement: {}\n",
run.result.risk_delta.after_score
));
report.push_str(&format!(
"- Linked contract context: {}\n",
available_unavailable(context.contract_available)
));
report.push_str(&format!(
"- Linked scan context: {}\n",
available_unavailable(context.scan.is_some())
));
report.push('\n');
report.push_str("## Declared Authority Injected\n");
report.push_str("| Name | Source | Hash | Required |\n");
report.push_str("|---|---|---|---|\n");
for item in &run.environment.secrets_injected {
report.push_str(&format!(
"| {} | {} | {} | {} |\n",
item.name,
item.source,
redact::short_hash(&item.hash),
yes_no(item.required)
));
}
report.push('\n');
report.push_str("## Ambient Authority Denied\n");
report.push_str("| Name | Hash | Reason |\n");
report.push_str("|---|---|---|\n");
for item in &run.environment.sensitive_env_denied {
report.push_str(&format!(
"| {} | {} | {} |\n",
item.name,
redact::short_hash(&item.hash),
item.reason
));
}
report.push('\n');
report.push_str("## Repo Risks Found\n");
report.push_str("| Severity | File | Line | Finding |\n");
report.push_str("|---|---:|---:|---|\n");
if let Some(scan) = context.scan {
for finding in &scan.findings {
report.push_str(&format!(
"| {} | {} | {} | {} |\n",
finding.severity.label(),
finding.file,
finding.line,
finding.message
));
}
} else {
report.push_str(
"| INFO | unavailable | 0 | Linked scan context unavailable; repo-risk findings were not loaded. |\n",
);
}
report.push('\n');
if !context.contract_available || context.scan.is_none() {
report.push_str("## Context Limitations\n");
if !context.contract_available {
report.push_str(
"- Linked contract context was unavailable; this report is limited to embedded run evidence and the contract hash reference.\n",
);
}
if context.scan.is_none() {
report.push_str(
"- Linked scan context was unavailable; repo-risk findings may be incomplete in this report.\n",
);
}
report.push('\n');
}
report.push_str("## Process Evidence\n");
report.push_str(&format!("- PID: `{}`\n", run.process.pid));
report.push_str(&format!("- Exit code: `{}`\n", run.process.exit_code));
report.push_str(&format!("- Duration: `{}ms`\n", run.process.duration_ms));
report.push_str(&format!("- CWD: `{}`\n\n", run.process.cwd));
report.push_str("## Proof Statement\n");
report.push_str(&proof_statement(run));
report
}
fn proof_statement(run: &RunEvidence) -> String {
let inherited_sensitive = sensitive_ambient_inherited_count(run);
if !run.result.contract_enforced {
return format!(
"This audit does not prove an enforced authority boundary because run evidence says contract enforcement did not occur.\nThe child process exit code was `{}`. Environment reduction claims are not made for this run.\n",
run.process.exit_code
);
}
let mut statement = String::new();
if run.process.exit_code == 0 {
statement.push_str(
"The command passed under an enforced authority boundary: the child process exited with code `0` after contract enforcement.\n",
);
} else {
statement.push_str(&format!(
"The authority boundary was enforced, but the child command failed with exit code `{}`.\n",
run.process.exit_code
));
}
if inherited_sensitive == 0 {
statement.push_str(
"Run evidence shows the child received only the safe baseline environment plus contract-declared variables.\n",
);
} else {
statement.push_str(&format!(
"Run evidence shows the safe baseline included `{inherited_sensitive}` sensitive ambient variable(s), so this audit does not prove complete sensitive ambient removal.\n",
));
}
statement
}
fn sensitive_ambient_inherited_count(run: &RunEvidence) -> usize {
run.environment
.safe_baseline_injected
.iter()
.filter(|name| scanner::is_sensitive_env_name(name))
.count()
}
fn environment_authority_exposure(run: &RunEvidence) -> &'static str {
if !run.result.contract_enforced {
return "unknown because contract was not enforced";
}
if sensitive_ambient_inherited_count(run) == 0 {
"safe baseline + contract-declared variables only"
} else {
"safe baseline included sensitive ambient variables"
}
}
fn load_contract(run_path: &Path, run: &RunEvidence) -> Result<AttestContract> {
let path = resolve_relative(run_path, Path::new(&run.contract.path));
let json =
fs::read_to_string(&path).with_context(|| format!("read contract: {}", path.display()))?;
serde_json::from_str(&json).context("parse contract")
}
fn load_scan(run_path: &Path, contract: &AttestContract) -> Result<ScanReport> {
let Some(scan_path) = &contract.source_scan else {
anyhow::bail!("contract has no source_scan");
};
let path = resolve_relative(run_path, Path::new(scan_path));
let json =
fs::read_to_string(&path).with_context(|| format!("read scan: {}", path.display()))?;
serde_json::from_str(&json).context("parse scan")
}
fn resolve_relative(anchor_file: &Path, path: &Path) -> PathBuf {
if path.is_absolute() || path.exists() {
return path.to_path_buf();
}
anchor_file
.parent()
.unwrap_or_else(|| Path::new("."))
.join(path)
}
fn yes_no(value: bool) -> &'static str {
if value {
"yes"
} else {
"no"
}
}
fn available_unavailable(value: bool) -> &'static str {
if value {
"available"
} else {
"unavailable"
}
}
fn ensure_parent_dir(path: &Path) -> Result<()> {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent)
.with_context(|| format!("create output directory: {}", parent.display()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{ScanReport, ScanSummary, SCAN_SCHEMA};
use tsafe_core::run_evidence::{
blake3_hash, ContractRef, DeniedSensitiveEnvEvidence, EnforcementResult,
EnvironmentEvidence, InjectedSecretEvidence, MachineEvidence, ProcessEvidence, RiskDelta,
RunEvidence, RUN_EVIDENCE_VERSION, RUN_SCHEMA,
};
fn sample_run() -> RunEvidence {
RunEvidence {
schema: RUN_SCHEMA.to_string(),
tsafe_attest_version: RUN_EVIDENCE_VERSION.to_string(),
started_at: chrono::Utc::now(),
finished_at: chrono::Utc::now(),
repo_path: ".".to_string(),
repo_commit: Some("abc123".to_string()),
command: vec!["npm".to_string(), "test".to_string()],
contract: ContractRef {
path: "tsafe.contract.json".to_string(),
hash: blake3_hash("contract"),
},
environment: EnvironmentEvidence {
parent_env_count: 81,
child_env_count: 7,
removed_env_count: 74,
safe_baseline_injected: vec!["PATH".to_string()],
secrets_injected: vec![InjectedSecretEvidence {
name: "DATABASE_URL".to_string(),
source: "literal://demo/DATABASE_URL".to_string(),
hash: blake3_hash("postgres://secret"),
redacted_value: redact::redacted("postgres://secret"),
required: true,
}],
sensitive_env_denied: vec![DeniedSensitiveEnvEvidence {
name: "AWS_SECRET_ACCESS_KEY".to_string(),
hash: blake3_hash("raw_fake_secret"),
reason: "Not declared in contract".to_string(),
}],
},
process: ProcessEvidence {
pid: 1,
exit_code: 0,
duration_ms: 10,
cwd: ".".to_string(),
},
machine: MachineEvidence {
hostname_hash: blake3_hash("host"),
username_hash: blake3_hash("user"),
os: "darwin".to_string(),
arch: "arm64".to_string(),
},
result: EnforcementResult {
contract_enforced: true,
violations: vec![],
risk_delta: RiskDelta {
before_score: 72,
after_score: 18,
},
},
signature: None,
}
}
#[test]
fn renders_markdown_with_proof_statement_and_no_raw_secret() {
let run = sample_run();
let rendered = render_markdown(&run, &AuditContext::missing());
assert!(rendered.contains("# tsafe attest audit"));
assert!(rendered.contains("Repo risk before enforcement: 72"));
assert!(rendered.contains("Residual repo risk after env enforcement: 18"));
assert!(rendered.contains("Linked contract context: unavailable"));
assert!(rendered.contains("Linked scan context: unavailable"));
assert!(rendered.contains("## Context Limitations"));
assert!(rendered.contains("Linked contract context was unavailable"));
assert!(rendered.contains("Linked scan context was unavailable"));
assert!(rendered.contains("Sensitive ambient vars inherited by child: 0"));
assert!(rendered.contains(
"Environment authority exposure after enforcement: safe baseline + contract-declared variables only"
));
assert!(rendered.contains("The command passed under an enforced authority boundary"));
assert!(rendered.contains("AWS_SECRET_ACCESS_KEY"));
assert!(!rendered.contains("raw_fake_secret"));
}
#[test]
fn failed_child_process_keeps_boundary_claim_conditional() {
let mut run = sample_run();
run.process.exit_code = 2;
let rendered = render_markdown(&run, &AuditContext::missing());
assert!(rendered.contains("Result: Failed"));
assert!(rendered.contains(
"The authority boundary was enforced, but the child command failed with exit code `2`."
));
assert!(!rendered.contains("The command passed under an enforced authority boundary"));
assert!(!rendered.contains("raw_fake_secret"));
}
#[test]
fn non_enforced_contract_does_not_use_successful_proof_language() {
let mut run = sample_run();
run.result.contract_enforced = false;
let rendered = render_markdown(&run, &AuditContext::missing());
assert!(rendered.contains("Contract enforced: no"));
assert!(rendered.contains(
"This audit does not prove an enforced authority boundary because run evidence says contract enforcement did not occur."
));
assert!(rendered.contains("Environment reduction claims are not made for this run."));
assert!(!rendered.contains("The command passed under an enforced authority boundary"));
assert!(!rendered.contains("safe baseline environment plus contract-declared variables"));
assert!(!rendered.contains("raw_fake_secret"));
}
#[test]
fn sensitive_safe_baseline_downgrades_removal_claim() {
let mut run = sample_run();
run.environment
.safe_baseline_injected
.push("AWS_ACCESS_KEY_ID".to_string());
let rendered = render_markdown(&run, &AuditContext::missing());
assert!(rendered.contains("Sensitive ambient vars inherited by child: 1"));
assert!(rendered.contains("this audit does not prove complete sensitive ambient removal"));
assert!(!rendered.contains("raw_fake_secret"));
}
#[test]
fn available_linked_context_avoids_missing_context_warning() {
let run = sample_run();
let scan = ScanReport {
schema: SCAN_SCHEMA.to_string(),
repo_path: ".".to_string(),
repo_commit: None,
scanned_at: chrono::Utc::now(),
scanner_version: "test".to_string(),
findings: vec![],
observed_env_reads: vec![],
ci_secret_references: vec![],
summary: ScanSummary::default(),
};
let context = AuditContext {
contract_available: true,
scan: Some(&scan),
};
let rendered = render_markdown(&run, &context);
assert!(rendered.contains("Linked contract context: available"));
assert!(rendered.contains("Linked scan context: available"));
assert!(!rendered.contains("## Context Limitations"));
assert!(!rendered.contains("repo-risk findings were not loaded"));
}
#[test]
fn linked_contract_without_scan_reports_only_scan_limitation() {
let run = sample_run();
let context = AuditContext {
contract_available: true,
scan: None,
};
let rendered = render_markdown(&run, &context);
assert!(rendered.contains("## Context Limitations"));
assert!(!rendered.contains("Linked contract context was unavailable"));
assert!(rendered.contains("Linked scan context was unavailable"));
}
}