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

Issue:
Add allow list for crates with build.rs
We talked about adding support for an explicit list of crates that we allow having `build.rs` build step. 

`build.rs` is not sandboxed so it usually has full access to the developer machine it is running on which is a potential exploit surface + we generally want to keep custom build steps and C/C++ dependencies to a mininum to reduce complexity and improve build times.

Related to https://github.com/EmbarkStudios/rust-ecosystem/issues/17


Discussion / hints:

Fix patch:
diff --git a/docs/src/checks/bans/cfg.md b/docs/src/checks/bans/cfg.md
index 795a1e8a..e88ac6cc 100644
--- a/docs/src/checks/bans/cfg.md
+++ b/docs/src/checks/bans/cfg.md
@@ -73,3 +73,7 @@ When dealing with duplicate versions, it's often the case that a particular crat
 Note that by default, the `depth` is infinite.
 
 **NOTE:** `skip-tree` is a very big hammer at the moment, and should be used with care.
+
+### 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.
diff --git a/docs/src/checks/bans/diags.md b/docs/src/checks/bans/diags.md
index ba3a86ef..d72dbde9 100644
--- a/docs/src/checks/bans/diags.md
+++ b/docs/src/checks/bans/diags.md
@@ -43,3 +43,7 @@ A crate version in [`bans.skip-tree`](cfg.md#the-skip-tree-field-optional) was n
 ### `B011` - skipping crate due to root skip
 
 A crate was skipped from being checked as a duplicate due to being transitively referenced by a crate version in [`bans.skip-tree`](cfg.md#the-skip-tree-field-optional).
+
+### `B012` - crate has build script but is not allowed to have one
+
+A crate which has been denied because it has a build script but is not part of the [`bans.allow-build-script`](cfg.md#the-allow-build-scripts-field-optional) list.
diff --git a/examples/10_allow_build_script/Cargo.lock b/examples/10_allow_build_script/Cargo.lock
new file mode 100644
index 00000000..e52e820b
--- /dev/null
+++ b/examples/10_allow_build_script/Cargo.lock
@@ -0,0 +1,53 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "allow-build-script"
+version = "0.1.0"
+dependencies = [
+ "openssl-sys",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "libc"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
diff --git a/examples/10_allow_build_script/Cargo.toml b/examples/10_allow_build_script/Cargo.toml
new file mode 100644
index 00000000..b8e1045a
--- /dev/null
+++ b/examples/10_allow_build_script/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "allow-build-script"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+openssl-sys = "0.9.74"
diff --git a/examples/10_allow_build_script/deny.toml b/examples/10_allow_build_script/deny.toml
new file mode 100644
index 00000000..3846f7d0
--- /dev/null
+++ b/examples/10_allow_build_script/deny.toml
@@ -0,0 +1,5 @@
+[bans]
+allow-build-scripts = [
+    { name = "openssl-sys" },
+    { name = "libc" },
+]
diff --git a/examples/10_allow_build_script/src/lib.rs b/examples/10_allow_build_script/src/lib.rs
new file mode 100644
index 00000000..1b4a90c9
--- /dev/null
+++ b/examples/10_allow_build_script/src/lib.rs
@@ -0,0 +1,8 @@
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        let result = 2 + 2;
+        assert_eq!(result, 4);
+    }
+}
diff --git a/src/bans.rs b/src/bans.rs
index 914752b5..5eedc200 100644
--- a/src/bans.rs
+++ b/src/bans.rs
@@ -190,6 +190,7 @@ pub fn check(
         highlight,
         tree_skipped,
         wildcards,
+        allow_build_scripts,
     } = ctx.cfg;
 
     let krate_spans = &ctx.krate_spans;
@@ -444,6 +445,23 @@ pub fn check(
             }
         }
 
+        if let Some(allow_build_scripts) = &allow_build_scripts {
+            let has_build_script = krate
+                .targets
+                .iter()
+                .any(|t| t.kind.iter().any(|k| *k == "custom-build"));
+
+            if has_build_script {
+                let allowed_build_script = allow_build_scripts.value.iter().any(|id| {
+                    krate.name == id.name && crate::match_req(&krate.version, id.version.as_ref())
+                });
+
+                if !allowed_build_script {
+                    pack.push(diags::BuildScriptNotAllowed { krate });
+                }
+            }
+        }
+
         if !pack.is_empty() {
             sink.push(pack);
         }
diff --git a/src/bans/cfg.rs b/src/bans/cfg.rs
index f2d126c4..c3cf661a 100644
--- a/src/bans/cfg.rs
+++ b/src/bans/cfg.rs
@@ -91,6 +91,8 @@ pub struct Config {
     /// How to handle wildcard dependencies
     #[serde(default = "crate::lint_allow")]
     pub wildcards: LintLevel,
+    /// List of crates that are allowed to have a build step.
+    pub allow_build_scripts: Option<Spanned<Vec<CrateId>>>,
 }
 
 impl Default for Config {
@@ -103,6 +105,7 @@ impl Default for Config {
             skip: Vec::new(),
             skip_tree: Vec::new(),
             wildcards: LintLevel::Allow,
+            allow_build_scripts: None,
         }
     }
 }
@@ -183,6 +186,18 @@ impl crate::cfg::UnvalidatedConfig for Config {
                 .into_iter()
                 .map(crate::Spanned::from)
                 .collect(),
+            allow_build_scripts: self.allow_build_scripts.map(|v| {
+                Spanned::new(
+                    v.value
+                        .into_iter()
+                        .map(|id| KrateId {
+                            name: id.name,
+                            version: id.version,
+                        })
+                        .collect(),
+                    v.span,
+                )
+            }),
         }
     }
 }
@@ -209,6 +224,7 @@ pub struct ValidConfig {
     pub(crate) skipped: Vec<Skrate>,
     pub(crate) tree_skipped: Vec<Spanned<TreeSkip>>,
     pub wildcards: LintLevel,
+    pub allow_build_scripts: Option<Spanned<Vec<KrateId>>>,
 }
 
 #[cfg(test)]
diff --git a/src/bans/diags.rs b/src/bans/diags.rs
index 79601c48..e090553c 100644
--- a/src/bans/diags.rs
+++ b/src/bans/diags.rs
@@ -221,3 +221,23 @@ impl<'a> From<SkippedByRoot<'a>> for Diag {
             .into()
     }
 }
+
+pub(crate) struct BuildScriptNotAllowed<'a> {
+    pub(crate) krate: &'a Krate,
+}
+
+impl<'a> From<BuildScriptNotAllowed<'a>> for Diag {
+    fn from(bs: BuildScriptNotAllowed<'a>) -> Self {
+        Diagnostic::new(Severity::Error)
+            .with_message(format!(
+                "crate '{}' has a build script but is not allowed to have one",
+                bs.krate
+            ))
+            .with_code("B012")
+            .with_notes(vec![
+                "the `bans.allow-build-scripts` field did not contain a match for the crate"
+                    .to_owned(),
+            ])
+            .into()
+    }
+}


Test patch: