pub mod attest;
pub mod cyclonedx_mlbom_model;
pub mod diff;
pub mod lint;
pub mod list;
pub mod livekit_tool_action;
pub mod mapping;
pub mod mastra_score_event;
pub mod mcp_execution_records;
pub mod mcp_supersession;
pub mod mcp_tunnel_observed;
pub mod openfeature_details;
pub mod promptfoo_jsonl;
pub mod pull;
pub mod push;
pub mod pydantic_case_result;
pub mod schema;
pub mod store_status;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use mapping::{DetailLevel, EvidenceMapper};
use std::fs::File;
use std::io::{self, Read};
#[derive(Debug, Subcommand, Clone)]
pub enum EvidenceCmd {
Export(EvidenceExportArgs),
Verify(EvidenceVerifyArgs),
#[command(name = "verify-mcp-records")]
VerifyMcpRecords(mcp_execution_records::McpExecutionRecordArgs),
#[command(name = "verify-mcp-supersession")]
VerifyMcpSupersession(mcp_supersession::McpSupersessionArgs),
#[command(name = "verify-mcp-tunnel-observed")]
VerifyMcpTunnelObserved(mcp_tunnel_observed::McpTunnelObservedArgs),
Show(EvidenceShowArgs),
Import(EvidenceImportArgs),
Schema(schema::SchemaArgs),
Lint(lint::LintArgs),
Diff(diff::DiffArgs),
Attest(attest::AttestArgs),
Push(push::PushArgs),
Pull(pull::PullArgs),
List(list::ListArgs),
StoreStatus(store_status::StoreStatusArgs),
#[cfg(feature = "tui")]
Explore(explore::ExploreArgs),
}
#[derive(Debug, Args, Clone)]
pub struct EvidenceExportArgs {
#[arg(long, alias = "input")]
pub profile: std::path::PathBuf,
#[arg(long, short = 'o')]
pub out: Option<std::path::PathBuf>,
#[arg(long, value_enum, default_value_t = DetailLevel::Observed)]
pub detail: DetailLevel,
}
#[derive(Debug, Args, Clone)]
pub struct EvidenceVerifyArgs {
#[arg(value_name = "BUNDLE", default_value = "-")]
pub bundle: std::path::PathBuf,
}
#[derive(Debug, Args, Clone)]
pub struct EvidenceShowArgs {
#[arg(value_name = "BUNDLE")]
pub bundle: std::path::PathBuf,
#[arg(long)]
pub no_verify: bool,
#[arg(long, default_value = "table")]
pub format: String,
}
#[derive(Debug, Args, Clone)]
pub struct EvidenceImportArgs {
#[command(subcommand)]
pub cmd: EvidenceImportCmd,
}
#[derive(Debug, Subcommand, Clone)]
pub enum EvidenceImportCmd {
#[command(name = "cyclonedx-mlbom-model")]
CycloneDxMlBomModel(cyclonedx_mlbom_model::CycloneDxMlBomModelArgs),
#[command(name = "openfeature-details")]
OpenFeatureDetails(openfeature_details::OpenFeatureDetailsArgs),
#[command(name = "mastra-score-event")]
MastraScoreEvent(mastra_score_event::MastraScoreEventArgs),
#[command(name = "pydantic-case-result")]
PydanticCaseResult(pydantic_case_result::PydanticCaseResultArgs),
#[command(name = "livekit-tool-action")]
LiveKitToolAction(livekit_tool_action::LiveKitToolActionArgs),
#[command(name = "promptfoo-jsonl")]
PromptfooJsonl(promptfoo_jsonl::PromptfooJsonlArgs),
}
pub async fn run(args: crate::cli::args::EvidenceArgs) -> Result<i32> {
match args.cmd {
EvidenceCmd::Export(a) => cmd_export(a),
EvidenceCmd::Verify(a) => cmd_verify(a),
EvidenceCmd::VerifyMcpRecords(a) => mcp_execution_records::cmd_verify_mcp_records(a),
EvidenceCmd::VerifyMcpSupersession(a) => mcp_supersession::cmd_verify_mcp_supersession(a),
EvidenceCmd::VerifyMcpTunnelObserved(a) => {
mcp_tunnel_observed::cmd_verify_mcp_tunnel_observed(a)
}
EvidenceCmd::Show(a) => cmd_show(a),
EvidenceCmd::Import(a) => cmd_import(a),
EvidenceCmd::Schema(a) => schema::cmd_schema(a),
EvidenceCmd::Lint(a) => lint::cmd_lint(a),
EvidenceCmd::Diff(a) => diff::cmd_diff(a),
EvidenceCmd::Attest(a) => attest::cmd_attest(a),
EvidenceCmd::Push(a) => push::cmd_push(a).await,
EvidenceCmd::Pull(a) => pull::cmd_pull(a).await,
EvidenceCmd::List(a) => list::cmd_list(a).await,
EvidenceCmd::StoreStatus(a) => store_status::cmd_store_status(a).await,
#[cfg(feature = "tui")]
EvidenceCmd::Explore(a) => explore::cmd_explore(a),
}
}
fn cmd_import(args: EvidenceImportArgs) -> Result<i32> {
match args.cmd {
EvidenceImportCmd::CycloneDxMlBomModel(a) => {
cyclonedx_mlbom_model::cmd_cyclonedx_mlbom_model(a)
}
EvidenceImportCmd::OpenFeatureDetails(a) => openfeature_details::cmd_openfeature_details(a),
EvidenceImportCmd::MastraScoreEvent(a) => mastra_score_event::cmd_mastra_score_event(a),
EvidenceImportCmd::PydanticCaseResult(a) => {
pydantic_case_result::cmd_pydantic_case_result(a)
}
EvidenceImportCmd::LiveKitToolAction(a) => livekit_tool_action::cmd_livekit_tool_action(a),
EvidenceImportCmd::PromptfooJsonl(a) => promptfoo_jsonl::cmd_promptfoo_jsonl(a),
}
}
fn cmd_export(args: EvidenceExportArgs) -> Result<i32> {
let profile = crate::cli::commands::profile_types::load_profile(&args.profile)
.with_context(|| format!("failed to load profile from {}", args.profile.display()))?;
let run_id_opt = profile.run_ids.back().cloned();
let mut mapper = EvidenceMapper::new(run_id_opt, &profile.name);
let events = mapper.map_profile(&profile, args.detail)?;
let run_id = mapper.run_id().to_string();
let out_path = args
.out
.unwrap_or_else(|| std::path::PathBuf::from(format!("assay_evidence_{}.tar.gz", run_id)));
let out_file = File::create(&out_path)
.with_context(|| format!("failed to create output file {}", out_path.display()))?;
let mut bw = assay_evidence::bundle::BundleWriter::new(out_file);
for ev in events {
bw.add_event(ev);
}
bw.finish().context("failed to finalize evidence bundle")?;
eprintln!("Exported evidence bundle to {}", out_path.display());
Ok(0)
}
fn cmd_verify(args: EvidenceVerifyArgs) -> Result<i32> {
if args.bundle.to_string_lossy() == "-" {
let mut buf = Vec::new();
io::stdin().read_to_end(&mut buf)?;
assay_evidence::bundle::verify_bundle(io::Cursor::new(buf))
.context("bundle verification failed")?;
eprintln!("Bundle verified (stdin): OK");
return Ok(0);
}
let f = File::open(&args.bundle)
.with_context(|| format!("failed to open bundle {}", args.bundle.display()))?;
let _ = assay_evidence::bundle::BundleReader::open(f)?;
eprintln!("Bundle verified ({}): OK", args.bundle.display());
Ok(0)
}
fn cmd_show(args: EvidenceShowArgs) -> Result<i32> {
let f = File::open(&args.bundle)
.with_context(|| format!("failed to open bundle {}", args.bundle.display()))?;
let br = if args.no_verify {
assay_evidence::bundle::BundleReader::open_unverified(f)
} else {
assay_evidence::bundle::BundleReader::open(f)
}
.context("failed to open bundle reader")?;
let verified = !args.no_verify; let manifest = br.manifest();
if args.format == "json" {
let events = br.events().collect::<Result<Vec<_>>>()?;
let bundle_json = serde_json::json!({
"manifest": manifest,
"events": events,
});
println!("{}", serde_json::to_string_pretty(&bundle_json)?);
return Ok(0);
}
println!("Evidence Bundle Inspector");
println!("=========================");
if !args.no_verify {
if verified {
println!("Verified: ✅ OK");
} else {
println!("Verified: ❌ FAILED (Integrity compromised)");
}
} else {
println!("Verified: ⚠️ SKIPPED");
}
println!("Bundle ID: {}", manifest.bundle_id);
println!(
"Producer: {} v{}",
manifest.producer.name, manifest.producer.version
);
println!("Run ID: {}", manifest.run_id);
println!("Events: {}", manifest.event_count);
let run_root_display: String = manifest.run_root.chars().take(16).collect();
println!("Run Root: {}...", run_root_display);
println!();
println!("{:<4} {:<25} {:<30} SUBJECT", "SEQ", "TIME", "TYPE");
println!("{:-<4} {:-<25} {:-<30} {:-<20}", "", "", "", "");
for ev_res in br.events() {
let ev = ev_res?;
let subject = ev.subject.as_deref().unwrap_or("-");
let time_str = ev.time.to_rfc3339();
let time_short = if time_str.len() > 19 {
time_str.chars().skip(11).take(8).collect::<String>()
} else {
time_str.clone()
};
println!(
"{:<4} {:<25} {:<30} {}",
ev.seq, time_short, ev.type_, subject
);
}
if !args.no_verify {
println!("\n✅ Verified Integrity");
} else {
println!("\n⚠️ Verification Skipped");
}
Ok(0)
}
#[cfg(feature = "tui")]
pub mod explore;