Skip to main content

zoi_cli/cmd/
audit.rs

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