use crate::{cargo::Lockfile, Category, Location, Severity};
use dyn_iter::{DynIter, IntoDynIterator as _};
use eyre::Result;
use std::io::{BufRead as _, BufReader};
use tracing::warn;
const DENY_ENGINE: &str = "deny";
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub struct GraphNode {
name: String,
version: String,
#[serde(default)]
kind: String,
#[serde(default)]
repeat: bool,
#[serde(default)]
parents: Vec<GraphNode>,
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub struct Label {
line: usize,
column: usize,
message: String,
span: String,
}
type AdvisoryReport = rustsec::advisory::Metadata;
#[derive(Debug, serde::Deserialize)]
pub struct LicenseReport {
code: String,
graphs: Vec<GraphNode>,
message: String,
labels: Vec<Label>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Report {
Advisory { advisory: Box<AdvisoryReport> },
License(Box<LicenseReport>),
}
pub struct Deny<'lock> {
issues: DynIter<'lock, Issue<'lock>>,
}
impl<'lock> Deny<'lock> {
#[inline]
pub fn try_new<R>(json_read: R, lockfile: &'lock Lockfile) -> Result<Self>
where
R: std::io::Read + 'static,
{
let reader = BufReader::new(json_read);
let issues = reader
.lines()
.map_while(Result::ok)
.flat_map(|line| serde_json::from_str::<serde_json::Value>(&line))
.filter_map(|value| value.get("fields").map(ToString::to_string))
.filter_map(move |s| match serde_json::from_str::<Report>(&s) {
Ok(deny_report) => Some((lockfile, deny_report)),
Err(e) => {
warn!("failed to deserialize '{s}': {e}");
None
}
})
.into_dyn_iter();
let deny = Self { issues };
Ok(deny)
}
}
impl<'lock> Iterator for Deny<'lock> {
type Item = Issue<'lock>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.issues.next()
}
}
pub type Issue<'lock> = (&'lock Lockfile, Report);
impl crate::Issue for Issue<'_> {
#[inline]
fn analyzer_id(&self) -> String {
DENY_ENGINE.to_owned()
}
#[inline]
fn issue_id(&self) -> String {
match self.1 {
Report::Advisory { ref advisory } => advisory.id.to_string(),
Report::License(ref license) => license.code.clone(),
}
}
#[inline]
fn fingerprint(&self) -> md5::Digest {
match self.1 {
Report::Advisory { ref advisory } => {
md5::compute(format!("{}:{}", advisory.package, advisory.id))
}
Report::License(ref license) => md5::compute(license.code.clone()),
}
}
#[inline]
fn category(&self) -> Category {
match self.1 {
Report::Advisory { .. } => Category::Security,
Report::License(_) => Category::Style,
}
}
#[inline]
fn severity(&self) -> Severity {
match self.1 {
Report::Advisory { .. } => Severity::Major,
Report::License(_) => Severity::Info,
}
}
#[inline]
fn location(&self) -> Option<Location> {
let crate_name = match self.1 {
Report::Advisory { ref advisory } => advisory.package.as_str().to_owned(),
Report::License(ref license) => license.graphs.first()?.name.as_str().to_owned(),
};
let message = match self.1 {
Report::Advisory { ref advisory } => format!(
"{} (see https://github.com/rustsec/advisory-db/blob/main/crates/{}/{}.md)",
advisory.title, advisory.package, advisory.id
),
Report::License(ref license) => license
.labels
.iter()
.fold(license.message.clone(), |message, label| {
format!("{}.\n`{}` {}", message, label.span, label.message)
}),
};
let location = Location {
path: self.0.lockfile_path.clone(),
range: self.0.dependency_range(&crate_name),
message,
};
Some(location)
}
}
#[cfg(test)]
mod tests {
use crate::{
Category, Issue as _, Severity, TextRange,
{cargo::PackageRange, deny::Deny, Lockfile},
};
use std::{io::Write as _, path::PathBuf};
use test_log::test;
#[test]
fn single_issue() {
let json = r#"{
"fields": {
"code": "B004",
"graphs": [
{
"name": "tracing-subscriber",
"parents": [
{
"name": "tracing-error",
"parents": [
{
"name": "color-eyre",
"parents": [
{
"name": "cargo-sonar",
"version": "0.8.1"
}
],
"version": "0.5.11"
},
{
"name": "color-spantrace",
"parents": [
{
"name": "color-eyre",
"repeat": true,
"version": "0.5.11"
}
],
"version": "0.1.6"
}
],
"version": "0.1.2"
}
],
"version": "0.2.25"
},
{
"name": "tracing-subscriber",
"parents": [
{
"name": "cargo-sonar",
"version": "0.8.1"
}
],
"version": "0.3.2"
}
],
"labels": [
{
"column": 1,
"line": 93,
"message": "lock entries",
"span": "tracing-subscriber 0.2.25 registry+https://github.com/rust-lang/crates.io-index\ntracing-subscriber 0.3.2 registry+https://github.com/rust-lang/crates.io-index"
}
],
"message": "found 2 duplicate entries for crate 'tracing-subscriber'",
"severity": "warning"
},
"type": "diagnostic"
}"#;
let json = json.to_owned().replace('\n', "");
let mut deny_json = tempfile::NamedTempFile::new().unwrap();
write!(deny_json, "{}", json).unwrap();
let deny_json = deny_json.reopen().unwrap();
let lockfile = Lockfile {
lockfile_path: PathBuf::from("Cargo.lock"),
dependencies: [(
"tracing-subscriber".to_owned(),
PackageRange {
range: TextRange::new((935, 1), (951, 2)),
name_range: TextRange::new((936, 9), (936, 26)),
version_range: TextRange::new((937, 12), (937, 17)),
},
)]
.into_iter()
.collect(),
};
let mut deny = Deny::try_new(deny_json, &lockfile).unwrap();
let issue = deny.next().unwrap();
assert_eq!(issue.analyzer_id(), "deny");
assert_eq!(issue.issue_uid(), "deny::B004");
assert!(matches!(issue.severity(), Severity::Info));
assert!(matches!(issue.category(), Category::Style));
let location = issue.location().unwrap();
assert_eq!(location.path, PathBuf::from("Cargo.lock"));
assert_eq!(location.message, "found 2 duplicate entries for crate 'tracing-subscriber'.\n`tracing-subscriber 0.2.25 registry+https://github.com/rust-lang/crates.io-index\ntracing-subscriber 0.3.2 registry+https://github.com/rust-lang/crates.io-index` lock entries");
assert_eq!(location.range.start.line, 935);
assert_eq!(location.range.end.line, 951);
assert_eq!(location.range.start.column, 1);
assert_eq!(location.range.end.column, 2);
}
#[test]
fn parsing_deny() {
let lockfile = Lockfile {
lockfile_path: PathBuf::from("tests/fixtures/Cargo.lock"),
dependencies: [].into_iter().collect(),
};
let deny = Deny::try_new(
std::fs::File::open("tests/fixtures/deny-2.json").unwrap(),
&lockfile,
)
.unwrap();
let issues = deny.collect::<Vec<_>>();
assert_eq!(issues.len(), 1);
}
}