Skip to content

Commit c9aa5b7

Browse files
authored
Add advisories.unmaintained back (#753)
Adds back the `unmaintained` field for the advisories config, but it now uses a scope rather than a lint level. Resolves: #752
1 parent fd67c49 commit c9aa5b7

16 files changed

+723
-80
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
<!-- next-header -->
1010
## [Unreleased] - ReleaseDate
11+
### Added
12+
- [PR#753](https://github.com/EmbarkStudios/cargo-deny/pull/753) resolved [#752](https://github.com/EmbarkStudios/cargo-deny/issues/752) by adding back the `advisories.unmaintained` config option. See the [docs](https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html#the-unmaintained-field-optional) for how it can be used. The default matches the current behavior, which is to error on any `unmaintained` advisory, but adding `unmaintained = "workspace"` to the `[advisories]` table will mean unmaintained advisories will only error if the crate is a direct dependency of your workspace.
13+
1114
## [0.18.1] - 2025-02-27
1215
### Fixed
1316
- [PR#749](https://github.com/EmbarkStudios/cargo-deny/pull/749) updated `krates` to pull in the fix for [EmbarkStudios/krates#100](https://github.com/EmbarkStudios/krates/issues/100).

Cargo.lock

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deny.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ targets = [
1111
all-features = true
1212

1313
[advisories]
14-
version = 2
14+
unmaintained = "workspace"
1515
ignore = [
1616
]
1717

docs/src/checks/advisories/README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ You can also use your own advisory databases instead of, or in addition to, the
1414

1515
## Use Case - Detecting unmaintained crates
1616

17-
The [advisory database](https://github.com/RustSec/advisory-db) also contains advisories for unmaintained crates, which in most cases users will want to avoid in favor of more actively maintained crates.
17+
The [advisory database](https://github.com/RustSec/advisory-db) also contains advisories for unmaintained crates, which in most cases users will want to avoid in favor of more actively maintained crates. By default, all `unmaintained` advisories will result in an error, but by using the following config you can error only if you directly depend on an unmaintained crate from your workspace.
18+
19+
```ini
20+
[advisories]
21+
unmaintained = 'workspace'
22+
```
1823

1924
## Example output
2025

docs/src/checks/advisories/cfg.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ version = 2
3939
The version field is (at the time of this writing) no longer used, the following fields have been removed and will now emit errors.
4040

4141
- `vulnerability` - Removed, all vulnerability advisories now emit errors.
42-
- `unmaintained` - Removed, all unmaintained advisories now emit errors.
4342
- `unsound` - Removed, all unsound advisories now emit errors.
4443
- `notice` - Removed, all notice advisories now emit errors.
4544
- `severity-threshold` - Removed, all vulnerability advisories now emit errors.
@@ -69,6 +68,19 @@ Every advisory in the advisory database contains a unique identifier, eg. `RUSTS
6968

7069
In addition, yanked crate versions can be ignored by specifying a [PackageSpec](../cfg.md#package-spec) with an optional `reason`.
7170

71+
### The `unmaintained` field (optional)
72+
73+
```ini
74+
unmaintained = 'workspace'
75+
```
76+
77+
Determines if ummaintained advisories will result in an error. An unmaintained error can still be ignored specifically via the [`ignore`](#the-ignore-field-optional) option.
78+
79+
- `all` (default) - Any crate that matches an unmaintained advisory will fail
80+
- `workspace` - Unmaintained advisories will only fail if they apply to a crate which is a direct dependency of one or more workspace crates.
81+
- `transitive` - Unmaintained advisories will only fail if they apply to a crate which is **not** a direct dependency of one or more workspace crates.
82+
- `none` - Unmaintained advisories are completely ignored.
83+
7284
### The `git-fetch-with-cli` field (optional)
7385

7486
Similar to cargo's [net.git-fetch-with-cli](https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli), this field allows you to opt-in to fetching advisory databases with the git CLI rather than using `gix`.

docs/src/checks/advisories/diags.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
<!-- markdownlint-disable-next-line heading-increment -->
44
### `vulnerability`
55

6-
A [`vulnerability`](cfg.md#the-vulnerability-field-optional) advisory was detected for a crate.
6+
A `vulnerability` advisory was detected for a crate.
77

88
### `notice`
99

10-
A [`notice`](cfg.md#the-notice-field-optional) advisory was detected for a crate.
10+
A `notice` advisory was detected for a crate.
1111

1212
### `unmaintained`
1313

1414
An [`unmaintained`](cfg.md#the-unmaintained-field-optional) advisory was detected for a crate.
1515

1616
### `unsound`
1717

18-
An [`unsound`](cfg.md#the-unsound-field-optional) advisory was detected for a crate.
18+
An `unsound` advisory was detected for a crate.
1919

2020
### `yanked`
2121

src/advisories.rs

+48-1
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,55 @@ pub fn check<R, S>(
7373
let mut ignore_hits: BitVec = BitVec::repeat(false, ctx.cfg.ignore.len());
7474
let mut ignore_yanked_hits: BitVec = BitVec::repeat(false, ctx.cfg.ignore_yanked.len());
7575

76+
use crate::cfg::Scope;
77+
let ws_set = if matches!(
78+
ctx.cfg.unmaintained.value,
79+
Scope::Workspace | Scope::Transitive
80+
) {
81+
ctx.krates
82+
.workspace_members()
83+
.filter_map(|wm| {
84+
if let krates::Node::Krate { id, .. } = wm {
85+
Some(id.clone())
86+
} else {
87+
None
88+
}
89+
})
90+
.collect::<std::collections::BTreeSet<_>>()
91+
} else {
92+
Default::default()
93+
};
94+
7695
// Emit diagnostics for any advisories found that matched crates in the graph
77-
for (krate, advisory) in &report.advisories {
96+
'lup: for (krate, advisory) in &report.advisories {
97+
'block: {
98+
if advisory
99+
.metadata
100+
.informational
101+
.as_ref()
102+
.is_some_and(|info| info.is_unmaintained())
103+
{
104+
match ctx.cfg.unmaintained.value {
105+
Scope::All => break 'block,
106+
Scope::None => continue 'lup,
107+
Scope::Workspace | Scope::Transitive => {
108+
let nid = ctx.krates.nid_for_kid(&krate.id).unwrap();
109+
let dds = ctx.krates.direct_dependents(nid);
110+
111+
let transitive = ctx.cfg.unmaintained.value == Scope::Transitive;
112+
if dds
113+
.iter()
114+
.any(|dd| ws_set.contains(&dd.krate.id) ^ transitive)
115+
{
116+
break 'block;
117+
}
118+
119+
continue 'lup;
120+
}
121+
}
122+
}
123+
}
124+
78125
let diag = ctx.diag_for_advisory(
79126
krate,
80127
&advisory.metadata,

src/advisories/cfg.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
LintLevel, PathBuf, Span, Spanned,
3-
cfg::{PackageSpecOrExtended, Reason, ValidationContext},
3+
cfg::{PackageSpecOrExtended, Reason, Scope, ValidationContext},
44
diag::{Diagnostic, FileId, Label},
55
utf8path,
66
};
@@ -75,6 +75,8 @@ pub struct Config {
7575
pub yanked: Spanned<LintLevel>,
7676
/// Ignore advisories for the given IDs
7777
ignore: Vec<Spanned<IgnoreId>>,
78+
/// Whether to error on unmaintained advisories, and for what scope
79+
pub unmaintained: Spanned<Scope>,
7880
/// Ignore yanked crates
7981
pub ignore_yanked: Vec<Spanned<PackageSpecOrExtended<Reason>>>,
8082
/// Use the git executable to fetch advisory database rather than gitoxide
@@ -98,6 +100,7 @@ impl Default for Config {
98100
db_path: None,
99101
db_urls: Vec::new(),
100102
ignore: Vec::new(),
103+
unmaintained: Spanned::new(crate::cfg::Scope::All),
101104
ignore_yanked: Vec::new(),
102105
yanked: Spanned::new(LintLevel::Warn),
103106
git_fetch_with_cli: None,
@@ -145,10 +148,11 @@ impl<'de> Deserialize<'de> for Config {
145148
let mut fdeps = Vec::new();
146149

147150
let _vulnerability = deprecated::<LintLevel>(&mut th, "vulnerability", &mut fdeps);
148-
let _unmaintained = deprecated::<LintLevel>(&mut th, "unmaintained", &mut fdeps);
149151
let _unsound = deprecated::<LintLevel>(&mut th, "unsound", &mut fdeps);
150152
let _notice = deprecated::<LintLevel>(&mut th, "notice", &mut fdeps);
151153

154+
let unmaintained = th.optional_s::<Scope>("unmaintained");
155+
152156
let yanked = th
153157
.optional_s("yanked")
154158
.unwrap_or(Spanned::new(LintLevel::Warn));
@@ -293,6 +297,7 @@ impl<'de> Deserialize<'de> for Config {
293297
db_urls,
294298
yanked,
295299
ignore,
300+
unmaintained: unmaintained.unwrap_or(Spanned::new(Scope::All)),
296301
ignore_yanked,
297302
git_fetch_with_cli,
298303
disable_yank_checking,
@@ -392,6 +397,7 @@ impl crate::cfg::UnvalidatedConfig for Config {
392397
db_path: db_path.unwrap_or_default(), // If we failed to get a path the default won't be used since errors will have occurred
393398
db_urls,
394399
ignore: ignore.into_iter().map(|s| s.value).collect(),
400+
unmaintained: self.unmaintained,
395401
ignore_yanked: ignore_yanked
396402
.into_iter()
397403
.map(|s| crate::bans::SpecAndReason {
@@ -415,6 +421,7 @@ pub struct ValidConfig {
415421
pub db_path: PathBuf,
416422
pub db_urls: Vec<Spanned<Url>>,
417423
pub(crate) ignore: Vec<IgnoreId>,
424+
pub(crate) unmaintained: Spanned<Scope>,
418425
pub(crate) ignore_yanked: Vec<crate::bans::SpecAndReason>,
419426
pub yanked: Spanned<LintLevel>,
420427
pub git_fetch_with_cli: bool,

src/advisories/snapshots/cargo_deny__advisories__cfg__test__deserializes_advisories_cfg-2.snap

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ expression: validated
1414
"reason": null
1515
}
1616
],
17+
"unmaintained": "Workspace",
1718
"ignore_yanked": [
1819
{
1920
"spec": {

src/cfg.rs

+16
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ pub trait UnvalidatedConfig {
5252
fn validate(self, ctx: ValidationContext<'_>) -> Self::ValidCfg;
5353
}
5454

55+
#[derive(Copy, Clone, PartialEq, strum::VariantNames, strum::VariantArray)]
56+
#[cfg_attr(test, derive(serde::Serialize))]
57+
#[strum(serialize_all = "kebab-case")]
58+
pub enum Scope {
59+
/// Matches any crate
60+
All,
61+
/// Matches crates in the workspace
62+
Workspace,
63+
/// Matches external crates
64+
Transitive,
65+
/// Matches no crates
66+
None,
67+
}
68+
69+
crate::enum_deser!(Scope);
70+
5571
#[derive(Clone)]
5672
#[cfg_attr(test, derive(Debug, PartialEq, Eq, serde::Serialize))]
5773
pub struct Reason(pub Spanned<String>);

tests/advisories.rs

+73-13
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,81 @@ fn detects_vulnerabilities() {
8888
fn detects_unmaintained() {
8989
let TestCtx { dbs, krates } = load();
9090

91-
let cfg = tu::Config::new("");
91+
fn unmaintained_advisories(v: Vec<serde_json::Value>) -> Vec<serde_json::Value> {
92+
v.into_iter()
93+
.filter(|diag| {
94+
diag.pointer("/fields/code").and_then(|code| code.as_str()) == Some("unmaintained")
95+
})
96+
.collect()
97+
}
9298

93-
let diags =
94-
tu::gather_diagnostics::<cfg::Config, _, _>(&krates, func_name!(), cfg, |ctx, tx| {
95-
advisories::check(
96-
ctx,
97-
&dbs,
98-
Option::<advisories::NoneReporter>::None,
99-
None,
100-
tx,
101-
);
102-
});
99+
{
100+
let cfg = tu::Config::new("");
101+
102+
let diags =
103+
tu::gather_diagnostics::<cfg::Config, _, _>(&krates, func_name!(), cfg, |ctx, tx| {
104+
advisories::check(
105+
ctx,
106+
&dbs,
107+
Option::<advisories::NoneReporter>::None,
108+
None,
109+
tx,
110+
);
111+
});
112+
113+
insta::assert_json_snapshot!(unmaintained_advisories(diags));
114+
}
103115

104-
let unmaintained_diag = find_by_code(&diags, "RUSTSEC-2016-0004").unwrap();
105-
insta::assert_json_snapshot!(unmaintained_diag);
116+
{
117+
let cfg = tu::Config::new("unmaintained = 'workspace'");
118+
119+
let diags =
120+
tu::gather_diagnostics::<cfg::Config, _, _>(&krates, func_name!(), cfg, |ctx, tx| {
121+
advisories::check(
122+
ctx,
123+
&dbs,
124+
Option::<advisories::NoneReporter>::None,
125+
None,
126+
tx,
127+
);
128+
});
129+
130+
insta::assert_json_snapshot!(unmaintained_advisories(diags));
131+
}
132+
133+
{
134+
let cfg = tu::Config::new("unmaintained = 'transitive'");
135+
136+
let diags =
137+
tu::gather_diagnostics::<cfg::Config, _, _>(&krates, func_name!(), cfg, |ctx, tx| {
138+
advisories::check(
139+
ctx,
140+
&dbs,
141+
Option::<advisories::NoneReporter>::None,
142+
None,
143+
tx,
144+
);
145+
});
146+
147+
insta::assert_json_snapshot!(unmaintained_advisories(diags));
148+
}
149+
150+
{
151+
let cfg = tu::Config::new("unmaintained = 'none'");
152+
153+
let diags =
154+
tu::gather_diagnostics::<cfg::Config, _, _>(&krates, func_name!(), cfg, |ctx, tx| {
155+
advisories::check(
156+
ctx,
157+
&dbs,
158+
Option::<advisories::NoneReporter>::None,
159+
None,
160+
tx,
161+
);
162+
});
163+
164+
insta::assert_json_snapshot!(unmaintained_advisories(diags));
165+
}
106166
}
107167

108168
/// Validates we emit diagnostics when an unsound advisory is detected

tests/cfg/advisories.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ ignore = [
66
"crate@0.1",
77
{ crate = "yanked", reason = "a new version has not been released" },
88
]
9+
unmaintained = "workspace"

0 commit comments

Comments
 (0)