use crate::audit::Audit;
use crate::project::Project;
use crate::runner::{BinaryRunner, RunStatus};
use crate::types::{AuditGroup, AuditLayer, AuditResult, AuditStatus, Confidence};
const SHORT_VERSION_FLAGS: &[&str] = &["-V", "-v", "-version"];
pub struct VersionAudit;
impl Audit for VersionAudit {
fn id(&self) -> &str {
"p3-version"
}
fn label(&self) -> &'static str {
"Version flag works (`--version` plus short alias)"
}
fn group(&self) -> AuditGroup {
AuditGroup::P3
}
fn layer(&self) -> AuditLayer {
AuditLayer::Behavioral
}
fn covers(&self) -> &'static [&'static str] {
&["p3-must-version", "p3-should-version-short"]
}
fn applicable(&self, project: &Project) -> bool {
project.runner.is_some()
}
fn run(&self, project: &Project) -> anyhow::Result<AuditResult> {
let runner = project.runner_ref();
let long = probe_version_flag(runner, "--version");
let short_match = SHORT_VERSION_FLAGS
.iter()
.find(|flag| probe_version_flag(runner, flag).is_ok());
let status = match (&long, short_match) {
(Ok(()), Some(_)) => AuditStatus::Pass,
(Ok(()), None) => AuditStatus::Warn(format!(
"`--version` works but no short alias responded (tried {}). \
Adding one shortens version probes for agents.",
SHORT_VERSION_FLAGS.join(", "),
)),
(Err(reason), _) => AuditStatus::Fail(reason.clone()),
};
Ok(AuditResult {
id: self.id().to_string(),
label: self.label().into(),
group: AuditGroup::P3,
layer: AuditLayer::Behavioral,
status,
confidence: Confidence::High,
})
}
}
fn probe_version_flag(runner: &BinaryRunner, flag: &str) -> Result<(), String> {
let result = runner.run(&[flag], &[]);
match result.status {
RunStatus::Ok if result.exit_code == Some(0) && !result.stdout.trim().is_empty() => Ok(()),
RunStatus::Ok if result.exit_code == Some(0) => Err(format!("`{flag}` produced no output")),
RunStatus::Ok => Err(format!(
"`{flag}` exited with code {}",
result.exit_code.unwrap_or(-1)
)),
_ => Err(format!("`{flag}` failed: {:?}", result.status)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audits::behavioral::tests::test_project_with_runner;
use crate::types::AuditStatus;
#[test]
fn version_pass_with_output() {
let project = test_project_with_runner("/bin/echo");
let result = VersionAudit.run(&project).expect("audit should run");
assert!(matches!(result.status, AuditStatus::Pass));
}
#[test]
fn version_fail_non_zero_exit() {
let project = test_project_with_runner("/bin/false");
let result = VersionAudit.run(&project).expect("audit should run");
assert!(matches!(result.status, AuditStatus::Fail(_)));
}
#[test]
fn version_handles_crash() {
let project = crate::audits::behavioral::tests::test_project_with_sh_script("kill -11 $$");
let result = VersionAudit
.run(&project)
.expect("audit should not panic on crash");
assert!(matches!(result.status, AuditStatus::Fail(_)));
}
#[test]
fn version_warn_when_only_long_form_works() {
let script = "case \"$1\" in --version) echo 1.0.0 ;; *) exit 1 ;; esac";
let project = crate::audits::behavioral::tests::test_project_with_sh_script(script);
let result = VersionAudit.run(&project).expect("audit should run");
match result.status {
AuditStatus::Warn(msg) => {
assert!(
msg.contains("short alias"),
"warn message should mention the missing short alias, got: {msg}"
);
}
other => panic!("expected Warn, got {other:?}"),
}
}
#[test]
fn version_pass_when_short_alias_works() {
let script = "case \"$1\" in --version|-V) echo 1.0.0 ;; *) exit 1 ;; esac";
let project = crate::audits::behavioral::tests::test_project_with_sh_script(script);
let result = VersionAudit.run(&project).expect("audit should run");
assert!(matches!(result.status, AuditStatus::Pass));
}
}