cargo-deny 0.14.13

Cargo plugin to help you manage large dependency graphs
Documentation
use super::cfg::IgnoreId;
use crate::{
    diag::{Check, Diagnostic, FileId, Label, Pack, Severity},
    LintLevel,
};
use rustsec::advisory::{Informational, Metadata, Versions};

impl IgnoreId {
    fn to_labels(&self, id: FileId, msg: impl Into<String>) -> Vec<Label> {
        let mut v = Vec::with_capacity(self.reason.as_ref().map_or(1, |_| 2));
        v.push(Label::primary(id, self.id.span).with_message(msg));

        if let Some(reason) = &self.reason {
            v.push(Label::secondary(id, reason.0.span).with_message("ignore reason"));
        }

        v
    }
}

#[derive(
    strum::Display,
    strum::EnumString,
    strum::EnumIter,
    strum::IntoStaticStr,
    Copy,
    Clone,
    Debug,
    PartialEq,
    Eq,
)]
#[strum(serialize_all = "kebab-case")]
pub enum Code {
    Vulnerability,
    Notice,
    Unmaintained,
    Unsound,
    Yanked,
    AdvisoryIgnored,
    YankedIgnored,
    IndexFailure,
    IndexCacheLoadFailure,
    AdvisoryNotDetected,
    YankedNotDetected,
    UnknownAdvisory,
}

impl From<Code> for String {
    fn from(c: Code) -> Self {
        c.to_string()
    }
}

fn get_notes_from_advisory(advisory: &Metadata) -> Vec<String> {
    let mut n = vec![format!("ID: {}", advisory.id)];
    if let Some(url) = advisory.id.url() {
        n.push(format!("Advisory: {url}"));
    }

    n.push(advisory.description.clone());

    if let Some(url) = &advisory.url {
        n.push(format!("Announcement: {url}"));
    }

    n
}

impl<'a> crate::CheckCtx<'a, super::cfg::ValidConfig> {
    pub(crate) fn diag_for_advisory<F>(
        &self,
        krate: &crate::Krate,
        krate_index: krates::NodeId,
        advisory: &Metadata,
        versions: Option<&Versions>,
        mut on_ignore: F,
    ) -> Pack
    where
        F: FnMut(usize),
    {
        #[derive(Clone, Copy)]
        enum AdvisoryType {
            Vulnerability,
            Notice,
            Unmaintained,
            Unsound,
        }

        let mut pack = Pack::with_kid(Check::Advisories, krate.id.clone());

        let (severity, ty) = {
            let adv_ty = advisory.informational.as_ref().map_or(AdvisoryType::Vulnerability, |info| {
                match info {
                    // Crate is unmaintained / abandoned
                    Informational::Unmaintained => AdvisoryType::Unmaintained,
                    Informational::Unsound => AdvisoryType::Unsound,
                    Informational::Notice => AdvisoryType::Notice,
                    Informational::Other(other) => {
                        unreachable!("rustsec only returns Informational::Other({other}) advisories if we ask, and there are none at the moment to ask for");
                    }
                    _ => unreachable!("non_exhaustive enums are the worst"),
                }
            });

            // Ok, we found a crate whose version lies within the range of an
            // advisory, but the user might have decided to ignore it
            // for "reasons", but in that case we still emit it to the log
            // so it doesn't just disappear into the aether
            let lint_level = if let Ok(index) = self
                .cfg
                .ignore
                .binary_search_by(|i| i.id.value.cmp(&advisory.id))
            {
                on_ignore(index);

                pack.push(
                    Diagnostic::note()
                        .with_message("advisory ignored")
                        .with_code(Code::AdvisoryIgnored)
                        .with_labels(
                            self.cfg.ignore[index]
                                .to_labels(self.cfg.file_id, "advisory ignored here"),
                        ),
                );

                LintLevel::Allow
            } else if let Some(deprecated) = &self.cfg.deprecated {
                'll: {
                    if let (Some(st), Some(sev)) = (
                        deprecated.severity_threshold,
                        advisory.cvss.as_ref().map(|c| c.severity()),
                    ) {
                        if sev < st {
                            break 'll LintLevel::Allow;
                        }
                    }

                    match adv_ty {
                        AdvisoryType::Vulnerability => deprecated.vulnerability,
                        AdvisoryType::Unmaintained => deprecated.unmaintained,
                        AdvisoryType::Unsound => deprecated.unsound,
                        AdvisoryType::Notice => deprecated.notice,
                    }
                }
            } else {
                LintLevel::Deny
            };

            (lint_level.into(), adv_ty)
        };

        let mut notes = get_notes_from_advisory(advisory);

        if let Some(versions) = versions {
            if versions.patched().is_empty() {
                notes.push("Solution: No safe upgrade is available!".to_owned());
            } else {
                notes.push(format!(
                    "Solution: Upgrade to {} (try `cargo update -p {}`)",
                    versions
                        .patched()
                        .iter()
                        .map(ToString::to_string)
                        .collect::<Vec<_>>()
                        .as_slice()
                        .join(" OR "),
                    krate.name,
                ));
            }
        };

        let (message, code) = match ty {
            AdvisoryType::Vulnerability => ("security vulnerability detected", Code::Vulnerability),
            AdvisoryType::Notice => ("notice advisory detected", Code::Notice),
            AdvisoryType::Unmaintained => ("unmaintained advisory detected", Code::Unmaintained),
            AdvisoryType::Unsound => ("unsound advisory detected", Code::Unsound),
        };

        let diag = pack.push(
            Diagnostic::new(severity)
                .with_message(advisory.title.clone())
                .with_labels(vec![self
                    .krate_spans
                    .label_for_index(krate_index.index(), message)])
                .with_code(code)
                .with_notes(notes),
        );

        if self.serialize_extra {
            diag.extra = serde_json::to_value(advisory).ok().map(|v| ("advisory", v));
        }

        pack
    }

    pub(crate) fn diag_for_yanked(
        &self,
        krate: &crate::Krate,
        krate_index: krates::NodeId,
    ) -> Pack {
        let mut pack = Pack::with_kid(Check::Advisories, krate.id.clone());
        pack.push(
            Diagnostic::new(self.cfg.yanked.value.into())
                .with_message(format!(
                    "detected yanked crate (try `cargo update -p {}`)",
                    krate.name
                ))
                .with_code(Code::Yanked)
                .with_labels(vec![self
                    .krate_spans
                    .label_for_index(krate_index.index(), "yanked version")]),
        );

        pack
    }

    pub(crate) fn diag_for_yanked_ignore(&self, krate: &crate::Krate, ignore: usize) -> Pack {
        let mut pack = Pack::with_kid(Check::Advisories, krate.id.clone());
        pack.push(
            Diagnostic::note()
                .with_message(format!("yanked crate '{krate}' detected, but ignored",))
                .with_code(Code::YankedIgnored)
                .with_labels(self.cfg.ignore_yanked[ignore].to_labels(Some("yanked ignore"))),
        );

        pack
    }

    pub(crate) fn diag_for_index_failure<D: std::fmt::Display>(
        &self,
        krate: &crate::Krate,
        krate_index: krates::NodeId,
        error: D,
    ) -> Pack {
        let mut labels = vec![self.krate_spans.label_for_index(
            krate_index.index(),
            "crate whose registry we failed to query",
        )];

        // Don't show the config location if it's the default, since it just points
        // to the beginning and confuses users
        if !self.cfg.yanked.span.is_empty() {
            labels.push(
                Label::primary(self.cfg.file_id, self.cfg.yanked.span)
                    .with_message("lint level defined here"),
            );
        }

        let mut pack = Pack::with_kid(Check::Advisories, krate.id.clone());
        pack.push(
            Diagnostic::new(Severity::Warning)
                .with_message("unable to check for yanked crates")
                .with_code(Code::IndexFailure)
                .with_labels(labels)
                .with_notes(vec![error.to_string()]),
        );
        pack
    }

    pub fn diag_for_index_load_failure(&self, error: impl std::fmt::Display) -> Pack {
        (
            Check::Advisories,
            Diagnostic::new(Severity::Error)
                .with_message("failed to load index cache")
                .with_code(Code::IndexCacheLoadFailure)
                .with_notes(vec![error.to_string()]),
        )
            .into()
    }

    pub(crate) fn diag_for_advisory_not_encountered(&self, not_hit: &IgnoreId) -> Pack {
        (
            Check::Advisories,
            Diagnostic::new(Severity::Warning)
                .with_message("advisory was not encountered")
                .with_code(Code::AdvisoryNotDetected)
                .with_labels(
                    not_hit.to_labels(self.cfg.file_id, "no crate matched advisory criteria"),
                ),
        )
            .into()
    }

    #[allow(clippy::unused_self)]
    pub(crate) fn diag_for_ignored_yanked_not_encountered(
        &self,
        not_hit: &crate::bans::SpecAndReason,
    ) -> Pack {
        (
            Check::Advisories,
            Diagnostic::new(Severity::Warning)
                .with_message("yanked crate was not encountered")
                .with_code(Code::YankedNotDetected)
                .with_labels(not_hit.to_labels(Some("yanked crate not detected"))),
        )
            .into()
    }

    pub(crate) fn diag_for_unknown_advisory(&self, unknown: &IgnoreId) -> Pack {
        (
            Check::Advisories,
            Diagnostic::new(Severity::Warning)
                .with_message("advisory not found in any advisory database")
                .with_code(Code::UnknownAdvisory)
                .with_labels(unknown.to_labels(self.cfg.file_id, "unknown advisory")),
        )
            .into()
    }
}