Skip to main content

scanr_engine/
lib.rs

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}