wordpress_vulnerable_scanner/
analyze.rs

1//! Analysis logic for vulnerability scanning
2
3use crate::scanner::{ComponentInfo, ComponentType, ScanResult};
4use crate::vulnerability::{Severity, Vulnerability, VulnerabilityClient};
5use futures::future::join_all;
6use serde::Serialize;
7
8/// Vulnerability analysis for a single component
9#[derive(Debug, Clone, Serialize)]
10pub struct ComponentVulnerabilities {
11    /// Component type
12    pub component_type: ComponentType,
13    /// Component slug
14    pub slug: String,
15    /// Detected version
16    pub version: Option<String>,
17    /// Vulnerabilities affecting this component
18    pub vulnerabilities: Vec<Vulnerability>,
19    /// Highest severity
20    pub max_severity: Option<Severity>,
21}
22
23impl ComponentVulnerabilities {
24    /// Check if there are any vulnerabilities
25    pub fn has_vulnerabilities(&self) -> bool {
26        !self.vulnerabilities.is_empty()
27    }
28
29    /// Count vulnerabilities
30    pub fn vuln_count(&self) -> usize {
31        self.vulnerabilities.len()
32    }
33}
34
35/// Summary of vulnerability counts
36#[derive(Debug, Clone, Default, Serialize)]
37pub struct VulnerabilitySummary {
38    /// Count of critical vulnerabilities
39    pub critical: usize,
40    /// Count of high severity vulnerabilities
41    pub high: usize,
42    /// Count of medium severity vulnerabilities
43    pub medium: usize,
44    /// Count of low severity vulnerabilities
45    pub low: usize,
46    /// Total count
47    pub total: usize,
48}
49
50impl VulnerabilitySummary {
51    /// Create from a list of vulnerabilities
52    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    /// Create from a list of vulnerability references (avoids cloning)
61    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    /// Add a severity to the counts
70    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    /// Check if there are any critical or high severity vulnerabilities
81    pub fn has_critical_or_high(&self) -> bool {
82        self.critical > 0 || self.high > 0
83    }
84
85    /// Check if there are any vulnerabilities
86    pub fn has_any(&self) -> bool {
87        self.total > 0
88    }
89
90    /// Get the highest severity level
91    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/// Complete vulnerability analysis
107#[derive(Debug, Clone, Serialize)]
108pub struct Analysis {
109    /// Target URL (if scanned from URL)
110    pub url: Option<String>,
111    /// Scan timestamp
112    pub scan_date: String,
113    /// Component vulnerability reports
114    pub components: Vec<ComponentVulnerabilities>,
115    /// Overall summary
116    pub summary: VulnerabilitySummary,
117}
118
119impl Analysis {
120    /// Get all vulnerabilities across all components
121    pub fn all_vulnerabilities(&self) -> Vec<&Vulnerability> {
122        self.components
123            .iter()
124            .flat_map(|c| c.vulnerabilities.iter())
125            .collect()
126    }
127
128    /// Filter components to only those with vulnerabilities
129    pub fn vulnerable_components(&self) -> impl Iterator<Item = &ComponentVulnerabilities> {
130        self.components.iter().filter(|c| c.has_vulnerabilities())
131    }
132
133    /// Get components by severity
134    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
142/// Analyzer for scan results
143pub struct Analyzer {
144    client: VulnerabilityClient,
145}
146
147impl Analyzer {
148    /// Create a new analyzer
149    pub fn new() -> crate::error::Result<Self> {
150        Ok(Self {
151            client: VulnerabilityClient::new()?,
152        })
153    }
154
155    /// Analyze scan results for vulnerabilities
156    pub async fn analyze(&self, scan: &ScanResult) -> Analysis {
157        // Fetch all vulnerability reports in parallel
158        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        // Build summary from all vulnerabilities
167        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    /// Analyze a single component
187    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
222/// Get current timestamp in ISO format (lightweight, no chrono dependency)
223fn 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    // Calculate date/time components
233    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    // Days since 1970-01-01
240    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}