1use std::error::Error;
2use std::fmt::{Display, Formatter};
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum EngineType {
10 SCA,
11 Container,
12 IaC,
13 SAST,
14 Secrets,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Severity {
20 Critical,
21 High,
22 Medium,
23 Low,
24 Unknown,
25}
26
27impl Display for Severity {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::Critical => write!(f, "critical"),
31 Self::High => write!(f, "high"),
32 Self::Medium => write!(f, "medium"),
33 Self::Low => write!(f, "low"),
34 Self::Unknown => write!(f, "unknown"),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Finding {
41 pub id: String,
42 pub engine: EngineType,
43 pub severity: Severity,
44 pub title: String,
45 pub description: String,
46 pub location: Option<String>,
47 pub remediation: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub enum ScanInput {
52 Path(PathBuf),
53 Image(String),
54 Tar(PathBuf),
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ScanMetadata {
59 pub engine: EngineType,
60 pub engine_name: String,
61 pub target: String,
62 pub total_dependencies: usize,
63 pub total_vulnerabilities: usize,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ScanResult {
68 pub findings: Vec<Finding>,
69 pub metadata: ScanMetadata,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
73pub struct SeverityCounts {
74 pub critical: usize,
75 pub high: usize,
76 pub medium: usize,
77 pub low: usize,
78 pub unknown: usize,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "UPPERCASE")]
83pub enum RiskLevel {
84 Low,
85 Moderate,
86 High,
87}
88
89impl Display for RiskLevel {
90 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::Low => write!(f, "LOW"),
93 Self::Moderate => write!(f, "MODERATE"),
94 Self::High => write!(f, "HIGH"),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100pub struct FindingSummary {
101 pub total: usize,
102 pub counts: SeverityCounts,
103 pub risk_level: RiskLevel,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107pub struct VulnerabilityPolicy {
108 pub max_critical: usize,
109 pub max_high: usize,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct PolicyEvaluation {
114 pub passed: bool,
115 pub violations: Vec<String>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct EngineError {
120 pub message: String,
121}
122
123impl EngineError {
124 pub fn new(message: impl Into<String>) -> Self {
125 Self {
126 message: message.into(),
127 }
128 }
129}
130
131impl Display for EngineError {
132 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133 write!(f, "{}", self.message)
134 }
135}
136
137impl Error for EngineError {}
138
139pub type EngineResult<T> = std::result::Result<T, EngineError>;
140
141pub trait ScanEngine {
142 fn name(&self) -> &'static str;
143
144 fn scan(&self, input: ScanInput) -> EngineResult<ScanResult>;
145}
146
147pub fn summarize_findings(findings: &[Finding]) -> FindingSummary {
148 let mut counts = SeverityCounts::default();
149 for finding in findings {
150 match finding.severity {
151 Severity::Critical => counts.critical += 1,
152 Severity::High => counts.high += 1,
153 Severity::Medium => counts.medium += 1,
154 Severity::Low => counts.low += 1,
155 Severity::Unknown => counts.unknown += 1,
156 }
157 }
158
159 let risk_level = if counts.critical > 0 || counts.high > 0 {
160 RiskLevel::High
161 } else if counts.medium > 0 || counts.unknown > 0 {
162 RiskLevel::Moderate
163 } else {
164 RiskLevel::Low
165 };
166
167 FindingSummary {
168 total: findings.len(),
169 counts,
170 risk_level,
171 }
172}
173
174pub fn evaluate_vulnerability_policy(
175 findings: &[Finding],
176 policy: &VulnerabilityPolicy,
177) -> PolicyEvaluation {
178 let summary = summarize_findings(findings);
179 let mut violations = Vec::new();
180
181 if summary.counts.critical > policy.max_critical {
182 violations.push(format!(
183 "critical vulnerabilities {} exceed max_critical {}",
184 summary.counts.critical, policy.max_critical
185 ));
186 }
187
188 if summary.counts.high > policy.max_high {
189 violations.push(format!(
190 "high vulnerabilities {} exceed max_high {}",
191 summary.counts.high, policy.max_high
192 ));
193 }
194
195 PolicyEvaluation {
196 passed: violations.is_empty(),
197 violations,
198 }
199}
200
201pub fn resolve_exit_code(vulnerability_failed: bool, license_failed: bool) -> i32 {
202 match (vulnerability_failed, license_failed) {
203 (false, false) => 0,
204 (true, false) => 2,
205 (false, true) => 3,
206 (true, true) => 4,
207 }
208}