1use anyhow::{Context, Result, anyhow};
10use chrono::{DateTime, Utc};
11use clap::{Parser, Subcommand, ValueEnum};
12use std::fs;
13use std::path::PathBuf;
14
15use auths_crypto::CurveType;
16use auths_sdk::context::AuthsContext;
17use auths_sdk::keychain::{KeyAlias, extract_public_key_bytes};
18use auths_sdk::storage_layout::layout;
19use auths_sdk::workflows::compliance::{
20 ComplianceFramework, ReleaseRecord, TransparencyInclusion, VsaParams, build_evidence_pack,
21 build_framework_report, build_offline_evidence_pack, load_witness_policy, sign_evidence_pack,
22 sign_framework_report,
23};
24use auths_verifier::{IdentityDID, Prefix};
25
26use crate::commands::executable::ExecutableCommand;
27use crate::config::CliConfig;
28use crate::factories::storage::build_auths_context;
29
30fn org_slug_alias(org: &str) -> String {
33 format!(
34 "org-{}",
35 org.chars()
36 .filter(|c| c.is_alphanumeric())
37 .take(20)
38 .collect::<String>()
39 .to_lowercase()
40 )
41}
42
43fn resolve_org_signing(
45 sdk_ctx: &AuthsContext,
46 org: &str,
47 key: Option<String>,
48) -> Result<(KeyAlias, CurveType)> {
49 let org_alias = KeyAlias::new_unchecked(key.unwrap_or_else(|| org_slug_alias(org)));
50 let (_pk, curve) = extract_public_key_bytes(
51 sdk_ctx.key_storage.as_ref(),
52 &org_alias,
53 sdk_ctx.passphrase_provider.as_ref(),
54 )
55 .with_context(|| format!("Failed to resolve org signing key '{org_alias}'"))?;
56 Ok((org_alias, curve))
57}
58
59#[derive(Debug, Clone, serde::Deserialize)]
62struct ReleaseInput {
63 artifact_digest: String,
64 signer: String,
65 #[serde(default)]
66 signed_at: Option<u128>,
67 #[serde(default)]
68 transparency: Option<TransparencyInclusion>,
69}
70
71impl ReleaseInput {
72 fn into_record(self) -> ReleaseRecord {
73 let signer_prefix = Prefix::new_unchecked(
74 self.signer
75 .strip_prefix("did:keri:")
76 .unwrap_or(&self.signer)
77 .to_string(),
78 );
79 ReleaseRecord {
80 artifact_digest: self.artifact_digest,
81 signer_prefix,
82 signed_at: self.signed_at,
83 transparency: self.transparency,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, ValueEnum)]
90pub enum CliFramework {
91 Slsa,
93 Sbom,
95 Cra,
97}
98
99impl From<CliFramework> for ComplianceFramework {
100 fn from(f: CliFramework) -> Self {
101 match f {
102 CliFramework::Slsa => ComplianceFramework::Slsa,
103 CliFramework::Sbom => ComplianceFramework::Sbom,
104 CliFramework::Cra => ComplianceFramework::Cra,
105 }
106 }
107}
108
109#[derive(Parser, Debug, Clone)]
111#[command(
112 about = "Compliance as a query — offline-verifiable evidence packs",
113 after_help = "Examples:
114 auths compliance report --org did:keri:EOrg --period 2026-Q3 \\
115 --framework slsa --releases releases.json --offline --out acme-2026Q3.evidence
116 # Build an offline-verifiable evidence pack
117
118Releases file (JSON array):
119 [{\"artifact_digest\":\"sha256:…\",\"signer\":\"did:keri:EMember\",\"signed_at\":41}]"
120)]
121pub struct ComplianceCommand {
122 #[clap(subcommand)]
123 pub subcommand: ComplianceSubcommand,
124}
125
126#[derive(Subcommand, Debug, Clone)]
128pub enum ComplianceSubcommand {
129 Report {
131 #[arg(long)]
133 org: String,
134
135 #[arg(long)]
137 period: String,
138
139 #[arg(long, value_enum, default_value = "slsa")]
142 framework: CliFramework,
143
144 #[arg(long)]
146 predicate: bool,
147
148 #[arg(long, default_value = "https://auths.dev/compliance")]
150 verifier_id: String,
151
152 #[arg(long)]
154 releases: PathBuf,
155
156 #[arg(long)]
158 offline: bool,
159
160 #[arg(long)]
162 sign: bool,
163
164 #[arg(long)]
166 key: Option<String>,
167
168 #[arg(long)]
170 witness_policy: Option<PathBuf>,
171
172 #[arg(long)]
174 out: Option<PathBuf>,
175 },
176}
177
178pub fn handle_compliance(
190 cmd: ComplianceCommand,
191 ctx: &CliConfig,
192 now: DateTime<Utc>,
193) -> Result<()> {
194 match cmd.subcommand {
195 ComplianceSubcommand::Report {
196 org,
197 period,
198 framework,
199 predicate,
200 verifier_id,
201 releases,
202 offline,
203 sign,
204 key,
205 witness_policy,
206 out,
207 } => {
208 let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
209 let passphrase_provider = ctx.passphrase_provider.clone();
210
211 let org_prefix =
212 Prefix::new_unchecked(org.strip_prefix("did:keri:").unwrap_or(&org).to_string());
213 let org_did = IdentityDID::from_prefix(org_prefix.as_str())
214 .map_err(|e| anyhow!("invalid org identifier '{org}': {e}"))?;
215
216 let raw = fs::read_to_string(&releases)
217 .with_context(|| format!("Failed to read releases file {releases:?}"))?;
218 let inputs: Vec<ReleaseInput> = serde_json::from_str(&raw)
219 .with_context(|| format!("Invalid JSON in releases file {releases:?}"))?;
220 let records: Vec<ReleaseRecord> =
221 inputs.into_iter().map(ReleaseInput::into_record).collect();
222
223 let policy_path = witness_policy.or_else(|| {
224 std::env::var("AUTHS_WITNESS_POLICY_PATH")
225 .ok()
226 .map(PathBuf::from)
227 });
228 let policy_result = load_witness_policy(policy_path.as_deref());
229
230 let sdk_ctx = build_auths_context(
231 &repo_path,
232 &ctx.env_config,
233 Some(passphrase_provider.clone()),
234 )?;
235 let framework = ComplianceFramework::from(framework);
236
237 let pack = if offline {
238 build_offline_evidence_pack(
239 &sdk_ctx,
240 org_did.clone(),
241 &org_prefix,
242 period,
243 framework,
244 &records,
245 &policy_result,
246 now,
247 )
248 } else {
249 build_evidence_pack(
250 &sdk_ctx,
251 org_did.clone(),
252 &org_prefix,
253 period,
254 framework,
255 &records,
256 &policy_result,
257 now,
258 )
259 }
260 .context("Failed to build compliance evidence pack")?;
261
262 let output = if predicate {
263 let vsa = VsaParams {
264 verifier_id: verifier_id.clone(),
265 time_verified: now,
266 allow_list: Default::default(),
267 };
268 let report = build_framework_report(&pack, &vsa)
269 .context("Failed to render the framework predicate")?;
270 if sign {
271 let (org_alias, curve) = resolve_org_signing(&sdk_ctx, &org, key)?;
272 sign_framework_report(&sdk_ctx, org_did.as_str(), &org_alias, curve, &report)
273 .context("Failed to org-sign the framework predicate")?
274 .to_canonical_json()
275 .context("Failed to serialize DSSE envelope")?
276 } else {
277 report
278 .to_intoto_statement()
279 .context("Failed to serialize the framework predicate")?
280 }
281 } else if sign {
282 let (org_alias, curve) = resolve_org_signing(&sdk_ctx, &org, key)?;
283 sign_evidence_pack(&sdk_ctx, org_did.as_str(), &org_alias, curve, &pack)
284 .context("Failed to org-sign the evidence pack")?
285 .to_canonical_json()
286 .context("Failed to serialize DSSE envelope")?
287 } else {
288 pack.canonicalize()
289 .context("Failed to serialize evidence pack")?
290 };
291
292 match &out {
293 Some(path) => {
294 fs::write(path, &output)
295 .with_context(|| format!("Failed to write pack to {path:?}"))?;
296 eprintln!("✅ Compliance evidence pack written to {path:?}");
297 eprintln!(" Rows: {}", pack.rows.len());
298 eprintln!(" Offline-verifiable: {}", pack.org_bundle.is_some());
299 eprintln!(" Org-signed (DSSE): {sign}");
300 eprintln!(
301 " Witness verdict: {}",
302 pack.equivocation_visibility.label
303 );
304 }
305 None => println!("{output}"),
306 }
307 Ok(())
308 }
309 }
310}
311
312impl ExecutableCommand for ComplianceCommand {
313 #[allow(clippy::disallowed_methods)] fn execute(&self, ctx: &CliConfig) -> Result<()> {
315 handle_compliance(self.clone(), ctx, Utc::now())
316 }
317}