Merge pull request #157 from RustSec/linter

Upgrade to `rustsec` crate v0.13.0-alpha1; add linter
This commit is contained in:
Tony Arcieri
2019-09-09 12:56:38 -07:00
committed by GitHub
22 changed files with 266 additions and 166 deletions

View File

@@ -10,6 +10,7 @@ publish = false
name = "rustsec-advisory-db"
[dependencies]
gumdrop = "0.6"
rustsec = "0.12"
crates_io_api = "0.5"
gumdrop = "0.6"
rustsec = "0.13.0-alpha1"
termcolor = "1"

View File

@@ -37,7 +37,7 @@ See [CONTRIBUTING.md] for more information.
Each advisory contains information in [TOML] format:
```toml
# Before you submit a PR using this template, please delete the comments
# Before you submit a PR using this template, **please delete the comments**
# explaining each field, as well as any unused fields.
[advisory]
@@ -73,7 +73,12 @@ patched_versions = [">= 1.2.0"]
# a change log entry, or a blogpost announcing the release (optional)
url = "https://github.com/mystuff/mycrate/issues/123"
# Keywords which describe this vulnerability, similar to Cargo (optional)
# Optional: Categories this advisory falls under. Valid categories are:
# "code-execution", "crypto-failure", "denial-of-service", "file-disclosure"
# "format-injection", "memory-corruption", "memory-exposure", "privilege-escalation"
categories = ["crypto-failure"]
# Freeform keywords which describe this vulnerability, similar to Cargo (optional)
keywords = ["ssl", "mitm"]
# Vulnerability aliases, e.g. CVE IDs (optional but recommended)
@@ -84,22 +89,28 @@ keywords = ["ssl", "mitm"]
# e.g. CVE for a C library wrapped by a -sys crate)
#references = ["CVE-2018-YYYY", "CVE-2018-ZZZZ"]
# CPU architectures impacted by this vulnerability (optional)
# Optional: metadata which narrows the scope of what this advisory affects
[affected]
# CPU architectures impacted by this vulnerability (optional).
# Only use this if the vulnerability is specific to a particular CPU architecture,
# e.g. the vulnerability is in x86 assembly.
# For a list of CPU architecture strings, see the "platforms" crate:
# <https://docs.rs/platforms/latest/platforms/target/enum.Arch.html>
#affected_arch = ["x86", "x86_64"]
#arch = ["x86", "x86_64"]
# Operating systems impacted by this vulnerability (optional)
# Only use this if the vulnerable is specific to a particular OS, e.g. it was
# located in a binding to a Windows-specific API.
# For a list of OS strings, see the "platforms" crate:
# <https://docs.rs/platforms/latest/platforms/target/enum.OS.html>
#affected_os = ["windows"]
#os = ["windows"]
# List of canonical paths to vulnerable functions (optional)
# The path syntax is cratename::path::to::function, without any
# return type or parameters. More information:
# <https://github.com/RustSec/advisory-db/issues/68>
# For example, for RUSTSEC-2018-0003, this would look like:
#affected_functions = ["smallvec::SmallVec::insert_many"]
# Table of canonical paths to vulnerable functions (optional)
# mapping to which versions impacted by this advisory used that particular
# name (e.g. if the function was renamed between versions).
# The path syntax is `cratename::path::to::function`, without any
# parameters or additional information, followed by a list of version reqs.
functions = { "mycrate::MyType::vulnerable_function" = ["< 1.2.0, >= 1.1.0"] }
```
## License

View File

@@ -15,8 +15,8 @@ The flaw was corrected by serializing the DOM tree iteratively instead.
patched_versions = [">= 2.1.0"]
url = "https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md#210"
keywords = ["stack-overflow", "crash"]
affected_functions = [
"ammonia::clean",
"ammonia::Document::to_string",
"ammonia::Document::write_to",
]
[affected.functions]
"ammonia::clean" = ["< 2.1.0"]
"ammonia::Document::to_string" = ["< 2.1.0"]
"ammonia::Document::write_to" = ["< 2.1.0"]

View File

