use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Verdict {
Secure,
Vulnerable,
Inconclusive,
Errored,
}
impl Verdict {
pub fn label(&self) -> &'static str {
match self {
Verdict::Secure => "PASSED",
Verdict::Vulnerable => "FAILED",
Verdict::Inconclusive => "INCONCLUSIVE",
Verdict::Errored => "ERROR",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub description: String,
pub passed: bool,
pub evaluated: bool,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttackOutcome {
pub name: String,
#[serde(default)]
pub suite: Option<String>,
pub severity: String,
pub target: String,
pub verdict: Verdict,
pub message: Option<String>,
pub checks: Vec<CheckResult>,
pub error: Option<String>,
pub issue_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleFinding {
pub rule: String,
pub severity: String,
pub file: String,
pub line: usize,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestRun {
pub project: Option<String>,
pub timestamp: String,
pub sources: Vec<String>,
pub attacks: Vec<AttackOutcome>,
pub rule_findings: Vec<RuleFinding>,
#[serde(default = "one")]
pub workers: usize,
#[serde(default)]
pub elapsed_ms: u128,
}
fn one() -> usize {
1
}
impl TestRun {
pub fn vulnerable_count(&self) -> usize {
self.attacks
.iter()
.filter(|a| a.verdict == Verdict::Vulnerable)
.count()
}
pub fn error_count(&self) -> usize {
self.attacks
.iter()
.filter(|a| a.verdict == Verdict::Errored)
.count()
}
pub fn inconclusive_count(&self) -> usize {
self.attacks
.iter()
.filter(|a| a.verdict == Verdict::Inconclusive)
.count()
}
pub fn passed_count(&self) -> usize {
self.attacks
.iter()
.filter(|a| a.verdict == Verdict::Secure)
.count()
}
pub fn has_vulnerabilities(&self) -> bool {
self.vulnerable_count() > 0
}
pub fn has_inconclusive(&self) -> bool {
self.inconclusive_count() > 0
}
pub fn save(&self, root: &Path, slug: &str) -> Result<PathBuf> {
let dir = root.join(".killer").join("results");
std::fs::create_dir_all(&dir)
.with_context(|| format!("failed to create {}", dir.display()))?;
let safe: String = slug
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let path = dir.join(format!("{safe}.json"));
let json = serde_json::to_string_pretty(self).context("failed to serialize results")?;
std::fs::write(&path, json)
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(path)
}
}