wordpress_vulnerable_scanner/
analyze.rs1use crate::scanner::{ComponentInfo, ComponentType, ScanResult};
4use crate::vulnerability::{Severity, Vulnerability, VulnerabilityClient};
5use futures::future::join_all;
6use serde::Serialize;
7
8#[derive(Debug, Clone, Serialize)]
10pub struct ComponentVulnerabilities {
11 pub component_type: ComponentType,
13 pub slug: String,
15 pub version: Option<String>,
17 pub vulnerabilities: Vec<Vulnerability>,
19 pub max_severity: Option<Severity>,
21}
22
23impl ComponentVulnerabilities {
24 pub fn has_vulnerabilities(&self) -> bool {
26 !self.vulnerabilities.is_empty()
27 }
28
29 pub fn vuln_count(&self) -> usize {
31 self.vulnerabilities.len()
32 }
33}
34
35#[derive(Debug, Clone, Default, Serialize)]
37pub struct VulnerabilitySummary {
38 pub critical: usize,
40 pub high: usize,
42 pub medium: usize,
44 pub low: usize,
46 pub total: usize,
48}
49
50impl VulnerabilitySummary {
51 pub fn from_vulnerabilities(vulns: &[Vulnerability]) -> Self {
53 let mut summary = Self::default();
54 for v in vulns {
55 summary.add_severity(v.severity);
56 }
57 summary
58 }
59
60 pub fn from_refs(vulns: &[&Vulnerability]) -> Self {
62 let mut summary = Self::default();
63 for v in vulns {
64 summary.add_severity(v.severity);
65 }
66 summary
67 }
68
69 fn add_severity(&mut self, severity: Severity) {
71 match severity {
72 Severity::Critical => self.critical += 1,
73 Severity::High => self.high += 1,
74 Severity::Medium => self.medium += 1,
75 Severity::Low => self.low += 1,
76 }
77 self.total += 1;
78 }
79
80 pub fn has_critical_or_high(&self) -> bool {
82 self.critical > 0 || self.high > 0
83 }
84
85 pub fn has_any(&self) -> bool {
87 self.total > 0
88 }
89
90 pub fn max_severity(&self) -> Option<Severity> {
92 if self.critical > 0 {
93 Some(Severity::Critical)
94 } else if self.high > 0 {
95 Some(Severity::High)
96 } else if self.medium > 0 {
97 Some(Severity::Medium)
98 } else if self.low > 0 {
99 Some(Severity::Low)
100 } else {
101 None
102 }
103 }
104}
105
106#[derive(Debug, Clone, Serialize)]
108pub struct Analysis {
109 pub url: Option<String>,
111 pub scan_date: String,
113 pub components: Vec<ComponentVulnerabilities>,
115 pub summary: VulnerabilitySummary,
117}
118
119impl Analysis {
120 pub fn all_vulnerabilities(&self) -> Vec<&Vulnerability> {
122 self.components
123 .iter()
124 .flat_map(|c| c.vulnerabilities.iter())
125 .collect()
126 }
127
128 pub fn vulnerable_components(&self) -> impl Iterator<Item = &ComponentVulnerabilities> {
130 self.components.iter().filter(|c| c.has_vulnerabilities())
131 }
132
133 pub fn components_by_severity(&self, severity: Severity) -> Vec<&ComponentVulnerabilities> {
135 self.components
136 .iter()
137 .filter(|c| c.max_severity == Some(severity))
138 .collect()
139 }
140}
141
142pub struct Analyzer {
144 client: VulnerabilityClient,
145}
146
147impl Analyzer {
148 pub fn new() -> crate::error::Result<Self> {
150 Ok(Self {
151 client: VulnerabilityClient::new()?,
152 })
153 }
154
155 pub async fn analyze(&self, scan: &ScanResult) -> Analysis {
157 let futures: Vec<_> = scan
159 .components
160 .iter()
161 .map(|component| self.analyze_component(component))
162 .collect();
163
164 let components: Vec<ComponentVulnerabilities> = join_all(futures).await;
165
166 let all_vulns: Vec<&Vulnerability> = components
168 .iter()
169 .flat_map(|c| c.vulnerabilities.iter())
170 .collect();
171
172 let summary = VulnerabilitySummary::from_refs(&all_vulns);
173
174 Analysis {
175 url: if scan.url.is_empty() {
176 None
177 } else {
178 Some(scan.url.clone())
179 },
180 scan_date: chrono_lite_now(),
181 components,
182 summary,
183 }
184 }
185
186 async fn analyze_component(&self, component: &ComponentInfo) -> ComponentVulnerabilities {
188 let report = match component.component_type {
189 ComponentType::Core => {
190 if let Some(ref version) = component.version {
191 self.client.fetch_core_vulns(version).await
192 } else {
193 None
194 }
195 }
196 ComponentType::Plugin => self.client.fetch_plugin_vulns(&component.slug).await,
197 ComponentType::Theme => self.client.fetch_theme_vulns(&component.slug).await,
198 };
199
200 let filtered = report
201 .unwrap_or_default()
202 .filter_by_version(component.version.as_deref());
203
204 let max_severity = filtered.max_severity();
205
206 ComponentVulnerabilities {
207 component_type: component.component_type,
208 slug: component.slug.clone(),
209 version: component.version.clone(),
210 vulnerabilities: filtered.vulnerabilities,
211 max_severity,
212 }
213 }
214}
215
216impl Default for Analyzer {
217 fn default() -> Self {
218 Self::new().expect("Failed to create analyzer")
219 }
220}
221
222fn chrono_lite_now() -> String {
224 use std::time::{SystemTime, UNIX_EPOCH};
225
226 let duration = SystemTime::now()
227 .duration_since(UNIX_EPOCH)
228 .unwrap_or_default();
229
230 let secs = duration.as_secs();
231
232 let days = secs / 86400;
234 let time_secs = secs % 86400;
235 let hours = time_secs / 3600;
236 let mins = (time_secs % 3600) / 60;
237 let secs = time_secs % 60;
238
239 let mut year = 1970;
241 let mut remaining_days = days;
242
243 loop {
244 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
245 if remaining_days < days_in_year {
246 break;
247 }
248 remaining_days -= days_in_year;
249 year += 1;
250 }
251
252 let days_in_months: [u64; 12] = if is_leap_year(year) {
253 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
254 } else {
255 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
256 };
257
258 let mut month = 1;
259 for days_in_month in days_in_months {
260 if remaining_days < days_in_month {
261 break;
262 }
263 remaining_days -= days_in_month;
264 month += 1;
265 }
266
267 let day = remaining_days + 1;
268
269 format!("{year:04}-{month:02}-{day:02}T{hours:02}:{mins:02}:{secs:02}Z")
270}
271
272fn is_leap_year(year: u64) -> bool {
273 year.is_multiple_of(4) && !year.is_multiple_of(100) || year.is_multiple_of(400)
274}