Skip to main content

auths_cli/commands/
compliance.rs

1//! `auths compliance` — compliance-as-a-query evidence packs.
2//!
3//! Thin presentation layer over [`auths_sdk::workflows::compliance`]: it reads the
4//! period's releases (from a caller file, or **discovered** from the release
5//! attestations anchored in the org KEL), calls the SDK query engine to classify
6//! each signer's authority **at release** (by KEL position), embeds the honest
7//! witness verdict, optionally embeds the org KEL bundle for offline verification,
8//! and optionally org-signs the pack as a DSSE-wrapped in-toto statement.
9//! `compliance attest` is the signing-time half: it anchors the release fact so the
10//! report can derive it. No domain logic lives here.
11
12use anyhow::{Context, Result, anyhow};
13use chrono::{DateTime, Utc};
14use clap::{Parser, Subcommand, ValueEnum};
15use std::fs;
16use std::path::{Path, PathBuf};
17
18use auths_crypto::CurveType;
19use auths_sdk::context::AuthsContext;
20use auths_sdk::keychain::{KeyAlias, extract_public_key_bytes};
21use auths_sdk::storage_layout::layout;
22use auths_sdk::workflows::compliance::{
23    ArtifactDigest, ComplianceFramework, ReleaseRecord, TransparencyInclusion,
24    VerifiedEvidencePack, VsaParams, attest_release, build_evidence_pack, build_framework_report,
25    build_offline_evidence_pack, discover_releases, load_witness_policy, sign_evidence_pack,
26    sign_framework_report, verify_signed_evidence_pack_offline,
27};
28use auths_sdk::workflows::org::resolve_org_signing_alias;
29use auths_sdk::workflows::roots::parse_roots_typed;
30use auths_verifier::{Ed25519PublicKey, IdentityDID, Prefix};
31
32use crate::commands::executable::ExecutableCommand;
33use crate::config::CliConfig;
34use crate::factories::storage::build_auths_context;
35use crate::ux::format::{JsonResponse, Output, is_json_mode};
36
37/// Resolve the org signing alias and its in-band curve.
38///
39/// The alias resolution is the same single source of truth the `auths org`
40/// commands use ([`resolve_org_signing_alias`]): the org's Primary key is
41/// located in the keychain by its DID, so the alias matches whatever `org
42/// create` stored regardless of the org's human-readable name.
43fn resolve_org_signing(
44    sdk_ctx: &AuthsContext,
45    org: &str,
46    key: Option<String>,
47) -> Result<(KeyAlias, CurveType)> {
48    let org_prefix = org.strip_prefix("did:keri:").unwrap_or(org);
49    let org_alias = resolve_org_signing_alias(sdk_ctx.key_storage.as_ref(), org_prefix, key)
50        .map_err(anyhow::Error::from)?;
51    let (_pk, curve) = extract_public_key_bytes(
52        sdk_ctx.key_storage.as_ref(),
53        &org_alias,
54        sdk_ctx.passphrase_provider.as_ref(),
55    )
56    .with_context(|| format!("Failed to resolve org signing key '{org_alias}'"))?;
57    Ok((org_alias, curve))
58}
59
60/// Hash an artifact file with SHA-256 into a parsed `sha256:<hex>` digest.
61fn file_artifact_digest(path: &Path) -> Result<ArtifactDigest> {
62    use sha2::{Digest, Sha256};
63    let bytes = fs::read(path).with_context(|| format!("Failed to read artifact file {path:?}"))?;
64    let digest = format!("sha256:{}", hex::encode(Sha256::digest(&bytes)));
65    ArtifactDigest::parse(&digest).map_err(|e| anyhow!("computed digest rejected: {e}"))
66}
67
68/// One release entry in the `--releases` JSON file. `signer` accepts a `did:keri:`
69/// or a bare KEL prefix; `transparency` is the optional log inclusion evidence.
70#[derive(Debug, Clone, serde::Deserialize)]
71struct ReleaseInput {
72    artifact_digest: String,
73    signer: String,
74    #[serde(default)]
75    signed_at: Option<u128>,
76    #[serde(default)]
77    transparency: Option<TransparencyInclusion>,
78}
79
80impl ReleaseInput {
81    fn into_record(self) -> ReleaseRecord {
82        let signer_prefix = Prefix::new_unchecked(
83            self.signer
84                .strip_prefix("did:keri:")
85                .unwrap_or(&self.signer)
86                .to_string(),
87        );
88        ReleaseRecord {
89            artifact_digest: self.artifact_digest,
90            signer_prefix,
91            signed_at: self.signed_at,
92            transparency: self.transparency,
93        }
94    }
95}
96
97/// CLI wrapper for the target compliance framework.
98#[derive(Debug, Clone, Copy, ValueEnum)]
99pub enum CliFramework {
100    /// SLSA provenance.
101    Slsa,
102    /// SPDX software bill of materials.
103    Sbom,
104    /// EU Cyber Resilience Act obligation mapping.
105    Cra,
106    /// SOC 2 Trust Services Criteria (TSC) control mapping.
107    Soc2,
108    /// ISO/IEC 27001:2022 Annex-A control mapping.
109    Iso27001,
110}
111
112impl From<CliFramework> for ComplianceFramework {
113    fn from(f: CliFramework) -> Self {
114        match f {
115            CliFramework::Slsa => ComplianceFramework::Slsa,
116            CliFramework::Sbom => ComplianceFramework::Sbom,
117            CliFramework::Cra => ComplianceFramework::Cra,
118            CliFramework::Soc2 => ComplianceFramework::Soc2,
119            CliFramework::Iso27001 => ComplianceFramework::Iso27001,
120        }
121    }
122}
123
124/// The `compliance` subcommand: compliance as a query over the org's event log.
125#[derive(Parser, Debug, Clone)]
126#[command(
127    about = "Compliance as a query — offline-verifiable evidence packs",
128    after_help = "Examples:
129  auths compliance attest --org did:keri:EOrg --artifact dist/cli-v2.4.0.tar.gz \\
130      --signer did:keri:EMember
131                        # At signing time: anchor the release (artifact digest +
132                        # signer) in the org KEL — the position becomes log fact
133
134  auths compliance report --org did:keri:EOrg --period 2026-Q3 \\
135      --framework slsa --discover --offline --out acme-2026Q3.evidence
136                        # Build an offline-verifiable evidence pack from the
137                        # releases anchored in the org KEL (signed_at derived)
138
139  auths compliance verify --pack acme-2026Q3.evidence --roots auths-roots \\
140      --log-key auths-log.pub
141                        # Auditor-side: verify a signed pack offline (exit 0
142                        # authentic / exit 1 rejected) — no account, no network.
143                        # --log-key pins the log operator: every row's checkpoint
144                        # signature must verify, not just its Merkle membership
145
146Releases file (JSON array, caller-asserted alternative to --discover):
147  [{\"artifact_digest\":\"sha256:…\",\"signer\":\"did:keri:EMember\",\"signed_at\":41}]"
148)]
149pub struct ComplianceCommand {
150    #[clap(subcommand)]
151    pub subcommand: ComplianceSubcommand,
152}
153
154/// Subcommands for compliance queries.
155#[derive(Subcommand, Debug, Clone)]
156pub enum ComplianceSubcommand {
157    /// Anchor a release attestation (artifact digest + signer) in the org KEL —
158    /// the signing position becomes part of the tamper-evident log
159    #[command(group(clap::ArgGroup::new("artifact_source").required(true)))]
160    Attest {
161        /// Organization identity ID (`did:keri:…`) or bare prefix
162        #[arg(long)]
163        org: String,
164
165        /// Artifact file to hash (sha256) and attest
166        #[arg(long, group = "artifact_source")]
167        artifact: Option<PathBuf>,
168
169        /// Pre-computed artifact digest (`sha256:<64 hex>`)
170        #[arg(long, group = "artifact_source")]
171        digest: Option<String>,
172
173        /// The signing member's identity (`did:keri:…`) or bare prefix
174        #[arg(long)]
175        signer: String,
176
177        /// Org signing key alias (defaults to the org slug alias)
178        #[arg(long)]
179        key: Option<String>,
180    },
181
182    /// Produce a compliance evidence pack for a reporting period.
183    #[command(group(clap::ArgGroup::new("release_source").required(true)))]
184    Report {
185        /// Organization identity ID (`did:keri:…`) or bare prefix
186        #[arg(long)]
187        org: String,
188
189        /// Reporting period label (free-form, e.g. `2026-Q3`)
190        #[arg(long)]
191        period: String,
192
193        /// Target framework (tags the pack; with `--predicate`, selects the
194        /// rendered predicate: SLSA provenance+VSA / SPDX SBOM / CRA mapping /
195        /// SOC 2 TSC mapping / ISO 27001 Annex-A mapping)
196        #[arg(long, value_enum, default_value = "slsa")]
197        framework: CliFramework,
198
199        /// Render the framework predicate (in-toto Statement) instead of the raw pack
200        #[arg(long)]
201        predicate: bool,
202
203        /// Verifier id recorded in the SLSA VSA (with `--predicate --framework slsa`)
204        #[arg(long, default_value = "https://auths.dev/compliance")]
205        verifier_id: String,
206
207        /// JSON file: array of `{ artifact_digest, signer, signed_at?, transparency? }`
208        /// — caller-asserted positions (alternative to `--discover`)
209        #[arg(long, group = "release_source")]
210        releases: Option<PathBuf>,
211
212        /// Derive the rows from the release attestations anchored in the org KEL
213        /// (`compliance attest`); each row's `signed_at` IS its anchoring position
214        #[arg(long, group = "release_source")]
215        discover: bool,
216
217        /// Embed the org KEL bundle so each row verifies offline (no network)
218        #[arg(long)]
219        offline: bool,
220
221        /// Org-sign the pack as a DSSE-wrapped in-toto statement
222        #[arg(long)]
223        sign: bool,
224
225        /// Org signing key alias (defaults to the org slug alias); used with `--sign`
226        #[arg(long)]
227        key: Option<String>,
228
229        /// Pinned witness-policy path (default: `$AUTHS_WITNESS_POLICY_PATH`, else fail-closed)
230        #[arg(long)]
231        witness_policy: Option<PathBuf>,
232
233        /// Output file (default: stdout)
234        #[arg(long)]
235        out: Option<PathBuf>,
236    },
237
238    /// Verify a DSSE-signed evidence pack offline — no account, no network, no keychain
239    Verify {
240        /// The DSSE-signed pack file (from `compliance report --offline --sign`)
241        #[arg(long)]
242        pack: PathBuf,
243
244        /// Pinned trust-roots file (one `did:keri:…` per line) — the only trust input
245        #[arg(long)]
246        roots: PathBuf,
247
248        /// Pinned log-operator key file (one hex Ed25519 key, `#` comments
249        /// allowed). When given, every row's transparency checkpoint must be
250        /// SIGNED by this operator — "in the log" becomes operator-attested
251        /// non-repudiation, not bare Merkle membership
252        #[arg(long)]
253        log_key: Option<PathBuf>,
254    },
255}
256
257/// Parse a pinned log-operator key file: the first non-empty, non-comment line,
258/// as a 64-hex-char Ed25519 public key. Fail-closed on anything else.
259fn parse_pinned_log_key(path: &Path) -> Result<Ed25519PublicKey> {
260    let raw = fs::read_to_string(path)
261        .with_context(|| format!("Failed to read pinned log-key file {path:?}"))?;
262    let line = raw
263        .lines()
264        .map(str::trim)
265        .find(|l| !l.is_empty() && !l.starts_with('#'))
266        .ok_or_else(|| anyhow!("pinned log-key file {path:?} contains no key"))?;
267    let bytes = hex::decode(line)
268        .map_err(|e| anyhow!("pinned log-key file {path:?} is not valid hex: {e}"))?;
269    Ed25519PublicKey::try_from_slice(&bytes)
270        .map_err(|e| anyhow!("pinned log-key file {path:?} rejected: {e}"))
271}
272
273/// Handle `auths compliance` subcommands.
274///
275/// Args:
276/// * `cmd`: The parsed compliance command.
277/// * `ctx`: The CLI config (repo path, passphrase provider, env).
278/// * `now`: The presentation-boundary timestamp (injected into the pack).
279///
280/// Usage:
281/// ```ignore
282/// handle_compliance(cmd, ctx, Utc::now())?;
283/// ```
284pub fn handle_compliance(
285    cmd: ComplianceCommand,
286    ctx: &CliConfig,
287    now: DateTime<Utc>,
288) -> Result<()> {
289    match cmd.subcommand {
290        ComplianceSubcommand::Attest {
291            org,
292            artifact,
293            digest,
294            signer,
295            key,
296        } => {
297            let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
298            let org_prefix =
299                Prefix::new_unchecked(org.strip_prefix("did:keri:").unwrap_or(&org).to_string());
300            let signer_prefix = Prefix::new_unchecked(
301                signer
302                    .strip_prefix("did:keri:")
303                    .unwrap_or(&signer)
304                    .to_string(),
305            );
306
307            let artifact_digest = match (&artifact, &digest) {
308                (Some(path), None) => file_artifact_digest(path)?,
309                (None, Some(d)) => {
310                    ArtifactDigest::parse(d).map_err(|e| anyhow!("invalid --digest value: {e}"))?
311                }
312                _ => unreachable!("clap requires exactly one of --artifact/--digest"),
313            };
314
315            let sdk_ctx = build_auths_context(
316                &repo_path,
317                &ctx.env_config,
318                Some(ctx.passphrase_provider.clone()),
319            )?;
320            let (org_alias, _curve) = resolve_org_signing(&sdk_ctx, &org, key)?;
321            let anchored = attest_release(
322                &sdk_ctx,
323                &org_prefix,
324                &org_alias,
325                artifact_digest,
326                signer_prefix,
327            )
328            .context("Failed to anchor the release attestation in the org KEL")?;
329
330            if is_json_mode() {
331                JsonResponse {
332                    success: true,
333                    command: "compliance attest".to_string(),
334                    data: Some(serde_json::json!({
335                        "org": format!("did:keri:{}", org_prefix.as_str()),
336                        "artifact_digest": anchored.artifact_digest.as_str(),
337                        "signer": format!("did:keri:{}", anchored.signer.as_str()),
338                        "signed_at": anchored.signed_at.to_string(),
339                        "attestation_said": anchored.attestation_said.as_str(),
340                    })),
341                    error: None,
342                }
343                .print()?;
344            } else {
345                let out = Output::stdout();
346                println!(
347                    "{}",
348                    out.success("Release attestation anchored in the org KEL")
349                );
350                println!("  Artifact:  {}", anchored.artifact_digest);
351                println!("  Signer:    did:keri:{}", anchored.signer.as_str());
352                println!("  Signed at: org KEL seq {}", anchored.signed_at);
353                println!("  SAID:      {}", anchored.attestation_said.as_str());
354            }
355            Ok(())
356        }
357
358        ComplianceSubcommand::Report {
359            org,
360            period,
361            framework,
362            predicate,
363            verifier_id,
364            releases,
365            discover,
366            offline,
367            sign,
368            key,
369            witness_policy,
370            out,
371        } => {
372            let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
373            let passphrase_provider = ctx.passphrase_provider.clone();
374
375            let org_prefix =
376                Prefix::new_unchecked(org.strip_prefix("did:keri:").unwrap_or(&org).to_string());
377            let org_did = IdentityDID::from_prefix(org_prefix.as_str())
378                .map_err(|e| anyhow!("invalid org identifier '{org}': {e}"))?;
379
380            let policy_path = witness_policy.or_else(|| {
381                std::env::var("AUTHS_WITNESS_POLICY_PATH")
382                    .ok()
383                    .map(PathBuf::from)
384            });
385            let policy_result = load_witness_policy(policy_path.as_deref());
386
387            let sdk_ctx = build_auths_context(
388                &repo_path,
389                &ctx.env_config,
390                Some(passphrase_provider.clone()),
391            )?;
392
393            let records: Vec<ReleaseRecord> = if discover {
394                discover_releases(sdk_ctx.registry.as_ref(), &org_prefix)
395                    .context("Failed to discover anchored releases from the org KEL")?
396            } else {
397                // clap's release_source group guarantees one of the two is set.
398                let Some(releases) = releases else {
399                    return Err(anyhow!("one of --releases or --discover is required"));
400                };
401                let raw = fs::read_to_string(&releases)
402                    .with_context(|| format!("Failed to read releases file {releases:?}"))?;
403                let inputs: Vec<ReleaseInput> = serde_json::from_str(&raw)
404                    .with_context(|| format!("Invalid JSON in releases file {releases:?}"))?;
405                inputs.into_iter().map(ReleaseInput::into_record).collect()
406            };
407            let framework = ComplianceFramework::from(framework);
408
409            let pack = if offline {
410                build_offline_evidence_pack(
411                    &sdk_ctx,
412                    org_did.clone(),
413                    &org_prefix,
414                    period,
415                    framework,
416                    &records,
417                    &policy_result,
418                    now,
419                )
420            } else {
421                build_evidence_pack(
422                    &sdk_ctx,
423                    org_did.clone(),
424                    &org_prefix,
425                    period,
426                    framework,
427                    &records,
428                    &policy_result,
429                    now,
430                )
431            }
432            .context("Failed to build compliance evidence pack")?;
433
434            let output = if predicate {
435                let vsa = VsaParams {
436                    verifier_id: verifier_id.clone(),
437                    time_verified: now,
438                    allow_list: Default::default(),
439                };
440                let report = build_framework_report(&pack, &vsa)
441                    .context("Failed to render the framework predicate")?;
442                if sign {
443                    let (org_alias, curve) = resolve_org_signing(&sdk_ctx, &org, key)?;
444                    sign_framework_report(&sdk_ctx, org_did.as_str(), &org_alias, curve, &report)
445                        .context("Failed to org-sign the framework predicate")?
446                        .to_canonical_json()
447                        .context("Failed to serialize DSSE envelope")?
448                } else {
449                    report
450                        .to_intoto_statement()
451                        .context("Failed to serialize the framework predicate")?
452                }
453            } else if sign {
454                let (org_alias, curve) = resolve_org_signing(&sdk_ctx, &org, key)?;
455                sign_evidence_pack(&sdk_ctx, org_did.as_str(), &org_alias, curve, &pack)
456                    .context("Failed to org-sign the evidence pack")?
457                    .to_canonical_json()
458                    .context("Failed to serialize DSSE envelope")?
459            } else {
460                pack.canonicalize()
461                    .context("Failed to serialize evidence pack")?
462            };
463
464            match &out {
465                Some(path) => {
466                    fs::write(path, &output)
467                        .with_context(|| format!("Failed to write pack to {path:?}"))?;
468                    eprintln!("✅ Compliance evidence pack written to {path:?}");
469                    eprintln!("   Rows:               {}", pack.rows.len());
470                    eprintln!("   Offline-verifiable: {}", pack.org_bundle.is_some());
471                    eprintln!("   Org-signed (DSSE):  {sign}");
472                    eprintln!(
473                        "   Witness verdict:    {}",
474                        pack.equivocation_visibility.label
475                    );
476                }
477                None => println!("{output}"),
478            }
479            Ok(())
480        }
481
482        ComplianceSubcommand::Verify {
483            pack,
484            roots,
485            log_key,
486        } => {
487            let envelope_json = fs::read_to_string(&pack)
488                .with_context(|| format!("Failed to read evidence pack {pack:?}"))?;
489            let roots_raw = fs::read_to_string(&roots)
490                .with_context(|| format!("Failed to read pinned-roots file {roots:?}"))?;
491            let pinned = parse_roots_typed(&roots_raw)
492                .map_err(|e| anyhow!("pinned roots file rejected: {e}"))?;
493            let pinned_log_key = log_key.as_deref().map(parse_pinned_log_key).transpose()?;
494
495            // Hard rejections (no envelope, bad DSSE signature, unpinned org,
496            // KEL tamper, duplicity) surface here as errors → exit 1.
497            let verified = verify_signed_evidence_pack_offline(
498                &envelope_json,
499                &pinned,
500                pinned_log_key.as_ref(),
501            )
502            .map_err(|e| anyhow!("evidence REJECTED: {e}"))?;
503            let authentic = verified.authentic();
504
505            if is_json_mode() {
506                JsonResponse {
507                    success: authentic,
508                    command: "compliance verify".to_string(),
509                    data: Some(verify_verdict_json(
510                        &pack,
511                        &verified,
512                        authentic,
513                        pinned_log_key.is_some(),
514                    )),
515                    error: (!authentic)
516                        .then(|| "a row is inconsistent with the embedded log".to_string()),
517                }
518                .print()?;
519            } else {
520                print_verify_report(&pack, &verified, authentic, pinned_log_key.is_some());
521            }
522
523            // A verified envelope whose rows diverge from the embedded log is
524            // still a rejection — the auditor's contract is the exit code.
525            if !authentic {
526                return Err(anyhow!(
527                    "evidence REJECTED — a row is inconsistent with the embedded log"
528                ));
529            }
530            Ok(())
531        }
532    }
533}
534
535/// The wire label of a row's authority verdict, read from its serde tag (the
536/// single source of truth for the vocabulary the pack itself carries).
537fn authority_label(verdict: &auths_sdk::workflows::compliance::RowVerdict) -> String {
538    serde_json::to_value(&verdict.authority_at_release)
539        .ok()
540        .and_then(|v| v["authority_at_signing"].as_str().map(|s| s.to_string()))
541        .unwrap_or_else(|| "?".to_string())
542}
543
544/// The machine-readable verdict for `compliance verify --json`.
545fn verify_verdict_json(
546    pack_path: &Path,
547    verified: &VerifiedEvidencePack,
548    authentic: bool,
549    log_key_pinned: bool,
550) -> serde_json::Value {
551    serde_json::json!({
552        "pack": pack_path,
553        "org": verified.pack.org.as_str(),
554        "period": verified.pack.period,
555        "framework": verified.pack.framework,
556        "dsse_signature": "verified",
557        "org_key_source": "authenticated embedded KEL",
558        "org_kel_seq": verified.org_kel_seq.to_string(),
559        "root_pinned": true,
560        "log_key_pinned": log_key_pinned,
561        "rows": verified.verdicts,
562        "authentic": authentic,
563    })
564}
565
566/// Render the auditor-facing verification report (green = proven, red = rejected).
567fn print_verify_report(
568    pack_path: &Path,
569    verified: &VerifiedEvidencePack,
570    authentic: bool,
571    log_key_pinned: bool,
572) {
573    let out = Output::stdout();
574    println!(
575        "Offline evidence-pack verification of {}",
576        pack_path.display()
577    );
578    println!("  Org:      {}", verified.pack.org.as_str());
579    println!(
580        "  Period:   {}   Framework: {:?}",
581        verified.pack.period, verified.pack.framework
582    );
583    println!(
584        "  {}",
585        out.success(
586            "DSSE signature verified — org key resolved from the authenticated KEL, not a keychain"
587        )
588    );
589    println!(
590        "  {}",
591        out.success(&format!(
592            "Root pinned · no duplicity · org KEL signature-authenticated (seq {})",
593            verified.org_kel_seq
594        ))
595    );
596    if log_key_pinned {
597        println!(
598            "  {}",
599            out.success(
600                "Log-operator key pinned — checkpoint signatures verified, not just Merkle membership"
601            )
602        );
603    }
604    println!("  Rows:");
605    for v in &verified.verdicts {
606        let label = authority_label(v);
607        let row_ok = v.authority_consistent && label.starts_with("authorized");
608        let mark = if v.authority_consistent { "✓" } else { "✗" };
609        let transparency = match (v.transparency_verified, v.checkpoint_attested) {
610            (Some(true), Some(true)) => "logged+operator-attested",
611            (Some(true), Some(false)) => "CHECKPOINT-UNATTESTED",
612            (Some(true), None) => "logged",
613            (Some(false), _) => "TRANSPARENCY-FAIL",
614            (None, _) => "unlogged",
615        };
616        let line = format!(
617            "{mark} {}  {}  {label}",
618            truncated(&v.artifact_digest, 19),
619            truncated(v.signer.as_str(), 21),
620        );
621        let line = if row_ok {
622            out.success(&line)
623        } else {
624            out.error(&line)
625        };
626        println!(
627            "    {line}  {}",
628            out.dim(&format!(
629                "consistent={} transparency={transparency}",
630                v.authority_consistent
631            ))
632        );
633    }
634    if authentic {
635        println!(
636            "  {}",
637            out.success(
638                "Verdict:  AUTHENTIC — evidence verified offline, trusting only the pinned roots"
639            )
640        );
641    } else {
642        println!(
643            "  {}",
644            out.error("Verdict:  REJECTED — evidence inconsistent with its own log")
645        );
646    }
647}
648
649/// First `n` characters with an ellipsis when truncated (row alignment helper).
650fn truncated(s: &str, n: usize) -> String {
651    let cut: String = s.chars().take(n).collect();
652    if cut.len() < s.len() {
653        format!("{cut}…")
654    } else {
655        cut
656    }
657}
658
659impl ExecutableCommand for ComplianceCommand {
660    #[allow(clippy::disallowed_methods)] // CLI is the presentation boundary
661    fn execute(&self, ctx: &CliConfig) -> Result<()> {
662        handle_compliance(self.clone(), ctx, Utc::now())
663    }
664}