@@ -1,12 +1,8 @@
[advisory]
id = "RUSTSEC-2018-0011"
package = "arrayfire"
date = "2018-12-18"
title = "Enum repr causing potential memory corruption"
description = """
The attribute repr() added to enums to be compatible with C-FFI caused
memory corruption on MSVC toolchain.
@@ -19,15 +15,12 @@ The issue seems to be interlinked with which version of Rust is being used.
The issue was fixed in crate 3.6.0.
"""
patched_versions = [">= 3.6.0"]
unaffected_versions = ["<= 3.5.0"]
url = "https://github.com/arrayfire/arrayfire-rust/pull/177"
categories = ["memory-corruption"]
keywords = ["enum", "repr"]
keywords = ["enum", "repr", "memory-corruption"]
affected_arch = ["x86_64"]
affected_os = ["windows"]
[affected]
arch = ["x86_64"]
os = ["windows"]

View File

@@ -17,4 +17,4 @@ impact `Digest` functionality.
"""
patched_versions = [">= 0.8.1"]
url = "https://github.com/RustCrypto/MACs/issues/19"
category = ["crypto-failure"]
categories = ["crypto-failure"]

View File

@@ -7,13 +7,16 @@ description = """
Affected versions of this crate did not properly implement the generativity,
because the invariant lifetimes were not necessarily `drop`ped.
This allows an attacker to mix up two arenas, using indices created from one
arena with another one. This might lead to an out-of-bounds read or write
This allows an attacker to mix up two arenas, using indices created from one
arena with another one. This might lead to an out-of-bounds read or write
access into the memory reserved for the arena.
The flaw was corrected by implementing generativity correctly in version 0.4.0.
"""
patched_versions = [">= 0.4.0"]
url = "https://github.com/llogiq/compact_arena/issues/22"
keywords = ["memory-corruption", "uninitialized-memory"]
affected_functions = ["compact_arena::SmallArena::new"]
categories = ["memory-corruption"]
keywords = ["uninitialized-memory"]
[affected.functions]
"compact_arena::SmallArena::new" = ["< 0.4.0"]

View File

@@ -4,8 +4,8 @@ package = "hyper"
date = "2016-05-09"
url = "https://github.com/hyperium/hyper/blob/master/CHANGELOG.md#v094-2016-05-09"
title = "HTTPS MitM vulnerability due to lack of hostname verification"
categories = ["crypto-failure"]
keywords = ["ssl", "mitm"]
affected_os = ["windows"]
patched_versions = [">= 0.9.4"]
references = ["RUSTSEC-2016-0001"]
description = """
@@ -18,3 +18,6 @@ CA-issued certificate, even if there's a hostname mismatch.
The problem was addressed by leveraging rust-openssl's built-in support for
hostname verification.
"""
[affected]
os = ["windows"]

View File

@@ -11,7 +11,7 @@ initializing all instances.
This could run Drop implementations on uninitialized types, equivalent to
use-after-free, and allow an attacker arbitrary code execution.
Two different fixes were applied. It is possible to conserve the interface by
ensuring proper initialization before calling `Vec::set_len`. Drop is no longer
called in case of panic, though.
@@ -24,4 +24,6 @@ patched_versions = [">= 0.21.3"]
unaffected_versions = ["< 0.10.2"]
url = "https://github.com/image-rs/image/pull/985"
keywords = ["drop", "use-after-free"]
affected_functions = ["image::hdr::HDRDecoder::read_image_transform"]
[affected.functions]
"image::hdr::HDRDecoder::read_image_transform" = ["< 0.21.3, >= 0.10.2"]

View File

@@ -4,14 +4,16 @@ package = "libflate"
date = "2019-07-04"
title = "MultiDecoder::read() drops uninitialized memory of arbitrary type on panic in client code"
description = """
Affected versions of libflate have set a field of an internal structure with a generic type to an uninitialized value in `MultiDecoder::read()` and reverted it to the original value after the function completed. However, execution of `MultiDecoder::read()` could be interrupted by a panic in caller-supplied `Read` implementation. This would cause `drop()` to be called on uninitialized memory of a generic type implementing `Read`.
Affected versions of libflate have set a field of an internal structure with a generic type to an uninitialized value in `MultiDecoder::read()` and reverted it to the original value after the function completed. However, execution of `MultiDecoder::read()` could be interrupted by a panic in caller-supplied `Read` implementation. This would cause `drop()` to be called on uninitialized memory of a generic type implementing `Read`.
This is equivalent to a use-after-free vulnerability and could allow an attacker to gain arbitrary code execution.
The flaw was corrected by aborting immediately instead of unwinding the stack in case of panic within `MultiDecoder::read()`. The issue was discovered and fixed by Shnatsel.
"""
patched_versions = [">= 0.1.25"]
unaffected_versions = ["< 0.1.14"]
url = "https://github.com/sile/libflate/issues/35"
keywords = ["drop", "use-after-free"]
affected_functions = ["libflate::gzip::MultiDecoder::read"]
[affected.functions]
"libflate::gzip::MultiDecoder::read" = ["< 0.1.25, >= 0.1.14"]

View File

@@ -2,9 +2,7 @@
id = "RUSTSEC-2019-0006"
package = "ncurses"
date = "2019-06-15"
title = "Buffer overflow and format vulnerabilities in functions exposed without unsafe"
description = """
`ncurses` exposes functions from the ncurses library which:
@@ -14,9 +12,12 @@ description = """
input to execute a format string attack, which trivially allows writing
arbitrary data to stack memory (functions in the `printw` family).
"""
patched_versions = []
url = "https://github.com/RustSec/advisory-db/issues/106"
affected_functions = ["ncurses::instr", "ncurses::mvwinstr", "ncurses::printw", "ncurses::mvprintw", "ncurses::mvwprintw"]
[affected.functions]
"ncurses::instr" = [">= 0"]
"ncurses::mvwinstr" = [">= 0"]
"ncurses::printw" = [">= 0"]
"ncurses::mvprintw" = [">= 0"]
"ncurses::mvwprintw" = [">= 0"]

View File

@@ -23,9 +23,8 @@ url = "https://github.com/matklad/once_cell/issues/46"
keywords = ["undefined_behavior"]
affected_functions = [
"once_cell::unsync::Lazy::force",
"once_cell::unsync::Lazy::deref",
"once_cell::sync::Lazy::force",
"once_cell::sync::Lazy::deref",
]
[affected.functions]
"once_cell::unsync::Lazy::force" = ["< 1.0.1, >= 0.2.5"]
"once_cell::unsync::Lazy::deref" = ["< 1.0.1, >= 0.2.5"]
"once_cell::sync::Lazy::force" = ["< 1.0.1, >= 0.2.5"]
"once_cell::sync::Lazy::deref" = ["< 1.0.1, >= 0.2.5"]

View File

@@ -2,17 +2,15 @@
id = "RUSTSEC-2019-0005"
package = "pancurses"
date = "2019-06-15"
title = "Format string vulnerabilities in `pancurses`"
description = """
`pancurses::mvprintw` and `pancurses::printw` passes a pointer from a rust `&str` to C,
allowing hostile input to execute a format string attack, which trivially allows writing
arbitrary data to stack memory.
"""
patched_versions = []
url = "https://github.com/RustSec/advisory-db/issues/106"
affected_functions = ["pancurses::mvprintw", "pancurses::printw"]
[affected.functions]
"pancurses::mvprintw" = [">= 0"]
"pancurses::printw" = [">= 0"]

View File

@@ -10,6 +10,9 @@ This allows an attacker to cause an Out of Memory condition while calling the
vulnerable method on untrusted data.
"""
url = "https://github.com/stepancheg/rust-protobuf/issues/411"
keywords = ["oom", "panic", "dos"]
affected_functions = ["stream::read_raw_bytes_into"]
categories = ["denial-of-service"]
keywords = ["oom", "panic"]
patched_versions = ["^1.7.5", ">= 2.6.0"]
[affected.functions]
"protobuf::stream::read_raw_bytes_into" = ["< 2.6.0"]

View File

@@ -10,13 +10,13 @@ value internally.
This is technically unsound and calling these methods from multiple threads
without synchronization could lead to unexpected and unpredictable behavior.
The flaw was corrected in release 0.5.0.
"""
patched_versions = [">= 0.5.0"]
url = "https://github.com/ebkalderon/renderdoc-rs/pull/32"
keywords = ["undefined_behavior"]
affected_functions = [
"renderdoc::api::RenderDocV110::trigger_multi_frame_capture",
"renderdoc::api::RenderDocV120::set_capture_file_comments",
]
[affected.functions]
"renderdoc::api::RenderDocV110::trigger_multi_frame_capture" = ["< 0.5.0"]
"renderdoc::api::RenderDocV120::set_capture_file_comments" = ["< 0.5.0"]

View File

@@ -2,7 +2,6 @@
id = "RUSTSEC-2018-0013"
package = "safe-transmute"
date = "2018-11-27"
title = "Vec-to-vec transmutations could lead to heap overflow/corruption"
description = """
Affected versions of this crate switched the length and capacity arguments in the Vec::from_raw_parts() constructor,
@@ -10,14 +9,12 @@ which could lead to memory corruption or data leakage.
The flaw was corrected by using the constructor correctly.
"""
patched_versions = [">= 0.10.1"]
unaffected_versions = ["< 0.4.0"]
url = "https://github.com/nabijaczleweli/safe-transmute-rs/pull/36"
keywords = ["memory-corruption"]
[affected_paths]
">= 0.4.0, <= 0.10.0" = ["safe_transmute::guarded_transmute_vec_permissive"]
"= 0.10.0" = ["safe_transmute::guarded_transmute_to_bytes_vec"]
# TODO(tarcieri): fix linter to respect crate name
#[affected.functions]
#"safe_transmute::guarded_transmute_vec_permissive" = [">= 0.4.0, <= 0.10.0"]
#"safe_transmute::guarded_transmute_to_bytes_vec" = ["= 0.10.0"]

View File

@@ -1,12 +1,8 @@
[advisory]
id = "RUSTSEC-2019-0008"
package = "simd-json"
date = "2019-06-24"
title = "Flaw in string parsing can lead to crashes due to invalid memory access."
description = """
The affected version of this crate did not guard against accessing memory
beyond the range of its input data. A pointer cast to read the data into
@@ -22,17 +18,14 @@ segflt | [ 32 | byte ] |
```
This allows an attacker to eventually crash a service.
The flaw was corrected by using a padding buffer for the last read from the
input. So that we are we never read over the boundary of the input data.
"""
patched_versions = [">= 0.1.15"]
unaffected_versions = ["<= 0.1.13"]
url = "https://github.com/Licenser/simdjson-rs/pull/27"
keywords = ["simd"]
affected_arch = ["x86", "x86_64"]
[affected]
arch = ["x86", "x86_64"]

View File

@@ -14,4 +14,6 @@ patched_versions = [">= 0.6.10"]
unaffected_versions = ["< 0.6.5"]
url = "https://github.com/servo/rust-smallvec/issues/148"
keywords = ["double free", "use after free", "arbitrary code execution"]
affected_functions = ["smallvec::SmallVec::grow"]
[affected.functions]
"smallvec::SmallVec::grow" = ["< 0.6.10, >= 0.6.5"]

View File

@@ -13,5 +13,7 @@ Credits to @ehuss for discovering, reporting and fixing the bug.
patched_versions = [">= 0.6.10"]
unaffected_versions = ["< 0.6.3"]
url = "https://github.com/servo/rust-smallvec/issues/149"
keywords = ["memory corruption", "arbitrary code execution"]
affected_functions = ["smallvec::SmallVec::grow"]
categories = ["code-execution", "memory-corruption"]
[affected.functions]
"smallvec::SmallVec::grow" = ["< 0.6.10, >= 0.6.3"]

View File

