use anyhow::{Context, Result};
use assay_evidence::lint::engine::{lint_bundle_with_options, LintOptions};
use assay_evidence::lint::packs::{load_packs, LoadedPack};
use assay_evidence::lint::sarif::{to_sarif_with_options, SarifOptions};
use assay_evidence::lint::Severity;
use assay_evidence::VerifyLimits;
use clap::Args;
use std::fs::File;
const DEFAULT_PACK: &str = "cicd-starter";
#[derive(Debug, Args, Clone)]
pub struct LintArgs {
#[arg(value_name = "BUNDLE", required_unless_present = "explain")]
pub bundle: Option<std::path::PathBuf>,
#[arg(long, default_value = "text")]
pub format: String,
#[arg(long, default_value = "error")]
pub fail_on: String,
#[arg(long, value_delimiter = ',')]
pub pack: Option<Vec<String>>,
#[arg(long, value_name = "RULE_ID")]
pub explain: Option<String>,
#[arg(long, default_value = "500")]
pub max_results: usize,
}
pub fn cmd_lint(args: LintArgs) -> Result<i32> {
if let Some(rule_ref) = &args.explain {
let packs = match load_packs_for_lint(&args.pack) {
Ok(p) => p,
Err(e) => {
eprintln!("Pack loading failed: {}", e);
return Ok(3);
}
};
return explain_rule(&packs, rule_ref);
}
let bundle = args
.bundle
.as_ref()
.expect("bundle required when not --explain");
let f = File::open(bundle)
.with_context(|| format!("failed to open bundle {}", bundle.display()))?;
let limits = VerifyLimits::default();
let (packs, is_default_pack) = if let Some(pack_refs) = &args.pack {
match load_packs(pack_refs) {
Ok(p) => (p, false),
Err(e) => {
eprintln!("Pack loading failed: {}", e);
return Ok(3); }
}
} else {
match load_packs(&[DEFAULT_PACK.to_string()]) {
Ok(p) => (p, true),
Err(e) => {
eprintln!("Default pack loading failed: {}", e);
return Ok(3);
}
}
};
let options = LintOptions {
packs,
max_results: Some(args.max_results),
bundle_path: Some(bundle.display().to_string()),
};
let result = match lint_bundle_with_options(f, limits, options) {
Ok(r) => r,
Err(e) => {
eprintln!("Verification failed: {}", e);
return Ok(2);
}
};
let report = &result.report;
let pack_meta = &result.pack_meta;
match args.format.as_str() {
"json" => {
let mut json_report = serde_json::to_value(report)?;
if let Some(meta) = pack_meta {
if let Some(disclaimer) = &meta.disclaimer {
json_report
.as_object_mut()
.unwrap()
.insert("disclaimer".into(), serde_json::json!(disclaimer));
}
if meta.truncated {
json_report
.as_object_mut()
.unwrap()
.insert("truncated".into(), serde_json::json!(true));
json_report.as_object_mut().unwrap().insert(
"truncated_count".into(),
serde_json::json!(meta.truncated_count),
);
}
}
println!("{}", serde_json::to_string_pretty(&json_report)?);
}
"sarif" => {
#[allow(deprecated)]
let sarif_options = SarifOptions {
pack_meta: pack_meta.clone(),
bundle_path: Some(bundle.display().to_string()),
working_directory: None, };
let sarif = to_sarif_with_options(report, sarif_options);
println!("{}", serde_json::to_string_pretty(&sarif)?);
}
_ => {
eprintln!("Assay Evidence Lint");
eprintln!("===================");
eprintln!(
"Bundle: {} (events: {}, verified: {})",
report.bundle_meta.bundle_id, report.bundle_meta.event_count, report.verified
);
if let Some(meta) = pack_meta {
let pack_str = meta
.packs
.iter()
.map(|p| {
let suffix = if is_default_pack && p.name == DEFAULT_PACK {
" (default)"
} else {
""
};
format!("{}@{}{}", p.name, p.version, suffix)
})
.collect::<Vec<_>>()
.join(", ");
eprintln!("Packs: {}", pack_str);
if is_default_pack {
eprintln!("(Next: add compliance packs — --pack eu-ai-act-baseline or --pack soc2-baseline)");
}
if let Some(disclaimer) = &meta.disclaimer {
eprintln!();
eprintln!("⚠️ COMPLIANCE DISCLAIMER");
eprintln!("{}", disclaimer);
}
if meta.truncated {
eprintln!();
eprintln!(
"⚠️ Results truncated: {} findings omitted (--max-results {})",
meta.truncated_count, args.max_results
);
}
}
eprintln!();
if report.findings.is_empty() {
eprintln!("No findings.");
} else {
for finding in &report.findings {
let loc_str = match &finding.location {
Some(loc) => format!("seq:{} line:{}", loc.seq, loc.line),
None => "global".into(),
};
let article_ref = finding
.tags
.iter()
.find(|t| t.starts_with("article_ref:"))
.map(|t| t.strip_prefix("article_ref:").unwrap_or(""));
if let Some(ref_) = article_ref {
eprintln!(
"[{}] {} ({}) {} [Article {}]",
finding.severity, finding.rule_id, loc_str, finding.message, ref_
);
} else {
eprintln!(
"[{}] {} ({}) {}",
finding.severity, finding.rule_id, loc_str, finding.message
);
}
eprintln!(
" Hint: run `assay evidence lint --explain {}`",
finding.rule_id
);
}
eprintln!();
eprintln!(
"Summary: {} total ({} errors, {} warnings, {} info)",
report.summary.total,
report.summary.errors,
report.summary.warnings,
report.summary.infos
);
}
}
}
let threshold = match args.fail_on.as_str() {
"error" => Severity::Error,
"warn" | "warning" => Severity::Warn,
"info" => Severity::Info,
_ => Severity::Error,
};
if report.has_findings_at_or_above(&threshold) {
Ok(1)
} else {
Ok(0)
}
}
fn load_packs_for_lint(
pack_refs: &Option<Vec<String>>,
) -> Result<Vec<LoadedPack>, assay_evidence::lint::packs::PackError> {
let refs: Vec<String> = pack_refs
.clone()
.unwrap_or_else(|| vec![DEFAULT_PACK.to_string()]);
load_packs(&refs)
}
fn explain_rule(packs: &[LoadedPack], rule_ref: &str) -> Result<i32> {
let want_canonical = rule_ref.contains('@') && rule_ref.contains(':');
let mut matches: Vec<(
&LoadedPack,
&assay_evidence::lint::packs::schema::PackRule,
String,
)> = Vec::new();
for p in packs {
for r in &p.definition.rules {
let canonical = p.canonical_rule_id(&r.id);
let ok = if want_canonical {
canonical == rule_ref
} else {
r.id == rule_ref || canonical == rule_ref
};
if ok {
matches.push((p, r, canonical));
}
}
}
if matches.is_empty() {
eprintln!("Rule '{}' not found in selected packs.", rule_ref);
return Ok(1);
}
if matches.len() > 1 && !want_canonical {
eprintln!("Rule '{}' is ambiguous. Specify canonical form:", rule_ref);
for (_, _, canonical) in &matches {
eprintln!(" --explain {}", canonical);
}
return Ok(1);
}
let (p, r, canonical) = &matches[0];
println!("{}", canonical);
println!("Pack: {}@{}", p.definition.name, p.definition.version);
println!("Severity: {:?}", r.severity);
println!();
println!("{}", r.description);
if let Some(article) = &r.article_ref {
println!();
println!("Reference: {}", article);
}
if let Some(help) = &r.help_markdown {
println!();
println!("---");
println!("{}", help.trim());
println!();
} else {
println!();
println!("(No additional help text provided.)");
}
Ok(0)
}