Repository: EmbarkStudios/cargo-deny (language: rust)

Issue:
Allow banning or requiring specific features.
**Is your feature request related to a problem? Please describe.**

In #225 I mentioned a case where a particular feature was banned. It would be good to be able to ban X crate ever from appearing while the feature is enabled. Some examples:

1. Certain parts of the `regex-syntax` unicode tables take up many megabytes for obscure regex features.

2. For a period of time in https://github.com/mozilla/application-services/ we had a feature where networking requests could be made through reqwest instead of via necko (gecko/Firefox's network engine).
    This was used for firefox-ios (which has no gecko), but shipping this to users on other platforms would have been considered a massive problem.

    We ultimately resolved it by switching from a feature-based solution to one where you pull in a reqwest backend and manually enable it. This feels more reliable given how features work.

3. In general features are well-known to be something that can easily be enabled at a distance by accident, and easily fail to be enabled by a misphrased cargo invocation (`cargo build -p --features` in workspaces does not respect the features you provide it -- although --all-features does). Some way of getting a handle here seems desirable.

**Describe the solution you'd like**

This is tricky a bit. Here's one way you could do it, but it's sort of bolting it on, and maybe extending the model somehow is better. Also, I'm not tied to names.

Solution high level.

- Add a field `deny-features` to `[ban.deny]` records, which only match if there's a feature in the final crate which is in the deny-features list
- Add a field `allow-features` to `[ban.deny]` records, which match if there's a feature in the final crate which isn't part of the allowlist
- Add a field `exact-features` to `[ban.deny]` records, which makes `allow-features` strict -- if this is true, then allow-features must be the exact featureset of the final crate.

Some examples:

```toml
# Example 1: forbid specific feature
[[bans.deny]]
name = "crate1"
# this `deny` block matches if any of these are present.
deny-features = ["badnews"]

# Example 2: Forbid all features other than the listed ones.
[[bans.deny]]
name = "crate2"
# this `deny` block matches if any features exist not present in the list
allow-features = ["fast-code", "faster-code"]

# Example 3: Require exact feature set
[[bans.deny]]
name = "crate3"
allow-features = ["good-vibes"]
exact-features = true
```

#### Algorithm

Note that by "the deny block matches" I mean: the crate is denied unless allowed by some other mechanism (skip, for example). Of course, if the deny block fails to match (e.g. the crate is accepted), the crate still may be denied if another deny block happens to match. Also, feature sets are, of course, not sensitive to order. (["A", "B", "C"] and ["C", "B", "A"] are the same).

- let `CF` be the final set of features for the indicated crate in the final crate graph.
- let `D` be a record in `bans.deny` which otherwise applies to the crate (not excluded by version, for example).
- let `DF` be `D.deny-features` if present, and the empty set otherwise.
- if `D.exact-features` is true:
    - let `AF` be `D.allow-features` if present, or the empty set if missing. 
    - if `AF` and `CF` are identical sets then `D` does not apply (e.g. crate accepted, but see above)
    - If `AF` and `CF` differ, then `D` does apply (e.g. crate denied, but see above).
- if `D.exact-features` is false or missing:
    - if `D.allowed-features` is missing, then all features are considered allowed.
    - otherwise, let `AF` be `D.allow-features`
        - If `CF` is a subset of `AF`, then `D` does not apply (accepted), otherwise D applies (denied)
        - Equivalently: if any item in CF exists not present in AF, the crate is denied

##### Edge cases
1. When loading a given `[[ban.deny]]` record, if any string is in both `deny-features` and `allow-features`, emit an error.

2. It is legal for the feature lists to refer to features which do not exist. They behave the same -- e.g. if a user requires a feature and the crate does not offer that feature, they have to figure it out.

3. For clarity:
    - if `allow-features` behaves similarly to `bans.allow` in that when not allowed it does not filter, and if it is provided it is very strict.
    - if `exact-features` is true and `allow-features` is missing, it's the same as `exact-features=true` and `allow-features=[]` -- all features are denied.
    - I suggested defaults for some of these, but it seems plausible that if you provided allow-features you should have to provide exact and vice versa.

---

**Describe alternatives you've considered**

See beginning. Scripts and avoiding the problem.

**Additional context**

N/A



Discussion / hints:
We've had similar problems as well, usually with regards to `default` features that creep in, so we are definitely motivated to add this kind of feature checking.
Another example usecase: using crates like `hyper` that expose traits from `tokio`, but with a different async runtime, you want to ensure the `tokio` runtime features are not being activated. That seems to fit into the proposed syntax

```toml
[[bans.deny]]
name = "tokio"
deny-features = ["rt-core", "io-driver", "time"]
```
I would start with this if it is not blocked by anything.
I don't believe it is blocked by anything, go for it! 

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 01f26d19..1eccc86c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -876,6 +876,7 @@ dependencies = [
  "console",
  "lazy_static",
  "linked-hash-map",
+ "serde",
  "similar",
  "yaml-rust",
 ]
