use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::finding::{Finding, Severity};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Engagement {
pub meta: EngagementMeta,
pub client: Client,
pub findings: Vec<Finding>,
#[serde(default)]
pub appendices: Vec<Appendix>,
pub output: OutputConfig,
#[serde(default)]
pub template: TemplateConfig,
#[serde(default)]
pub severity_thresholds: SeverityThresholds,
#[serde(default)]
pub library: LibraryConfig,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EngagementMeta {
pub name: String,
pub slug: String,
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub start_date: Option<String>,
#[serde(default)]
pub end_date: Option<String>,
#[serde(default = "default_report_version")]
pub report_version: String,
}
fn default_report_version() -> String {
"1.0".to_string()
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Client {
#[serde(default)]
pub name: String,
#[serde(default)]
pub contact: String,
#[serde(default)]
pub email: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, toml::Value>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OutputConfig {
#[serde(default = "default_formats")]
pub formats: Vec<String>,
#[serde(default = "default_output_dir")]
pub directory: String,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
formats: default_formats(),
directory: default_output_dir(),
}
}
}
fn default_formats() -> Vec<String> {
vec!["html".to_string(), "json".to_string()]
}
fn default_output_dir() -> String {
"output".to_string()
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct TemplateConfig {
#[serde(default)]
pub html: Option<String>,
#[serde(default)]
pub docx: Option<String>,
#[serde(default)]
pub pdf: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct LibraryConfig {
#[serde(default)]
pub path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct SeverityThresholds {
#[serde(default)]
pub critical: Option<u32>,
#[serde(default)]
pub high: Option<u32>,
#[serde(default)]
pub medium: Option<u32>,
#[serde(default)]
pub low: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Appendix {
pub title: String,
pub body_markdown: String,
pub body_html: String,
}
impl Engagement {
pub fn sort_findings(&mut self) {
self.findings.sort_by(|a, b| {
b.severity
.rank()
.cmp(&a.severity.rank())
.then_with(|| a.id.cmp(&b.id))
});
}
pub fn severity_counts(&self) -> Vec<(Severity, usize)> {
let mut counts = [
(Severity::Critical, 0usize),
(Severity::High, 0),
(Severity::Medium, 0),
(Severity::Low, 0),
(Severity::Info, 0),
];
for f in &self.findings {
for slot in &mut counts {
if slot.0 == f.severity {
slot.1 += 1;
}
}
}
counts.to_vec()
}
}