Skip to main content

zoi_cli/cmd/
audit.rs

1use crate::pkg::{config, db, local, types};
2use anyhow::Result;
3use colored::*;
4use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets::UTF8_FULL};
5use semver::{Version, VersionReq};
6
7pub fn run(all: bool, registry_filter: Option<String>, repo_filter: Option<String>) -> Result<()> {
8    if !all {
9        println!(
10            "{} Auditing installed packages for vulnerabilities...",
11            "::".bold().blue()
12        );
13    } else {
14        println!(
15            "{} Listing all known vulnerabilities...",
16            "::".bold().blue()
17        );
18    }
19
20    let config = config::read_config()?;
21    let mut registries = Vec::new();
22    if let Some(reg) = registry_filter {
23        registries.push(reg);
24    } else {
25        if let Some(default) = &config.default_registry {
26            registries.push(default.handle.clone());
27        }
28        for reg in &config.added_registries {
29            registries.push(reg.handle.clone());
30        }
31    }
32
33    let mut all_advisories = Vec::new();
34    for handle in registries {
35        if let Ok(advisories) = db::list_all_advisories(&handle) {
36            for (adv, repo) in advisories {
37                all_advisories.push((adv, repo, handle.clone()));
38            }
39        }
40    }
41
42    if let Some(rf) = &repo_filter {
43        all_advisories.retain(|(_, repo, _)| {
44            if rf.contains('/') {
45                repo == rf
46            } else {
47                repo.split('/').any(|part| part == rf)
48            }
49        });
50    }
51
52    if all_advisories.is_empty() {
53        println!(
54            "\n{}",
55            "No vulnerabilities found matching your criteria.".green()
56        );
57        return Ok(());
58    }
59
60    if all {
61        print_advisories_table(all_advisories)?;
62    } else {
63        let installed = local::get_installed_packages()?;
64        let mut vulnerable_installed = Vec::new();
65
66        for manifest in installed {
67            for (adv, repo, reg) in &all_advisories {
68                let package_match = adv.package == manifest.name
69                    && *repo == manifest.repo
70                    && *reg == manifest.registry_handle;
71
72                let sub_package_match = match (&adv.sub_package, &manifest.sub_package) {
73                    (Some(adv_sub), Some(man_sub)) => adv_sub == man_sub,
74                    (None, _) => true,
75                    (Some(_), None) => false,
76                };
77
78                if package_match
79                    && sub_package_match
80                    && let Ok(version) = Version::parse(&manifest.version)
81                    && let Ok(req) = VersionReq::parse(&adv.affected_range)
82                    && req.matches(&version)
83                {
84                    vulnerable_installed.push((adv.clone(), manifest.clone()));
85                }
86            }
87        }
88
89        if vulnerable_installed.is_empty() {
90            println!(
91                "\n{}",
92                "No vulnerabilities found in installed packages.".green()
93            );
94        } else {
95            println!(
96                "\n{} Found {} vulnerabilities in installed packages:",
97                "Warning".red().bold(),
98                vulnerable_installed.len()
99            );
100            print_vulnerable_table(vulnerable_installed)?;
101        }
102    }
103
104    Ok(())
105}
106
107fn print_advisories_table(advisories: Vec<(types::Advisory, String, String)>) -> Result<()> {
108    let mut table = Table::new();
109    table
110        .load_preset(UTF8_FULL)
111        .set_content_arrangement(ContentArrangement::Dynamic)
112        .set_header(vec![
113            Cell::new("ID").add_attribute(Attribute::Bold),
114            Cell::new("Package").add_attribute(Attribute::Bold),
115            Cell::new("Severity").add_attribute(Attribute::Bold),
116            Cell::new("Affected").add_attribute(Attribute::Bold),
117            Cell::new("Fixed In").add_attribute(Attribute::Bold),
118            Cell::new("Summary").add_attribute(Attribute::Bold),
119        ]);
120
121    for (adv, _, _) in advisories {
122        let severity_cell = match adv.severity {
123            types::Severity::Low => Cell::new("Low").fg(comfy_table::Color::Blue),
124            types::Severity::Medium => Cell::new("Medium").fg(comfy_table::Color::Yellow),
125            types::Severity::High => Cell::new("High").fg(comfy_table::Color::Red),
126            types::Severity::Critical => Cell::new("Critical")
127                .fg(comfy_table::Color::Magenta)
128                .add_attribute(Attribute::Bold),
129        };
130
131        let package_display = if let Some(sub) = &adv.sub_package {
132            format!("{}:{}", adv.package, sub)
133        } else {
134            adv.package.clone()
135        };
136
137        table.add_row(vec![
138            Cell::new(adv.id).fg(comfy_table::Color::Cyan),
139            Cell::new(package_display),
140            severity_cell,
141            Cell::new(adv.affected_range),
142            Cell::new(adv.fixed_in.unwrap_or_else(|| "N/A".to_string()))
143                .fg(comfy_table::Color::Green),
144            Cell::new(adv.summary),
145        ]);
146    }
147
148    println!("{table}");
149    Ok(())
150}
151
152fn print_vulnerable_table(
153    vulnerable: Vec<(types::Advisory, types::InstallManifest)>,
154) -> Result<()> {
155    let mut table = Table::new();
156    table
157        .load_preset(UTF8_FULL)
158        .set_content_arrangement(ContentArrangement::Dynamic)
159        .set_header(vec![
160            Cell::new("Package").add_attribute(Attribute::Bold),
161            Cell::new("Installed").add_attribute(Attribute::Bold),
162            Cell::new("ID").add_attribute(Attribute::Bold),
163            Cell::new("Severity").add_attribute(Attribute::Bold),
164            Cell::new("Fixed In").add_attribute(Attribute::Bold),
165            Cell::new("Summary").add_attribute(Attribute::Bold),
166        ]);
167
168    for (adv, manifest) in vulnerable {
169        let severity_cell = match adv.severity {
170            types::Severity::Low => Cell::new("Low").fg(comfy_table::Color::Blue),
171            types::Severity::Medium => Cell::new("Medium").fg(comfy_table::Color::Yellow),
172            types::Severity::High => Cell::new("High").fg(comfy_table::Color::Red),
173            types::Severity::Critical => Cell::new("Critical")
174                .fg(comfy_table::Color::Magenta)
175                .add_attribute(Attribute::Bold),
176        };
177
178        let package_display = if let Some(sub) = &manifest.sub_package {
179            format!("{}:{}", manifest.name, sub)
180        } else {
181            manifest.name.clone()
182        };
183
184        table.add_row(vec![
185            Cell::new(package_display).fg(comfy_table::Color::Cyan),
186            Cell::new(manifest.version).fg(comfy_table::Color::Red),
187            Cell::new(adv.id).fg(comfy_table::Color::DarkGrey),
188            severity_cell,
189            Cell::new(adv.fixed_in.unwrap_or_else(|| "N/A".to_string()))
190                .fg(comfy_table::Color::Green),
191            Cell::new(adv.summary),
192        ]);
193    }
194
195    println!("{table}");
196    Ok(())
197}