@@ -916,7 +917,7 @@ dependencies = [
 [[package]]
 name = "krates"
 version = "0.11.0"
-source = "git+https://github.com/EmbarkStudios/krates?branch=add-features#ad91555dd9f5718212a0acedcaa2676cd48ab191"
+source = "git+https://github.com/EmbarkStudios/krates?branch=add-features#8c74a5809e782a130b261134480641a46c214638"
 dependencies = [
  "cargo_metadata",
  "cfg-expr",
diff --git a/Cargo.toml b/Cargo.toml
index 5974f470..67a4b702 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -99,7 +99,7 @@ url = "2.1"
 
 [dev-dependencies]
 # Snapshot testing
-insta = "1.21"
+insta = { version = "1.21", features = ["json"] }
 # We use this for creating fake crate directories for crawling license files on disk
 tempfile = "3.1.0"
 
diff --git a/docs/src/checks/bans/cfg.md b/docs/src/checks/bans/cfg.md
index 852f6773..47d231dc 100644
--- a/docs/src/checks/bans/cfg.md
+++ b/docs/src/checks/bans/cfg.md
@@ -36,7 +36,7 @@ When multiple versions of the same crate are encountered and `multiple-versions`
 
 ### Crate specifier
 
-The `allow`, `deny`, `skip`, and `skip-tree` fields all use a crate identifier to specify what crate(s) they want to match against.
+The `allow`, `deny`, `features`, `skip`, and `skip-tree` fields all use a crate identifier to specify what crate(s) they want to match against.
 
 ```ini
 { name = "some-crate-name-here", version = "<= 0.7.0" }
@@ -50,13 +50,50 @@ The name of the crate.
 
 An optional version constraint specifying the range of crate versions that will match. Defaults to any version.
 
+### The `deny` field (optional)
+
+```ini
+deny = [{ name = "crate-you-don't-want", version = "<= 0.7.0" }]
+```
+
+Determines specific crates that are denied.
+
 #### The `wrappers` field (optional)
 
-For `deny` entries, this field allows specific crates to have a direct dependency on the banned crate but denies all transitive dependencies on it.
+```ini
+deny = [{ name = "crate-you-don't-want", version = "<= 0.7.0", wrappers = ["this-can-use-it"] }]
+```
+
+This field allows specific crates to have a direct dependency on the banned crate but denies all transitive dependencies on it.
+
+### The `allow` field (optional)
+
+Determines specific crates that are allowed. If the `allow` list has one or more entries, then any crate not in that list will be denied, so use with care.
+
+### The `features` field (optional)
+
+```ini
+[[bans.features]]
+name = "featured-krate"
+version = "1.0"
+deny = ["bad-feature"]
+allow = ["good-feature"]
+exact = true
+```
+
+Allows specification of crate specific allow/deny lists of features.
+
+#### The `features.deny` field (optional)
+
+Denies specific features for the crate.
+
+#### The `features.allow` field (optional)
 
-### The `allow` and `deny` fields (optional)
+Allows specific features for the crate, enabled features not in this list are denied.
 
-Determines specific crates that are allowed or denied. If the `allow` list has one or more entries, then any crate not in that list will be denied, so use with care.
+#### The `features.exact` field (optional)
+
+If specified, requires that the features in `allow` exactly match the features enabled on the crate, and will fail if features are allowed that are not enabled.
 
 ### The `skip` field (optional)
 
@@ -77,21 +114,3 @@ Note that by default, the `depth` is infinite.
 ### The `allow-build-scripts` field (optional)
 
 Specifies all the crates that are allowed to have a build script. If this option is omitted, all crates are allowed to have a build script, and if this option is set to an empty list, no crate is allowed to have a build script.
-
-### The `deny-features` field (optional)
-
-If any of the denied features for a specific crate is used in the dependency graph, cargo-deny will deny it.
-
-**Note:** If this field is provided, cargo-deny will not ban the crate, unless it uses denied features.
-
-### The `allow-features` field (optional)
-
-A specific crate can only use the features provided in this config entry. If this is an empty set, it will have no effect.
-
-**Note:** If this field is provided, cargo-deny will not ban the crate, unless it uses non-allowed features.
-
-### The `exact-features` field (optional)
-
-Makes `allow-features` strict. If this is true, the feature set of the crate must be exactly the same as the `allow-features` set.
-
-**Note:** If this field is provided, cargo-deny will not ban the crate, unless the feature set doesn't match exactly.
diff --git a/src/advisories/cfg.rs b/src/advisories/cfg.rs
index 1a3b1552..ad999b30 100644
--- a/src/advisories/cfg.rs
+++ b/src/advisories/cfg.rs
@@ -171,7 +171,7 @@ mod test {
     use std::borrow::Cow;
 
     #[test]
-    fn works() {
+    fn deserializes_advisories_cfg() {
         #[derive(Deserialize)]
         #[serde(deny_unknown_fields)]
         struct Advisories {
diff --git a/src/bans.rs b/src/bans.rs
index f789ad5e..798a1e47 100644
--- a/src/bans.rs
+++ b/src/bans.rs
@@ -192,6 +192,9 @@ pub fn check(
         file_id,
         denied,
         allowed,
+        features,
+        workspace_default_features,
+        external_default_features,
         skipped,
         multiple_versions,
         highlight,
@@ -207,10 +210,11 @@ pub fn check(
         sink.push(build_diags);
     }
 
-    let (denied_ids, ban_wrappers): (Vec<_>, Vec<_>) = denied
-        .iter()
-        .map(|kb| (kb.id.clone(), kb.wrappers.clone()))
-        .unzip();
+    let (denied_ids, ban_wrappers): (Vec<_>, Vec<_>) =
+        denied.into_iter().map(|kb| (kb.id, kb.wrappers)).unzip();
+
+    let (feature_ids, features): (Vec<_>, Vec<_>) =
+        features.into_iter().map(|cf| (cf.id, cf.features)).unzip();
 
     // Keep track of all the crates we skip, and emit a warning if
     // we encounter a skip that didn't actually match any crate version
@@ -303,7 +307,7 @@ pub fn check(
             sink.push(pack);
         }
 
-        if let Some(ref og) = output_graph {
+        if let Some(og) = &output_graph {
             match graph::create_graph(
                 multi_detector.name,
                 highlight,
@@ -311,15 +315,15 @@ pub fn check(
                 &multi_detector.dupes,
             ) {
                 Ok(graph) => {
-                    if let Err(e) = og(DupGraph {
+                    if let Err(err) = og(DupGraph {
                         duplicate: multi_detector.name.to_owned(),
                         graph,
                     }) {
-                        log::error!("{}", e);
+                        log::error!("{err}");
                     }
                 }
-                Err(e) => {
-                    log::error!("unable to create graph for {}: {}", multi_detector.name, e);
+                Err(err) => {
+                    log::error!("unable to create graph for {}: {err}", multi_detector.name);
                 }
             };
         }
@@ -328,6 +332,7 @@ pub fn check(
     for (i, krate) in ctx.krates.krates().enumerate() {
         let mut pack = Pack::with_kid(Check::Bans, krate.id.clone());
 
+        // Check if the crate has been explicitly banned
         if let Some(matches) = matches(&denied_ids, krate) {
             for rm in matches {
                 let ban_cfg = CfgCoord {
@@ -337,74 +342,131 @@ pub fn check(
 
                 // The crate is banned, but it might have be allowed if it's wrapped
                 // by one or more particular crates
-                let wrappers = ban_wrappers.get(rm.index);
-
-                // We can also ban specific features only, so a wrapper crate would
-                // not be required
-                let feature_bans = &denied[rm.index].features;
-
-                let is_allowed_by_wrapper = match wrappers {
-                    Some(wrappers) if !wrappers.is_empty() => {
-                        let nid = ctx.krates.nid_for_kid(&krate.id).unwrap();
-                        let graph = ctx.krates.graph();
-
-                        // Ensure that every single crate that has a direct dependency
-                        // on the banned crate is an allowed wrapper
-                        graph
-                            .edges_directed(nid, Direction::Incoming)
-                            .filter_map(|edge| {
-                                if let krates::Edge::Dep { .. } = edge.weight() {
-                                    Some(edge.source())
-                                } else {
-                                    None
-                                }
-                            })
-                            .all(|nid| {
-                                let src = &ctx.krates[nid];
-
-                                let (diag, is_allowed): (Diag, _) =
-                                    match wrappers.iter().find(|aw| aw.value == src.name) {
-                                        Some(aw) => (
-                                            diags::BannedAllowedByWrapper {
-                                                ban_cfg: ban_cfg.clone(),
-                                                ban_exception_cfg: CfgCoord {
-                                                    file: file_id,
-                                                    span: aw.span.clone(),
-                                                },
-                                                banned_krate: krate,
-                                                wrapper_krate: src,
-                                            }
-                                            .into(),
-                                            true,
-                                        ),
-                                        None => (
-                                            diags::BannedUnmatchedWrapper {
-                                                ban_cfg: ban_cfg.clone(),
-                                                banned_krate: krate,
-                                                parent_krate: src,
-                                            }
-                                            .into(),
-                                            false,
-                                        ),
-                                    };
+                let is_allowed_by_wrapper = if let Some(wrappers) = ban_wrappers.get(rm.index) {
+                    let nid = ctx.krates.nid_for_kid(&krate.id).unwrap();
+                    let graph = ctx.krates.graph();
+
+                    // Ensure that every single crate that has a direct dependency
+                    // on the banned crate is an allowed wrapper
+                    graph
+                        .edges_directed(nid, Direction::Incoming)
+                        .filter_map(|edge| {
+                            if let krates::Edge::Dep { .. } = edge.weight() {
+                                Some(edge.source())
+                            } else {
+                                None
+                            }
+                        })
+                        .all(|nid| {
+                            let src = &ctx.krates[nid];
+
+                            let (diag, is_allowed): (Diag, _) =
+                                match wrappers.iter().find(|aw| aw.value == src.name) {
+                                    Some(aw) => (
+                                        diags::BannedAllowedByWrapper {
+                                            ban_cfg: ban_cfg.clone(),
+                                            ban_exception_cfg: CfgCoord {
+                                                file: file_id,
+                                                span: aw.span.clone(),
+                                            },
+                                            banned_krate: krate,
+                                            wrapper_krate: src,
+                                        }
+                                        .into(),
+                                        true,
+                                    ),
+                                    None => (
+                                        diags::BannedUnmatchedWrapper {
+                                            ban_cfg: ban_cfg.clone(),
+                                            banned_krate: krate,
+                                            parent_krate: src,
+                                        }
+                                        .into(),
+                                        false,
+                                    ),
+                                };
+
+                            pack.push(diag);
+                            is_allowed
+                        })
+                } else {
+                    false
+                };
 
-                                pack.push(diag);
-                                is_allowed
-                            })
+                if !is_allowed_by_wrapper {
+                    pack.push(diags::ExplicitlyBanned { krate, ban_cfg });
+                }
+            }
+        }
+
+        if !allowed.is_empty() {
+            // Since only allowing specific crates is pretty draconian,
+            // also emit which allow filters actually passed each crate
+            match matches(&allowed, krate) {
+                Some(matches) => {
+                    for rm in matches {
+                        pack.push(diags::ExplicitlyAllowed {
+                            krate,
+                            allow_cfg: CfgCoord {
+                                file: file_id,
+                                span: rm.id.span.clone(),
+                            },
+                        });
                     }
-                    _ => feature_bans.is_some(),
-                };
+                }
+                None => {
+                    pack.push(diags::ImplicitlyBanned { krate });
+                }
+            }
+        }
 
-                // Ensure that the feature set of this krate, wherever it's used
-                // as a dependency, matches the ban entry.
-                let feature_set_allowed = if let Some(feature_bans) = feature_bans {
-                    let enabled_features = ctx.krates.get_enabled_features(&krate.id).unwrap();
+        let enabled_features = ctx.krates.get_enabled_features(&krate.id).unwrap();
 
+        let default_lint_level = if enabled_features.contains("default") {
+            if ctx.krates.workspace_members().any(|n| {
+                if let krates::Node::Krate { id, .. } = n {
+                    id == &krate.id
+                } else {
+                    false
+                }
+            }) {
+                workspace_default_features.as_ref()
+            } else {
+                external_default_features.as_ref()
+            }
+        } else {
+            None
+        };
+
+        if let Some(ll) = default_lint_level {
+            if ll.value == LintLevel::Warn {
+                pack.push(diags::DefaultFeatureEnabled {
+                    krate,
+                    level: ll,
+                    file_id,
+                });
+            }
+        }
+
+        // Check if the crate has had features denied/allowed or are required to be exact
+        if let Some(matches) = matches(&feature_ids, krate) {
+            for rm in matches {
+                let feature_bans = &features[rm.index];
+
+                let feature_set_allowed = {
                     // Gather features that were present, but not explicitly allowed
                     let not_explicitly_allowed: Vec<_> = enabled_features
                         .iter()
                         .filter_map(|ef| {
                             if !feature_bans.allow.value.iter().any(|af| &af.value == ef) {
+                                if ef == "default" {
+                                    if let Some(ll) = default_lint_level {
+                                        if ll.value != LintLevel::Deny {
+                                            return None;
+                                        }
+                                    }
+                                }
+
                                 Some(ef.as_str())
                             } else {
                                 None
@@ -449,16 +511,71 @@ pub fn check(
                         // the check has failed
                         let diag_count = pack.len();
 
-                        // Add diagnostics if features were explicitly allowed, but weren't present
+                        // Add diagnostics if features were explicitly allowed,
+                        // but didn't contain 1 or more features that were enabled
                         if !feature_bans.allow.value.is_empty() {
                             for feature in &not_explicitly_allowed {
-                                pack.push(diags::FeatureNotExplicitlyAllowed {
+                                // Since the user has not specified `exact` we
+                                // can also look at the full tree of features to
+                                // determine if the feature is covered by an allowed
+                                // parent feature
+                                fn has_feature(
+                                    map: &std::collections::HashMap<String, Vec<String>>,
+                                    parent: &str,
+                                    feature: &str,
+                                ) -> bool {
+                                    if let Some(parent) = map.get(parent) {
+                                        parent.iter().any(|f| {
+                                            let pf = krates::ParsedFeature::from(f.as_str());
+
+                                            if let krates::Feature::Simple(feat) = pf.feat() {
+                                                if feat == feature {
+                                                    true
+                                                } else {
+                                                    has_feature(map, feat, feature)
+                                                }
+                                            } else {
+                                                false
+                                            }
+                                        })
+                                    } else {
+                                        false
+                                    }
+                                }
+
+                                if !feature_bans.allow.value.iter().any(|allowed| {
+                                    has_feature(&krate.features, allowed.value.as_str(), feature)
+                                }) {
+                                    pack.push(diags::FeatureNotExplicitlyAllowed {
+                                        krate,
+                                        feature,
+                                        allowed: CfgCoord {
+                                            file: file_id,
+                                            span: feature_bans.allow.span.clone(),
+                                        },
+                                    });
+                                }
+                            }
+                        }
+
+                        // If the default feature has been denied at a global
+                        // level but not at the crate level, emit an error with
+                        // the global span, otherwise the crate level setting,
+                        // if the default feature was banned explicitly, takes
+                        // precedence
+                        if let Some(ll) = default_lint_level {
+                            if ll.value == LintLevel::Deny
+                                && !feature_bans
+                                    .allow
+                                    .value
+                                    .iter()
+                                    .any(|d| d.value == "default")
+                                && !feature_bans.deny.iter().any(|d| d.value == "default")
+                            {
+                                pack.push(diags::DefaultFeatureEnabled {
                                     krate,
-                                    feature,
-                                    allowed: CfgCoord {
-                                        file: file_id,
-                                        span: feature_bans.allow.span.clone(),
-                                    },
+                                    level: ll,
+                                    file_id,
                                 });
                             }
                         }
@@ -477,19 +594,15 @@ pub fn check(
 
                         diag_count <= pack.len()
                     }
-                } else {
-                    true
                 };
 
-                if !is_allowed_by_wrapper || !feature_set_allowed {
-                    pack.push(diags::ExplicitlyBanned { krate, ban_cfg });
-                } else if let Some(feature_bans) = &denied[rm.index].features {
-                    // If the crate isn't actually banned, but does reference
-                    // features that don't exist, emit warnings about them so
-                    // the user can cleanup their config. We _could_ emit these
-                    // warnings if the crate is banned, but feature graphs in
-                    // particular can be massive and adding warnings into the mix
-                    // will just make parsing the error graphs harder
+                // If the crate isn't actually banned, but does reference
+                // features that don't exist, emit warnings about them so
+                // the user can cleanup their config. We _could_ emit these
+                // warnings if the crate is banned, but feature graphs in
+                // particular can be massive and adding warnings into the mix
+                // will just make parsing the error graphs harder
+                if feature_set_allowed {
                     for feature in feature_bans
                         .allow
                         .value
@@ -506,26 +619,13 @@ pub fn check(
                     }
                 }
             }
-        }
-
-        if !allowed.is_empty() {
-            // Since only allowing specific crates is pretty draconian,
-            // also emit which allow filters actually passed each crate
-            match matches(&allowed, krate) {
-                Some(matches) => {
-                    for rm in matches {
-                        pack.push(diags::ExplicitlyAllowed {
-                            krate,
-                            allow_cfg: CfgCoord {
-                                file: file_id,
-                                span: rm.id.span.clone(),
-                            },
-                        });
-                    }
-                }
-                None => {
-                    pack.push(diags::ImplicitlyBanned { krate });
-                }
+        } else if let Some(ll) = default_lint_level {
+            if ll.value == LintLevel::Deny {
+                pack.push(diags::DefaultFeatureEnabled {
+                    krate,
+                    level: ll,
+                    file_id,
+                });
             }
         }
 
@@ -539,9 +639,9 @@ pub fn check(
                     },
                 });
 
-                // Keep a count of the number of times each skip filter is hit
-                // so that we can report unused filters to the user so that they
-                // can cleanup their configs as their dependency graph changes over time
+                // Mark each skip filter that is hit so that we can report unused
+                // filters to the user so that they can cleanup their configs as
+                // their dependency graph changes over time
                 skip_hit.as_mut_bitslice().set(rm.index, true);
             }
         } else if !tree_skipper.matches(krate, &mut pack) {
diff --git a/src/bans/cfg.rs b/src/bans/cfg.rs
index 5d9b83c5..3a2bd1b6 100644
--- a/src/bans/cfg.rs
+++ b/src/bans/cfg.rs
@@ -16,10 +16,24 @@ pub struct CrateId {
     pub version: Option<VersionReq>,
 }
 
+#[derive(Deserialize, Clone)]
+#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct CrateBan {
+    pub name: Spanned<String>,
+    pub version: Option<VersionReq>,
+    /// One or more crates that will allow this crate to be used if it is a
+    /// direct dependency
+    #[serde(default)]
+    pub wrappers: Vec<Spanned<String>>,
+}
+
 #[derive(Deserialize, Clone)]
 #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
 #[serde(deny_unknown_fields)]
-pub struct FeatureBans {
+pub struct CrateFeatures {
+    pub name: Spanned<String>,
+    pub version: Option<VersionReq>,
     /// All features that are allowed to be used.
     #[serde(default)]
     pub allow: Spanned<Vec<Spanned<String>>>,
@@ -31,20 +45,6 @@ pub struct FeatureBans {
     pub exact: Spanned<bool>,
 }
 
-#[derive(Deserialize, Clone)]
-#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
-#[serde(rename_all = "kebab-case", deny_unknown_fields)]
-pub struct CrateBan {
-    pub name: Spanned<String>,
-    pub version: Option<VersionReq>,
-    /// One or more crates that will allow this crate to be used if it is a
-    /// direct dependency
-    #[serde(default)]
-    pub wrappers: Vec<Spanned<String>>,
-    #[serde(default)]
-    pub features: Option<FeatureBans>,
-}
-
 #[derive(Deserialize, Clone)]
 #[cfg_attr(test, derive(Debug, PartialEq, Eq))]
 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
@@ -98,6 +98,17 @@ pub struct Config {
     /// If specified, means only the listed crates are allowed
     #[serde(default)]
     pub allow: Vec<Spanned<CrateId>>,
+    /// Allows specifying features that are or are not allowed on crates
+    #[serde(default)]
+    pub features: Vec<CrateFeatures>,
+    /// The default lint level for default features for external, non-workspace
+    /// crates, can be overriden in `features` on a crate by crate basis
+    #[serde(default)]
+    pub external_default_features: Option<Spanned<LintLevel>>,
+    /// The default lint level for default features for workspace crates, can be
+    /// overriden in `features` on a crate by crate basis
+    #[serde(default)]
+    pub workspace_default_features: Option<Spanned<LintLevel>>,
     /// If specified, disregards the crate completely
     #[serde(default)]
     pub skip: Vec<Spanned<CrateId>>,
@@ -119,6 +130,9 @@ impl Default for Config {
             highlight: GraphHighlight::All,
             deny: Vec::new(),
             allow: Vec::new(),
+            features: Vec::new(),
+            external_default_features: None,
+            workspace_default_features: None,
             skip: Vec::new(),
             skip_tree: Vec::new(),
             wildcards: LintLevel::Allow,
@@ -153,7 +167,6 @@ impl crate::cfg::UnvalidatedConfig for Config {
                     cb.name.span,
                 ),
                 wrappers: cb.wrappers,
-                features: cb.features,
             })
             .collect();
 
@@ -196,15 +209,6 @@ impl crate::cfg::UnvalidatedConfig for Config {
             if let Some(dupe) = exact_match(&skipped, &d.id.value) {
                 diags.push(dupe_crate_diag((&d.id, "deny"), (dupe, "skip")));
             }
-
-            // Ensure that a feature isn't both allowed and denied
-            if let Some(fb) = &d.features {
-                for allowed in &fb.allow.value {
-                    if let Some(denied) = fb.deny.iter().find(|df| df.value == allowed.value) {
-                        diags.push(dupe_feature_diag(&d.id, allowed, denied));
-                    }
-                }
-            }
         }
 
         for all in &allowed {
@@ -213,12 +217,45 @@ impl crate::cfg::UnvalidatedConfig for Config {
             }
         }
 
+        // Ensure that a feature isn't both allowed and denied
+        let features = self
+            .features
+            .into_iter()
+            .map(|cf| {
+                let id = Skrate::new(
+                    KrateId {
+                        name: cf.name.value,
+                        version: cf.version,
+                    },
+                    cf.name.span,
+                );
+
+                for allowed in &cf.allow.value {
+                    if let Some(denied) = cf.deny.iter().find(|df| df.value == allowed.value) {
+                        diags.push(dupe_feature_diag(&id, allowed, denied));
+                    }
+                }
+
+                KrateFeatures {
+                    id,
+                    features: Features {
+                        allow: cf.allow,
+                        deny: cf.deny,
+                        exact: cf.exact,
+                    },
+                }
+            })
+            .collect();
+
         ValidConfig {
             file_id: cfg_file,
             multiple_versions: self.multiple_versions,
             highlight: self.highlight,
             denied,
             allowed,
+            features,
+            external_default_features: self.external_default_features,
+            workspace_default_features: self.workspace_default_features,
             skipped,
             wildcards: self.wildcards,
             tree_skipped: self
@@ -253,7 +290,19 @@ pub(crate) type Skrate = Spanned<KrateId>;
 pub(crate) struct KrateBan {
     pub id: Skrate,
     pub wrappers: Vec<Spanned<String>>,
-    pub features: Option<FeatureBans>,
+}
+
+#[cfg_attr(test, derive(Debug))]
+pub struct Features {
+    pub allow: Spanned<Vec<Spanned<String>>>,
+    pub deny: Vec<Spanned<String>>,
+    pub exact: Spanned<bool>,
+}
+
+#[cfg_attr(test, derive(Debug))]
+pub(crate) struct KrateFeatures {
+    pub id: Skrate,
+    pub features: Features,
 }
 
 pub struct ValidConfig {
@@ -262,6 +311,9 @@ pub struct ValidConfig {
     pub highlight: GraphHighlight,
     pub(crate) denied: Vec<KrateBan>,
     pub(crate) allowed: Vec<Skrate>,
+    pub(crate) features: Vec<KrateFeatures>,
+    pub external_default_features: Option<Spanned<LintLevel>>,
+    pub workspace_default_features: Option<Spanned<LintLevel>>,
     pub(crate) skipped: Vec<Skrate>,
     pub(crate) tree_skipped: Vec<Spanned<TreeSkip>>,
     pub wildcards: LintLevel,
@@ -296,7 +348,7 @@ mod test {
     }
 
     #[test]
-    fn works() {
+    fn deserializes_ban_cfg() {
         #[derive(Deserialize)]
         #[serde(deny_unknown_fields)]
         struct Bans {
@@ -312,6 +364,14 @@ mod test {
         assert_eq!(validated.file_id, cd.id);
         assert_eq!(validated.multiple_versions, LintLevel::Deny);
         assert_eq!(validated.highlight, GraphHighlight::SimplestPath);
+        assert_eq!(
+            validated.external_default_features.unwrap().value,
+            LintLevel::Deny
+        );
+        assert_eq!(
+            validated.workspace_default_features.unwrap().value,
+            LintLevel::Warn
+        );
 
         assert_eq!(
             validated.allowed,
@@ -335,5 +395,12 @@ mod test {
                 depth: Some(20),
             }]
         );
+
+        let kf = &validated.features[0];
+
+        assert_eq!(kf.id, kid!("featured-krate", "1.0"));
+        assert_eq!(kf.features.deny[0].value, "bad-feature");
+        assert_eq!(kf.features.allow.value[0].value, "good-feature");
+        assert!(kf.features.exact.value);
     }
 }
diff --git a/src/bans/diags.rs b/src/bans/diags.rs
index 63a12343..a418bf46 100644
--- a/src/bans/diags.rs
+++ b/src/bans/diags.rs
@@ -336,7 +336,7 @@ impl From<FeatureNotExplicitlyAllowed<'_>> for Diag {
             diag,
             graph_nodes: std::iter::once(GraphNode {
                 kid: fna.krate.id.clone(),
-                feature: Some(fna.feature.to_owned()),
+                feature: None,
             })
             .collect(),
             extra: None,
@@ -405,3 +405,34 @@ impl From<UnknownFeature<'_>> for Diag {
         }
     }
 }
+
+pub(crate) struct DefaultFeatureEnabled<'a> {
+    pub(crate) krate: &'a Krate,
+    pub(crate) level: &'a Spanned<crate::LintLevel>,
+    pub(crate) file_id: FileId,
+}
+
+impl From<DefaultFeatureEnabled<'_>> for Diag {
+    fn from(dfe: DefaultFeatureEnabled<'_>) -> Diag {
+        let diag = Diagnostic::new(dfe.level.value.into())
+            .with_message(format!(
+                "'default' feature enabled for crate '{}'",
+                dfe.krate,
+            ))
+            .with_code("B017")
+            .with_labels(vec![
+                Label::primary(dfe.file_id, dfe.level.span.clone()).with_message("lint level")
+            ]);
+
+        Diag {
+            diag,
+            graph_nodes: std::iter::once(GraphNode {
+                kid: dfe.krate.id.clone(),
+                feature: Some("default".to_owned()),
+            })
+            .collect(),
+            extra: None,
+            with_features: true,
+        }
+    }
+}
diff --git a/src/cfg.rs b/src/cfg.rs
index aef9eae1..1c903471 100644
--- a/src/cfg.rs
+++ b/src/cfg.rs
@@ -11,11 +11,6 @@ pub trait UnvalidatedConfig {
     ) -> Self::ValidCfg;
 }
 
-pub(crate) const NAME: &str = "$__toml_private_Spanned";
-pub(crate) const START: &str = "$__toml_private_start";
-pub(crate) const END: &str = "$__toml_private_end";
-pub(crate) const VALUE: &str = "$__toml_private_value";
-
 #[derive(Default)]
 pub struct Spanned<T> {
     pub(crate) value: T,
@@ -131,6 +126,11 @@ where
     where
         D: de::Deserializer<'de>,
     {
+        pub(crate) const NAME: &str = "$__toml_private_Spanned";
+        pub(crate) const START: &str = "$__toml_private_start";
+        pub(crate) const END: &str = "$__toml_private_end";
+        pub(crate) const VALUE: &str = "$__toml_private_value";
+
         struct SpannedVisitor<T>(::std::marker::PhantomData<T>);
 
         impl<'de, T> de::Visitor<'de> for SpannedVisitor<T>
diff --git a/src/licenses/cfg.rs b/src/licenses/cfg.rs
index 274f377f..c1df0384 100644
--- a/src/licenses/cfg.rs
+++ b/src/licenses/cfg.rs
@@ -376,7 +376,7 @@ mod test {
     use crate::cfg::{test::*, *};
 
     #[test]
-    fn works() {
+    fn deserializes_licenses_cfg() {
         #[derive(Deserialize)]
         #[serde(deny_unknown_fields)]
         struct Licenses {
diff --git a/src/sources/cfg.rs b/src/sources/cfg.rs
index ad8fae35..d02b6169 100644
--- a/src/sources/cfg.rs
+++ b/src/sources/cfg.rs
@@ -202,7 +202,7 @@ mod test {
     use crate::cfg::{test::*, *};
 
     #[test]
-    fn works() {
+    fn deserializes_sources_cfg() {
         #[derive(Deserialize)]
         #[serde(deny_unknown_fields)]
         struct Sources {


Test patch:
diff --git a/src/test_utils.rs b/src/test_utils.rs
index 44394535..eb9a1582 100644
--- a/src/test_utils.rs
+++ b/src/test_utils.rs
@@ -4,7 +4,7 @@ use crate::{
 };
 use anyhow::Context as _;
 
-#[derive(Default)]
+#[derive(Default, Clone)]
 pub struct KrateGather<'k> {
     pub name: &'k str,
     pub features: &'k [&'k str],
diff --git a/tests/cfg/bans.toml b/tests/cfg/bans.toml
index 22ccde49..0da26771 100644
--- a/tests/cfg/bans.toml
+++ b/tests/cfg/bans.toml
@@ -2,13 +2,13 @@
 multiple-versions = "deny"
 wildcards = "deny"
 highlight = "simplest-path"
+workspace-default-features = "warn"
+external-default-features = "deny"
 allow = [
     { name = "all-versionsa" },
     { name = "specific-versiona", version = "<0.1.1" },
 ]
-skip-tree = [
-    { name = "blah", depth = 20 },
-]
+skip-tree = [{ name = "blah", depth = 20 }]
 
 [[bans.deny]]
 name = "all-versionsd"
@@ -21,3 +21,10 @@ version = "=0.1.9"
 [[bans.skip]]
 name = "rand"
 version = "=0.6.5"
+
+[[bans.features]]
+name = "featured-krate"
+version = "1.0"
+deny = ["bad-feature"]
+allow = ["good-feature"]
+exact = true
diff --git a/tests/feature_bans.rs b/tests/feature_bans.rs
index c7811d36..fb74fea5 100644
--- a/tests/feature_bans.rs
+++ b/tests/feature_bans.rs
@@ -10,7 +10,7 @@ fn bans_workspace_features() {
     let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
         KrateGather::new("features-galore"),
         "bans_workspace_features",
-        Some("deny = [{ name = 'features-galore', features.deny = ['simple'] }]"),
+        Some("features = [{ name = 'features-galore', deny = ['simple'] }]"),
         Some(&["x86_64-unknown-linux-gnu"]),
         |ctx, cs, tx| {
             bans::check(ctx, None, cs, tx);
@@ -18,7 +18,7 @@ fn bans_workspace_features() {
     )
     .unwrap();
 
-    insta::assert_snapshot!(tu::to_snapshot(diags));
+    insta::assert_json_snapshot!(diags);
 }
 
 /// Ensures non-workspace features are banned
@@ -32,7 +32,7 @@ fn bans_external_features() {
             ..Default::default()
         },
         "bans_external_features",
-        Some("deny = [{ name = 'libssh2-sys', features.deny = ['zlib-ng-compat'] }]"),
+        Some("features = [{ name = 'libssh2-sys', deny = ['zlib-ng-compat'] }]"),
         Some(&["x86_64-unknown-linux-gnu"]),
         |ctx, cs, tx| {
             bans::check(ctx, None, cs, tx);
@@ -40,7 +40,51 @@ fn bans_external_features() {
     )
     .unwrap();
 
-    insta::assert_snapshot!(tu::to_snapshot(diags));
+    insta::assert_json_snapshot!(diags);
+}
+
+/// Ensures non-workspace features can be allowed
+#[test]
+fn allows_external_features() {
+    let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        KrateGather {
+            name: "features-galore",
+            features: &["zlib", "ssh"],
+            no_default_features: true,
+            ..Default::default()
+        },
+        "allows_external_features",
+        Some("features = [{ name = 'libssh2-sys', allow = ['zlib-ng-compat'] }]"),
+        Some(&["x86_64-unknown-linux-gnu"]),
+        |ctx, cs, tx| {
+            bans::check(ctx, None, cs, tx);
+        },
+    )
+    .unwrap();
+
+    insta::assert_json_snapshot!(diags);
+}
+
+/// Ensures workspace features fail if not all are allowed
+#[test]
+fn fails_if_not_all_features_allowed() {
+    let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        KrateGather {
+            name: "features-galore",
+            features: &["zlib", "ssh"],
+            no_default_features: true,
+            ..Default::default()
+        },
+        "bans_external_features",
+        Some("features = [{ name = 'features-galore', allow = ['ssh'] }]"),
+        Some(&["x86_64-unknown-linux-gnu"]),
+        |ctx, cs, tx| {
+            bans::check(ctx, None, cs, tx);
+        },
+    )
+    .unwrap();
+
+    insta::assert_json_snapshot!(diags);
 }
 
 /// Ensures features banned in a crate with multiple versions are all found
@@ -49,7 +93,7 @@ fn bans_features_from_multiple_versions() {
     let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
         KrateGather::new("features-galore"),
         "bans_features_from_multiple_versions",
-        Some("multiple-versions = 'allow'\ndeny = [{ name = 'windows-sys', features.deny = ['Win32_System_LibraryLoader'] }]"),
+        Some("multiple-versions = 'allow'\nfeatures = [{ name = 'windows-sys', deny = ['Win32_System_LibraryLoader'] }]"),
         Some(&["x86_64-pc-windows-msvc"]),
         |ctx, cs, tx| {
             bans::check(ctx, None, cs, tx);
@@ -57,7 +101,24 @@ fn bans_features_from_multiple_versions() {
     )
     .unwrap();
 
-    insta::assert_snapshot!(tu::to_snapshot(diags));
+    insta::assert_json_snapshot!(diags);
+}
+
+/// Ensures exact works
+#[test]
+fn exact_features() {
+    let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        KrateGather::new("features-galore"),
+        "bans_features_from_multiple_versions",
+        Some("multiple-versions = 'allow'\nfeatures = [{ name = 'windows-sys', exact = true, allow = ['Win32_System_LibraryLoader'] }]"),
+        Some(&["x86_64-pc-windows-msvc"]),
+        |ctx, cs, tx| {
+            bans::check(ctx, None, cs, tx);
+        },
+    )
+    .unwrap();
+
+    insta::assert_json_snapshot!(diags);
 }
 
 /// Ensures weak dependencies are properly pruned from the graph
@@ -82,3 +143,113 @@ fn weak_dependencies_pruned() {
 
     assert!(diags.is_empty());
 }
+
+/// Ensures the `workspace_default_features` cfg works as expected
+#[test]
+fn workspace_default_features() {
+    let kg = KrateGather {
+        name: "features-galore",
+        no_default_features: false,
+        ..Default::default()
+    };
+
+    {
+        let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        kg.clone(),
+            "workspace_default_features_gets_overriden",
+            Some("workspace-default-features = 'warn'\nfeatures = [{ name = 'features-galore', deny = ['default'] }]"),
+            Some(&["x86_64-unknown-linux-gnu"]),
+            |ctx, cs, tx| {
+                bans::check(ctx, None, cs, tx);
+            },
+        )
+        .unwrap();
+
+        insta::assert_json_snapshot!(diags);
+    }
+
+    {
+        let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+            kg.clone(),
+            "workspace_default_features_denies",
+            Some("workspace-default-features = 'deny'\n"),
+            Some(&["x86_64-unknown-linux-gnu"]),
+            |ctx, cs, tx| {
+                bans::check(ctx, None, cs, tx);
+            },
+        )
+        .unwrap();
+
+        insta::assert_json_snapshot!(diags);
+    }
+
+    {
+        let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        kg,
+            "workspace_default_features_deny_default",
+            Some("workspace-default-features = 'deny'\nfeatures = [{ name = 'features-galore', allow = ['default'] }]"),
+            Some(&["x86_64-unknown-linux-gnu"]),
+            |ctx, cs, tx| {
+                bans::check(ctx, None, cs, tx);
+            },
+        )
+        .unwrap();
+
+        insta::assert_json_snapshot!(diags);
+    }
+}
+
+/// Ensures the `external_default_features` cfg works as expected
+#[test]
+fn external_default_features() {
+    let kg = KrateGather {
+        name: "features-galore",
+        no_default_features: true,
+        ..Default::default()
+    };
+
+    {
+        let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        kg.clone(),
+            "external_default_features_gets_overriden",
+            Some("external-default-features = 'allow'\nfeatures = [{ name = 'bitflags', deny = ['default'] }]"),
+            Some(&["x86_64-unknown-linux-gnu"]),
+            |ctx, cs, tx| {
+                bans::check(ctx, None, cs, tx);
+            },
+        )
+        .unwrap();
+
+        insta::assert_json_snapshot!(diags);
+    }
+
+    {
+        let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+            kg.clone(),
+            "external_default_features_denies",
+            Some("external-default-features = 'deny'\n"),
+            Some(&["x86_64-unknown-linux-gnu"]),
+            |ctx, cs, tx| {
+                bans::check(ctx, None, cs, tx);
+            },
+        )
+        .unwrap();
+
+        insta::assert_json_snapshot!(diags);
+    }
+
+    {
+        let diags = tu::gather_diagnostics::<cfg::Config, _, _, _>(
+        kg,
+            "external_default_features_deny_default",
+            Some("external-default-features = 'deny'\nfeatures = [{ name = 'bitflags', allow = ['default'] }]"),
+            Some(&["x86_64-unknown-linux-gnu"]),
+            |ctx, cs, tx| {
+                bans::check(ctx, None, cs, tx);
+            },
+        )
+        .unwrap();
+
+        insta::assert_json_snapshot!(diags);
+    }
+}
diff --git a/tests/snapshots/feature_bans__allows_external_features.snap b/tests/snapshots/feature_bans__allows_external_features.snap
new file mode 100644
index 00000000..e96edf3f
--- /dev/null
+++ b/tests/snapshots/feature_bans__allows_external_features.snap
@@ -0,0 +1,5 @@
+---
+source: tests/feature_bans.rs
+expression: "tu::to_snapshot(diags)"
+---
+[]
diff --git a/tests/snapshots/feature_bans__bans_external_features.snap b/tests/snapshots/feature_bans__bans_external_features.snap
index 6bba1748..46a5512a 100644
--- a/tests/snapshots/feature_bans__bans_external_features.snap
+++ b/tests/snapshots/feature_bans__bans_external_features.snap
@@ -50,6 +50,72 @@ expression: "tu::to_snapshot(diags)"
                               }
                             }
                           ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "git2",
+                            "name": "ssh"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "features-galore",
+                                "version": "0.1.0"
+                              },
+                              "repeat": true
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "ssh"
+                              },
+                              "repeat": true
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "git2",
+                            "name": "ssh_key_from_memory"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "features-galore",
+                                "version": "0.1.0"
+                              },
+                              "repeat": true
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "ssh"
+                              },
+                              "repeat": true
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "git2",
+                            "name": "zlib-ng-compat"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "features-galore",
+                                "version": "0.1.0"
+                              },
+                              "repeat": true
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "zlib"
+                              },
+                              "repeat": true
+                            }
+                          ]
                         }
                       ]
                     },
@@ -71,6 +137,13 @@ expression: "tu::to_snapshot(diags)"
                                 "version": "0.14.4"
                               },
                               "repeat": true
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "git2",
+                                "name": "ssh"
+                              },
+                              "repeat": true
                             }
                           ]
                         }
@@ -95,6 +168,13 @@ expression: "tu::to_snapshot(diags)"
                             "version": "0.14.4"
                           },
                           "repeat": true
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "git2",
+                            "name": "ssh_key_from_memory"
+                          },
+                          "repeat": true
                         }
                       ]
                     },
@@ -110,6 +190,13 @@ expression: "tu::to_snapshot(diags)"
                             "version": "0.14.4"
                           },
                           "repeat": true
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "git2",
+                            "name": "zlib-ng-compat"
+                          },
+                          "repeat": true
                         }
                       ]
                     }
@@ -129,7 +216,7 @@ expression: "tu::to_snapshot(diags)"
       ],
       "labels": [
         {
-          "column": 50,
+          "column": 45,
           "line": 1,
           "message": "feature denied here",
           "span": "'zlib-ng-compat'"
diff --git a/tests/snapshots/feature_bans__bans_features_from_multiple_versions.snap b/tests/snapshots/feature_bans__bans_features_from_multiple_versions.snap
index 9619528d..29d7d823 100644
--- a/tests/snapshots/feature_bans__bans_features_from_multiple_versions.snap
+++ b/tests/snapshots/feature_bans__bans_features_from_multiple_versions.snap
@@ -78,7 +78,7 @@ expression: "tu::to_snapshot(diags)"
       ],
       "labels": [
         {
-          "column": 50,
+          "column": 45,
           "line": 2,
           "message": "feature denied here",
           "span": "'Win32_System_LibraryLoader'"
@@ -156,7 +156,7 @@ expression: "tu::to_snapshot(diags)"
       ],
       "labels": [
         {
-          "column": 50,
+          "column": 45,
           "line": 2,
           "message": "feature denied here",
           "span": "'Win32_System_LibraryLoader'"
diff --git a/tests/snapshots/feature_bans__bans_workspace_features.snap b/tests/snapshots/feature_bans__bans_workspace_features.snap
index aab33da0..f96244d7 100644
--- a/tests/snapshots/feature_bans__bans_workspace_features.snap
+++ b/tests/snapshots/feature_bans__bans_workspace_features.snap
@@ -32,7 +32,7 @@ expression: "tu::to_snapshot(diags)"
       ],
       "labels": [
         {
-          "column": 54,
+          "column": 49,
           "line": 1,
           "message": "feature denied here",
           "span": "'simple'"
diff --git a/tests/snapshots/feature_bans__exact_features.snap b/tests/snapshots/feature_bans__exact_features.snap
new file mode 100644
index 00000000..33215271
--- /dev/null
+++ b/tests/snapshots/feature_bans__exact_features.snap
@@ -0,0 +1,766 @@
+---
+source: tests/feature_bans.rs
+expression: "tu::to_snapshot(diags)"
+---
+[
+  {
+    "fields": {
+      "code": "B013",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.36.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32"
+              },
+              "parents": [
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_Foundation"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "parking_lot_core",
+                        "version": "0.9.3"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "features-galore",
+                            "version": "0.1.0"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "default"
+                              }
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "json"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "simple"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Feature": {
+                                        "crate_name": "features-galore",
+                                        "name": "default"
+                                      },
+                                      "repeat": true
+                                    }
+                                  ]
+                                }
+                              ]
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "repeat": true
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_System"
+                  },
+                  "parents": [
+                    {
+                      "Feature": {
+                        "crate_name": "windows-sys",
+                        "name": "Win32_System_LibraryLoader"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "parking_lot_core",
+                            "version": "0.9.3"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "windows-sys",
+                        "name": "Win32_System_SystemServices"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "parking_lot_core",
+                            "version": "0.9.3"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "windows-sys",
+                        "name": "Win32_System_WindowsProgramming"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "parking_lot_core",
+                            "version": "0.9.3"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.36.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32_Foundation"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "parking_lot_core",
+                    "version": "0.9.3"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "default"
+                          }
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "json"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "default"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "simple"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.36.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32_System"
+              },
+              "parents": [
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_System_LibraryLoader"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "parking_lot_core",
+                        "version": "0.9.3"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "features-galore",
+                            "version": "0.1.0"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "default"
+                              }
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "json"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "simple"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Feature": {
+                                        "crate_name": "features-galore",
+                                        "name": "default"
+                                      },
+                                      "repeat": true
+                                    }
+                                  ]
+                                }
+                              ]
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "repeat": true
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_System_SystemServices"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "parking_lot_core",
+                        "version": "0.9.3"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                },
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_System_WindowsProgramming"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "parking_lot_core",
+                        "version": "0.9.3"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.36.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32_System_SystemServices"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "parking_lot_core",
+                    "version": "0.9.3"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "default"
+                          }
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "json"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "default"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "simple"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.36.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32_System_WindowsProgramming"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "parking_lot_core",
+                    "version": "0.9.3"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "default"
+                          }
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "json"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "default"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "simple"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.36.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "parking_lot_core",
+                    "version": "0.9.3"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "default"
+                          }
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "json"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "default"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "simple"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 45,
+          "line": 2,
+          "message": "exact enabled here",
+          "span": "true"
+        }
+      ],
+      "message": "feature set for crate 'windows-sys = 0.36.1' did not match exactly",
+      "notes": [
+        "'Win32' feature was enabled but not explicitly allowed",
+        "'Win32_Foundation' feature was enabled but not explicitly allowed",
+        "'Win32_System' feature was enabled but not explicitly allowed",
+        "'Win32_System_SystemServices' feature was enabled but not explicitly allowed",
+        "'Win32_System_WindowsProgramming' feature was enabled but not explicitly allowed",
+        "'default' feature was enabled but not explicitly allowed"
+      ],
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B013",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.42.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32"
+              },
+              "parents": [
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_System"
+                  },
+                  "parents": [
+                    {
+                      "Feature": {
+                        "crate_name": "windows-sys",
+                        "name": "Win32_System_LibraryLoader"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "features-galore",
+                            "version": "0.1.0"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "default"
+                              }
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "json"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "simple"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Feature": {
+                                        "crate_name": "features-galore",
+                                        "name": "default"
+                                      },
+                                      "repeat": true
+                                    }
+                                  ]
+                                }
+                              ]
+                            },
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "repeat": true
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.42.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "Win32_System"
+              },
+              "parents": [
+                {
+                  "Feature": {
+                    "crate_name": "windows-sys",
+                    "name": "Win32_System_LibraryLoader"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "default"
+                          }
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "json"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "simple"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "features-galore",
+                                    "name": "default"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "simple"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "Krate": {
+            "name": "windows-sys",
+            "version": "0.42.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "windows-sys",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "features-galore",
+                    "version": "0.1.0"
+                  },
+                  "parents": [
+                    {
+                      "Feature": {
+                        "crate_name": "features-galore",
+                        "name": "default"
+                      }
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "features-galore",
+                        "name": "json"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "features-galore",
+                            "name": "simple"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "features-galore",
+                                "name": "default"
+                              },
+                              "repeat": true
+                            }
+                          ]
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "features-galore",
+                        "name": "simple"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 45,
+          "line": 2,
+          "message": "exact enabled here",
+          "span": "true"
+        }
+      ],
+      "message": "feature set for crate 'windows-sys = 0.42.0' did not match exactly",
+      "notes": [
+        "'Win32' feature was enabled but not explicitly allowed",
+        "'Win32_System' feature was enabled but not explicitly allowed",
+        "'default' feature was enabled but not explicitly allowed"
+      ],
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]
diff --git a/tests/snapshots/feature_bans__external_default_features-2.snap b/tests/snapshots/feature_bans__external_default_features-2.snap
new file mode 100644
index 00000000..4efadc22
--- /dev/null
+++ b/tests/snapshots/feature_bans__external_default_features-2.snap
@@ -0,0 +1,591 @@
+---
+source: tests/feature_bans.rs
+expression: diags
+---
+[
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "bitflags",
+            "version": "1.3.2"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "bitflags",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "git2",
+                    "version": "0.14.4"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'bitflags = 1.3.2'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "libc",
+            "version": "0.2.134"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "libc",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "git2",
+                    "version": "0.14.4"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      }
+                    }
+                  ]
+                },
+                {
+                  "Krate": {
+                    "name": "jobserver",
+                    "version": "0.1.25"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "cc",
+                        "version": "1.0.73"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "kind": "build",
+                            "name": "libz-sys",
+                            "version": "1.1.8"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "libz-sys",
+                                "name": "libc"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "libgit2-sys",
+                                    "version": "0.13.4+1.4.2"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Krate": {
+                                        "name": "git2",
+                                        "version": "0.14.4"
+                                      },
+                                      "repeat": true
+                                    }
+                                  ]
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "cc",
+                            "name": "jobserver"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "cc",
+                                "name": "parallel"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "kind": "build",
+                                    "name": "libgit2-sys",
+                                    "version": "0.13.4+1.4.2"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "cc",
+                            "name": "parallel"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "cc",
+                        "name": "jobserver"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                },
+                {
+                  "Krate": {
+                    "name": "libgit2-sys",
+                    "version": "0.13.4+1.4.2"
+                  },
+                  "repeat": true
+                },
+                {
+                  "Krate": {
+                    "name": "libz-sys",
+                    "version": "1.1.8"
+                  },
+                  "repeat": true
+                },
+                {
+                  "Krate": {
+                    "name": "parking_lot_core",
+                    "version": "0.9.3"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'libc = 0.2.134'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "percent-encoding",
+            "version": "2.2.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "percent-encoding",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "form_urlencoded",
+                    "version": "1.1.0"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "url",
+                        "version": "2.3.1"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "url",
+                            "name": "default"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "git2",
+                                "version": "0.14.4"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "features-galore",
+                                    "version": "0.1.0"
+                                  }
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "Krate": {
+                    "name": "url",
+                    "version": "2.3.1"
+                  },
+                  "repeat": true
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'percent-encoding = 2.2.0'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "tinyvec",
+            "version": "1.6.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "tinyvec",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "unicode-normalization",
+                    "version": "0.1.22"
+                  },
+                  "parents": [
+                    {
+                      "Feature": {
+                        "crate_name": "unicode-normalization",
+                        "name": "default"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "idna",
+                            "version": "0.3.0"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "url",
+                                "version": "2.3.1"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "url",
+                                    "name": "default"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Krate": {
+                                        "name": "git2",
+                                        "version": "0.14.4"
+                                      },
+                                      "parents": [
+                                        {
+                                          "Krate": {
+                                            "name": "features-galore",
+                                            "version": "0.1.0"
+                                          }
+                                        }
+                                      ]
+                                    }
+                                  ]
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "unicode-normalization",
+                        "name": "std"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "unicode-normalization",
+                            "name": "default"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'tinyvec = 1.6.0'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "unicode-bidi",
+            "version": "0.3.8"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "unicode-bidi",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "idna",
+                    "version": "0.3.0"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "url",
+                        "version": "2.3.1"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "url",
+                            "name": "default"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "git2",
+                                "version": "0.14.4"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "features-galore",
+                                    "version": "0.1.0"
+                                  }
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'unicode-bidi = 0.3.8'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "unicode-normalization",
+            "version": "0.1.22"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "unicode-normalization",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "idna",
+                    "version": "0.3.0"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "url",
+                        "version": "2.3.1"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "url",
+                            "name": "default"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "git2",
+                                "version": "0.14.4"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "features-galore",
+                                    "version": "0.1.0"
+                                  }
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'unicode-normalization = 0.1.22'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "url",
+            "version": "2.3.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "url",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "git2",
+                    "version": "0.14.4"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'url = 2.3.1'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]
diff --git a/tests/snapshots/feature_bans__external_default_features-3.snap b/tests/snapshots/feature_bans__external_default_features-3.snap
new file mode 100644
index 00000000..897b33c0
--- /dev/null
+++ b/tests/snapshots/feature_bans__external_default_features-3.snap
@@ -0,0 +1,543 @@
+---
+source: tests/feature_bans.rs
+expression: diags
+---
+[
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "libc",
+            "version": "0.2.134"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "libc",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "git2",
+                    "version": "0.14.4"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      }
+                    }
+                  ]
+                },
+                {
+                  "Krate": {
+                    "name": "jobserver",
+                    "version": "0.1.25"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "cc",
+                        "version": "1.0.73"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "kind": "build",
+                            "name": "libz-sys",
+                            "version": "1.1.8"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "libz-sys",
+                                "name": "libc"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "libgit2-sys",
+                                    "version": "0.13.4+1.4.2"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Krate": {
+                                        "name": "git2",
+                                        "version": "0.14.4"
+                                      },
+                                      "repeat": true
+                                    }
+                                  ]
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "cc",
+                            "name": "jobserver"
+                          },
+                          "parents": [
+                            {
+                              "Feature": {
+                                "crate_name": "cc",
+                                "name": "parallel"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "kind": "build",
+                                    "name": "libgit2-sys",
+                                    "version": "0.13.4+1.4.2"
+                                  },
+                                  "repeat": true
+                                }
+                              ]
+                            }
+                          ]
+                        },
+                        {
+                          "Feature": {
+                            "crate_name": "cc",
+                            "name": "parallel"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "cc",
+                        "name": "jobserver"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                },
+                {
+                  "Krate": {
+                    "name": "libgit2-sys",
+                    "version": "0.13.4+1.4.2"
+                  },
+                  "repeat": true
+                },
+                {
+                  "Krate": {
+                    "name": "libz-sys",
+                    "version": "1.1.8"
+                  },
+                  "repeat": true
+                },
+                {
+                  "Krate": {
+                    "name": "parking_lot_core",
+                    "version": "0.9.3"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      },
+                      "repeat": true
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'libc = 0.2.134'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "percent-encoding",
+            "version": "2.2.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "percent-encoding",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "form_urlencoded",
+                    "version": "1.1.0"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "url",
+                        "version": "2.3.1"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "url",
+                            "name": "default"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "git2",
+                                "version": "0.14.4"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "features-galore",
+                                    "version": "0.1.0"
+                                  }
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "Krate": {
+                    "name": "url",
+                    "version": "2.3.1"
+                  },
+                  "repeat": true
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'percent-encoding = 2.2.0'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "tinyvec",
+            "version": "1.6.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "tinyvec",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "unicode-normalization",
+                    "version": "0.1.22"
+                  },
+                  "parents": [
+                    {
+                      "Feature": {
+                        "crate_name": "unicode-normalization",
+                        "name": "default"
+                      },
+                      "parents": [
+                        {
+                          "Krate": {
+                            "name": "idna",
+                            "version": "0.3.0"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "url",
+                                "version": "2.3.1"
+                              },
+                              "parents": [
+                                {
+                                  "Feature": {
+                                    "crate_name": "url",
+                                    "name": "default"
+                                  },
+                                  "parents": [
+                                    {
+                                      "Krate": {
+                                        "name": "git2",
+                                        "version": "0.14.4"
+                                      },
+                                      "parents": [
+                                        {
+                                          "Krate": {
+                                            "name": "features-galore",
+                                            "version": "0.1.0"
+                                          }
+                                        }
+                                      ]
+                                    }
+                                  ]
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    },
+                    {
+                      "Feature": {
+                        "crate_name": "unicode-normalization",
+                        "name": "std"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "unicode-normalization",
+                            "name": "default"
+                          },
+                          "repeat": true
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'tinyvec = 1.6.0'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "unicode-bidi",
+            "version": "0.3.8"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "unicode-bidi",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "idna",
+                    "version": "0.3.0"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "url",
+                        "version": "2.3.1"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "url",
+                            "name": "default"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "git2",
+                                "version": "0.14.4"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "features-galore",
+                                    "version": "0.1.0"
+                                  }
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'unicode-bidi = 0.3.8'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "unicode-normalization",
+            "version": "0.1.22"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "unicode-normalization",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "idna",
+                    "version": "0.3.0"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "url",
+                        "version": "2.3.1"
+                      },
+                      "parents": [
+                        {
+                          "Feature": {
+                            "crate_name": "url",
+                            "name": "default"
+                          },
+                          "parents": [
+                            {
+                              "Krate": {
+                                "name": "git2",
+                                "version": "0.14.4"
+                              },
+                              "parents": [
+                                {
+                                  "Krate": {
+                                    "name": "features-galore",
+                                    "version": "0.1.0"
+                                  }
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'unicode-normalization = 0.1.22'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "url",
+            "version": "2.3.1"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "url",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "git2",
+                    "version": "0.14.4"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 29,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'url = 2.3.1'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]
diff --git a/tests/snapshots/feature_bans__external_default_features.snap b/tests/snapshots/feature_bans__external_default_features.snap
new file mode 100644
index 00000000..b1dc5972
--- /dev/null
+++ b/tests/snapshots/feature_bans__external_default_features.snap
@@ -0,0 +1,54 @@
+---
+source: tests/feature_bans.rs
+expression: diags
+---
+[
+  {
+    "fields": {
+      "code": "B015",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "bitflags",
+            "version": "1.3.2"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "bitflags",
+                "name": "default"
+              },
+              "parents": [
+                {
+                  "Krate": {
+                    "name": "git2",
+                    "version": "0.14.4"
+                  },
+                  "parents": [
+                    {
+                      "Krate": {
+                        "name": "features-galore",
+                        "version": "0.1.0"
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 42,
+          "line": 2,
+          "message": "feature denied here",
+          "span": "'default'"
+        }
+      ],
+      "message": "feature 'default' for crate 'bitflags = 1.3.2' is explicitly denied",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]
diff --git a/tests/snapshots/feature_bans__fails_if_not_all_features_allowed.snap b/tests/snapshots/feature_bans__fails_if_not_all_features_allowed.snap
new file mode 100644
index 00000000..5b9aa28d
--- /dev/null
+++ b/tests/snapshots/feature_bans__fails_if_not_all_features_allowed.snap
@@ -0,0 +1,44 @@
+---
+source: tests/feature_bans.rs
+expression: "tu::to_snapshot(diags)"
+---
+[
+  {
+    "fields": {
+      "code": "B014",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "features-galore",
+            "version": "0.1.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "features-galore",
+                "name": "ssh"
+              }
+            },
+            {
+              "Feature": {
+                "crate_name": "features-galore",
+                "name": "zlib"
+              }
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 49,
+          "line": 1,
+          "message": "allowed features",
+          "span": "['ssh']"
+        }
+      ],
+      "message": "feature 'zlib' for crate 'features-galore = 0.1.0' was not explicitly allowed",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]
diff --git a/tests/snapshots/feature_bans__workspace_default_features-2.snap b/tests/snapshots/feature_bans__workspace_default_features-2.snap
new file mode 100644
index 00000000..8412fe37
--- /dev/null
+++ b/tests/snapshots/feature_bans__workspace_default_features-2.snap
@@ -0,0 +1,38 @@
+---
+source: tests/feature_bans.rs
+expression: diags
+---
+[
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "features-galore",
+            "version": "0.1.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "features-galore",
+                "name": "default"
+              }
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 30,
+          "line": 1,
+          "message": "lint level",
+          "span": "'deny'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'features-galore = 0.1.0'",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]
diff --git a/tests/snapshots/feature_bans__workspace_default_features-3.snap b/tests/snapshots/feature_bans__workspace_default_features-3.snap
new file mode 100644
index 00000000..b421cf85
--- /dev/null
+++ b/tests/snapshots/feature_bans__workspace_default_features-3.snap
@@ -0,0 +1,5 @@
+---
+source: tests/feature_bans.rs
+expression: diags
+---
+[]
diff --git a/tests/snapshots/feature_bans__workspace_default_features.snap b/tests/snapshots/feature_bans__workspace_default_features.snap
new file mode 100644
index 00000000..d289495a
--- /dev/null
+++ b/tests/snapshots/feature_bans__workspace_default_features.snap
@@ -0,0 +1,70 @@
+---
+source: tests/feature_bans.rs
+expression: diags
+---
+[
+  {
+    "fields": {
+      "code": "B017",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "features-galore",
+            "version": "0.1.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "features-galore",
+                "name": "default"
+              }
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 30,
+          "line": 1,
+          "message": "lint level",
+          "span": "'warn'"
+        }
+      ],
+      "message": "'default' feature enabled for crate 'features-galore = 0.1.0'",
+      "severity": "warning"
+    },
+    "type": "diagnostic"
+  },
+  {
+    "fields": {
+      "code": "B015",
+      "graphs": [
+        {
+          "Krate": {
+            "name": "features-galore",
+            "version": "0.1.0"
+          },
+          "parents": [
+            {
+              "Feature": {
+                "crate_name": "features-galore",
+                "name": "default"
+              }
+            }
+          ]
+        }
+      ],
+      "labels": [
+        {
+          "column": 49,
+          "line": 2,
+          "message": "feature denied here",
+          "span": "'default'"
+        }
+      ],
+      "message": "feature 'default' for crate 'features-galore = 0.1.0' is explicitly denied",
+      "severity": "error"
+    },
+    "type": "diagnostic"
+  }
+]