@@ -9,10 +9,12 @@ Wrong memory orderings inside the RwLock implementation allow for two writers to
Only users of the RwLock implementation are affected. Users of Once (including users of lazy_static with the `spin_no_std` feature enabled) are NOT affected.
On strongly ordered CPU architectures like x86, the only real way that this would lead to a memory corruption is if the compiler reorders an access after the lock is yielded, which is possible but in practice unlikely. It is a more serious issue on weakly ordered architectures such as ARM which, except in the presence of certain instructions, allow the hardware to decide which accesses are seen at what times. Therefore on an ARM system it is likely that using the wrong memory ordering would result in a memory corruption, even if the compiler itself doesn't reorder the memory accesses in a buggy way.
The flaw was corrected by https://github.com/mvdnes/spin-rs/pull/66.
"""
patched_versions = [">= 0.5.2"]
url = "https://github.com/mvdnes/spin-rs/issues/65"
keywords = ["atomic", "ordering", "spin", "lock", "mutex", "rwlock"]
affected_functions = ["spin::RwLock::new"]
[affected.functions]
"spin::RwLock::new" = ["< 0.5.2"]

View File

@@ -12,4 +12,6 @@ patched_versions = [">= 1.22.0"]
unaffected_versions = ["< 1.3.0"]
categories = ["code-execution", "denial-of-service"]
url = "https://github.com/rust-lang/rust/issues/44800"
affected_functions = ["std::collections::vec_deque::VecDeque::reserve"]
[affected.functions]
"std::collections::vec_deque::VecDeque::reserve" = ["< 1.22.0, >= 1.3.0"]

View File

@@ -96,4 +96,6 @@ unaffected_versions = ["< 1.26.0"]
url = "https://groups.google.com/forum/#!topic/rustlang-security-announcements/CmSuTm-SaU0"
categories = ["denial-of-service", "memory-corruption"]
cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
affected_functions = ["std::str::repeat"]
[affected.functions]
"std::str::repeat" = ["< 1.29.1, >= 1.26.0"]

View File

@@ -1,120 +1,204 @@
//! RustSec Advisory DB Linter
#![allow(clippy::never_loop)]
use gumdrop::Options;
use rustsec::{AdvisoryDatabase, Repository};
use std::{env, process::exit};
use rustsec::package;
use std::{env, io::Write, path::PathBuf, process::exit};
use termcolor::{
Color::{Green, Red},
ColorChoice, ColorSpec, StandardStream, WriteColor,
};
const MIN_EXPECTED_ADVISORIES: usize = 5;
/// Minimum number of advisories we expect in the database
const MIN_EXPECTED_ADVISORIES: usize = 10;
/// Subcommands
#[derive(Debug, Options)]
enum Opts {
#[options(help = "show help for a command")]
Help(HelpOpts),
macro_rules! writeln_color {
($stream:expr, $color:path, $fmt:expr, $msg:expr) => {
let mut color = ColorSpec::new();
color.bold();
color.set_fg(Some($color));
$stream.set_color(&color).unwrap();
#[options(help = "check the advisory DB is well-formed")]
Check(CheckOpts),
writeln!($stream, $fmt, $msg).unwrap();
$stream.reset().unwrap();
};
}
/// Options for the `help` command
#[derive(Debug, Default, Options)]
struct HelpOpts {
#[options(free)]
commands: Vec<String>,
macro_rules! writeln_success {
($stream:expr, $msg:expr) => {
writeln_color!($stream, Green, "✔ {}", $msg);
};
($stream:expr, $fmt:expr, $($arg:tt)+) => {
writeln_success!($stream, format!($fmt, $($arg)+));
}
}
/// Options for the `check` command
#[derive(Debug, Default, Options)]
struct CheckOpts {}
macro_rules! writeln_error {
($stream:expr, $msg:expr) => {
writeln_color!($stream, Red, "✘ {}", $msg);
};
($stream:expr, $fmt:expr, $($arg:tt)+) => {
writeln_error!($stream, format!($fmt, $($arg)+));
}
}
fn main() {
let args: Vec<_> = env::args().collect();
let opts = Opts::parse_args_default(&args[1..]).unwrap_or_else(|e| {
let opts = Commands::parse_args_default(&args[1..]).unwrap_or_else(|e| {
match e.to_string().as_ref() {
// Show usage if no command name is given or if "help" is given
"missing command name" => help(&[]),
"missing command name" => help(),
string => eprintln!("{}: {}", args[0], string),
}
exit(2);
exit(1);
});
match opts {
Opts::Help(opts) => help(&opts.commands),
Opts::Check(_) => check(),
Commands::Help(_) => help(),
Commands::Check(check) => check.run(),
}
}
exit(0);
/// Subcommands
#[derive(Debug, Options)]
enum Commands {
#[options(help = "show help for a command")]
Help(HelpCommand),
#[options(help = "check the advisory DB is well-formed")]
Check(CheckCommand),
}
/// Options for the `help` command
#[derive(Debug, Default, Options)]
struct HelpCommand {
#[options(free)]
commands: Vec<String>,
}
/// Print help message
fn help(_commands: &[String]) {
fn help() {
println!("Usage: {} [COMMAND] [OPTIONS]", env::args().next().unwrap());
println!();
println!("Available commands:");
println!();
println!("{}", Opts::command_list().unwrap());
println!("{}", Commands::command_list().unwrap());
println!();
}
fn check() {
let repo = Repository::open(".").unwrap();
/// Options for the `check` command
#[derive(Debug, Default, Options)]
struct CheckCommand {}
// Ensure Advisories.toml parses
let db = AdvisoryDatabase::from_repository(&repo).unwrap();
let advisories = db.advisories();
impl CheckCommand {
fn run(&self) {
let mut stdout = StandardStream::stdout(ColorChoice::Auto);
let repo = rustsec::Repository::open(".").unwrap();
// Ensure we're parsing some advisories
if advisories.len() > MIN_EXPECTED_ADVISORIES {
println!(
"*** Check succeeded! Successfully parsed {} advisories.",
advisories.len()
);
} else {
panic!(
"Missing advisories! Expected at least {}, but got {}",
MIN_EXPECTED_ADVISORIES,
advisories.len()
);
// Ensure Advisories.toml parses
let db = rustsec::Database::load(&repo).unwrap();
let advisories = db.iter();
// Ensure we're parsing some advisories
if advisories.len() > MIN_EXPECTED_ADVISORIES {
writeln_success!(
&mut stdout,
"Successfully parsed {} advisories",
advisories.len()
);
} else {
writeln_error!(
&mut stdout,
"Missing advisories! Expected at least {}, but got {}",
MIN_EXPECTED_ADVISORIES,
advisories.len()
);
exit(1);
}
let cratesio_client = crates_io_api::SyncClient::new();
let mut invalid_advisories = 0;
for advisory in advisories {
if !self.check_advisory(&mut stdout, &cratesio_client, advisory) {
invalid_advisories += 1;
}
}
if invalid_advisories == 0 {
writeln_success!(&mut stdout, "All advisories are well-formed");
} else {
writeln_error!(
&mut stdout,
"{} advisories contain errors!",
invalid_advisories
);
exit(1);
}
}
let cratesio_client = crates_io_api::SyncClient::new();
for advisory in advisories {
check_advisory(&cratesio_client, advisory);
}
println!("*** Check succeeded! All advisories refer to valid crates.");
}
fn check_advisory(cratesio_client: &crates_io_api::SyncClient, advisory: &rustsec::Advisory) {
let response = cratesio_client
.get_crate(advisory.package.as_str())
.unwrap_or_else(|_| {
panic!(
"Failed to get package from crates.io: {}",
advisory.package.as_str()
)
});
if response.crate_data.name != advisory.package.as_str() {
panic!(
"crates.io package name does not match package name in advisory for {}",
advisory.package.as_str()
);
}
// Check that each path in `affected_paths` starts with the crate name
if let Some(ref version_req_paths) = advisory.affected_paths {
for (_, paths) in version_req_paths.iter() {
for path in paths {
if path.crate_name() != response.crate_data.name {
panic!(
"{}: affected_path does not begin with crate name: {}",
response.crate_data.name,
path.crate_name()
)
fn check_advisory(
&self,
stdout: &mut StandardStream,
cratesio_client: &crates_io_api::SyncClient,
advisory: &rustsec::Advisory,
) -> bool {
if advisory.metadata.collection == Some(package::Collection::Crates) {
match cratesio_client.get_crate(advisory.metadata.package.as_str()) {
Ok(response) => {
if response.crate_data.name != advisory.metadata.package.as_str() {
writeln_error!(
stdout,
"crates.io package name does not match package name in advisory for {}",
advisory.metadata.package.as_str()
);
return false;
}
}
Err(err) => {
writeln_error!(
stdout,
"Failed to get package `{}` from crates.io: {}",
advisory.metadata.package.as_str(),
err
);
return false;
}
}
}
let mut advisory_path = PathBuf::from(".")
.join(advisory.metadata.collection.as_ref().unwrap().to_string())
.join(advisory.metadata.package.as_str())
.join(advisory.metadata.id.as_str());
advisory_path.set_extension("toml");
let lint = rustsec::advisory::Linter::lint_file(&advisory_path).unwrap();
if lint.errors().is_empty() {
writeln_success!(
stdout,
"{} successfully passed lint",
advisory_path.display()
);
true
} else {
writeln_error!(
stdout,
"{} contained the following lint errors:",
advisory_path.display()
);
for error in lint.errors() {
writeln!(stdout, " - {}", error).unwrap();
}
false
}
}
}