Skip to main content

mir_extractor/rules/
supply_chain.rs

1//! Supply chain security rules.
2//!
3//! Rules detecting supply chain vulnerabilities:
4//! - RUSTSEC advisory dependencies
5//! - Yanked crate versions
6//! - Cargo auditable metadata
7//! - Proc-macro side effects
8
9use crate::{Exploitability, Finding, MirPackage, Rule, RuleMetadata, RuleOrigin, Severity};
10use semver::{Version, VersionReq};
11use std::collections::HashSet;
12use std::ffi::OsStr;
13use std::fs;
14use std::path::Path;
15use walkdir::WalkDir;
16
17use super::utils::filter_entry;
18
19// ============================================================================
20// RUSTCOLA018: RUSTSEC Unsound Dependency Rule
21// ============================================================================
22
23struct UnsoundAdvisory {
24    crate_name: &'static str,
25    version_req: &'static str,
26    advisory_id: &'static str,
27    summary: &'static str,
28}
29
30/// Detects dependencies with known RUSTSEC advisories.
31pub struct RustsecUnsoundDependencyRule {
32    metadata: RuleMetadata,
33}
34
35impl RustsecUnsoundDependencyRule {
36    pub fn new() -> Self {
37        Self {
38            metadata: RuleMetadata {
39                id: "RUSTCOLA018".to_string(),
40                name: "rustsec-unsound-dependency".to_string(),
41                short_description: "Dependency has known RUSTSEC advisory".to_string(),
42                full_description: "Detects dependencies that have known soundness issues documented in RUSTSEC advisories.".to_string(),
43                help_uri: Some("https://rustsec.org/".to_string()),
44                default_severity: Severity::High,
45                origin: RuleOrigin::BuiltIn,
46                cwe_ids: Vec::new(),
47                fix_suggestion: None,
48                exploitability: Exploitability::default(),
49            },
50        }
51    }
52
53    fn advisories() -> &'static [UnsoundAdvisory] {
54        &[
55            UnsoundAdvisory {
56                crate_name: "arrayvec",
57                version_req: "<= 0.4.10",
58                advisory_id: "RUSTSEC-2018-0001",
59                summary: "arrayvec::ArrayVec::insert can cause memory corruption",
60            },
61            UnsoundAdvisory {
62                crate_name: "time",
63                version_req: "< 0.2.23",
64                advisory_id: "RUSTSEC-2020-0071",
65                summary: "Potential segfault in localtime_r",
66            },
67            UnsoundAdvisory {
68                crate_name: "crossbeam-deque",
69                version_req: "< 0.7.4",
70                advisory_id: "RUSTSEC-2021-0093",
71                summary: "Race condition may result in double-free",
72            },
73            UnsoundAdvisory {
74                crate_name: "owning_ref",
75                version_req: "< 0.4.2",
76                advisory_id: "RUSTSEC-2022-0044",
77                summary: "Unsound StableAddress impl",
78            },
79            UnsoundAdvisory {
80                crate_name: "smallvec",
81                version_req: "< 1.10.0",
82                advisory_id: "RUSTSEC-2021-0009",
83                summary: "SmallVec::insert_many can cause memory exposure",
84            },
85            UnsoundAdvisory {
86                crate_name: "fixedbitset",
87                version_req: "< 0.4.0",
88                advisory_id: "RUSTSEC-2019-0003",
89                summary: "FixedBitSet::insert unsound aliasing",
90            },
91        ]
92    }
93
94    fn advisory_matches(advisory: &UnsoundAdvisory, version: &Version) -> bool {
95        VersionReq::parse(advisory.version_req)
96            .ok()
97            .map(|req| req.matches(version))
98            .unwrap_or(false)
99    }
100}
101
102impl Rule for RustsecUnsoundDependencyRule {
103    fn metadata(&self) -> &RuleMetadata {
104        &self.metadata
105    }
106
107    fn evaluate(
108        &self,
109        package: &MirPackage,
110        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
111    ) -> Vec<Finding> {
112        let mut findings = Vec::new();
113        let crate_root = Path::new(&package.crate_root);
114        let lock_path = crate_root.join("Cargo.lock");
115
116        if !lock_path.exists() {
117            return findings;
118        }
119
120        let Ok(contents) = fs::read_to_string(&lock_path) else {
121            return findings;
122        };
123
124        let Ok(doc) = toml::from_str::<toml::Value>(&contents) else {
125            return findings;
126        };
127
128        let Some(packages) = doc.get("package").and_then(|value| value.as_array()) else {
129            return findings;
130        };
131
132        let relative_lock = lock_path
133            .strip_prefix(crate_root)
134            .unwrap_or(&lock_path)
135            .to_string_lossy()
136            .replace('\\', "/");
137
138        let mut emitted = HashSet::new();
139
140        for pkg in packages {
141            let Some(name) = pkg.get("name").and_then(|v| v.as_str()) else {
142                continue;
143            };
144            let Some(version_str) = pkg.get("version").and_then(|v| v.as_str()) else {
145                continue;
146            };
147            let Ok(version) = Version::parse(version_str) else {
148                continue;
149            };
150
151            for advisory in Self::advisories() {
152                if advisory.crate_name != name {
153                    continue;
154                }
155
156                if !Self::advisory_matches(advisory, &version) {
157                    continue;
158                }
159
160                let key = (
161                    name.to_string(),
162                    version_str.to_string(),
163                    advisory.advisory_id,
164                );
165                if !emitted.insert(key) {
166                    continue;
167                }
168
169                let evidence = vec![format!(
170                    "{} {} matches {} ({})",
171                    name, version_str, advisory.advisory_id, advisory.summary
172                )];
173
174                findings.push(Finding {
175                    rule_id: self.metadata.id.clone(),
176                    rule_name: self.metadata.name.clone(),
177                    severity: self.metadata.default_severity,
178                    message: format!(
179                        "Dependency '{}' v{} is flagged unsound by advisory {}",
180                        name, version_str, advisory.advisory_id
181                    ),
182                    function: relative_lock.clone(),
183                    function_signature: format!("{} {}", name, version_str),
184                    evidence,
185                    span: None,
186                    ..Default::default()
187                });
188            }
189        }
190
191        findings
192    }
193}
194
195// ============================================================================
196// RUSTCOLA019: Yanked Crate Rule
197// ============================================================================
198
199struct YankedRelease {
200    crate_name: &'static str,
201    version: &'static str,
202    reason: &'static str,
203}
204
205/// Detects dependencies pinned to yanked versions.
206pub struct YankedCrateRule {
207    metadata: RuleMetadata,
208}
209
210impl YankedCrateRule {
211    pub fn new() -> Self {
212        Self {
213            metadata: RuleMetadata {
214                id: "RUSTCOLA019".to_string(),
215                name: "yanked-crate-version".to_string(),
216                short_description: "Dependency references a yanked crate version".to_string(),
217                full_description:
218                    "Highlights crates pinned to versions that have been yanked from crates.io."
219                        .to_string(),
220                help_uri: Some(
221                    "https://doc.rust-lang.org/cargo/reference/publishing.html#removing-a-version"
222                        .to_string(),
223                ),
224                default_severity: Severity::Medium,
225                origin: RuleOrigin::BuiltIn,
226                cwe_ids: Vec::new(),
227                fix_suggestion: None,
228                exploitability: Exploitability::default(),
229            },
230        }
231    }
232
233    fn releases() -> &'static [YankedRelease] {
234        &[
235            YankedRelease {
236                crate_name: "memoffset",
237                version: "0.5.6",
238                reason: "Yanked due to soundness issue (RUSTSEC-2021-0119)",
239            },
240            YankedRelease {
241                crate_name: "chrono",
242                version: "0.4.19",
243                reason: "Yanked pending security fixes",
244            },
245        ]
246    }
247}
248
249impl Rule for YankedCrateRule {
250    fn metadata(&self) -> &RuleMetadata {
251        &self.metadata
252    }
253
254    fn evaluate(
255        &self,
256        package: &MirPackage,
257        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
258    ) -> Vec<Finding> {
259        let mut findings = Vec::new();
260        let crate_root = Path::new(&package.crate_root);
261        let lock_path = crate_root.join("Cargo.lock");
262
263        if !lock_path.exists() {
264            return findings;
265        }
266
267        let Ok(contents) = fs::read_to_string(&lock_path) else {
268            return findings;
269        };
270
271        let Ok(doc) = toml::from_str::<toml::Value>(&contents) else {
272            return findings;
273        };
274
275        let Some(packages) = doc.get("package").and_then(|value| value.as_array()) else {
276            return findings;
277        };
278
279        let relative_lock = lock_path
280            .strip_prefix(crate_root)
281            .unwrap_or(&lock_path)
282            .to_string_lossy()
283            .replace('\\', "/");
284
285        let mut emitted = HashSet::new();
286
287        for pkg in packages {
288            let Some(name) = pkg.get("name").and_then(|v| v.as_str()) else {
289                continue;
290            };
291            let Some(version) = pkg.get("version").and_then(|v| v.as_str()) else {
292                continue;
293            };
294
295            for release in Self::releases() {
296                if release.crate_name != name || release.version != version {
297                    continue;
298                }
299
300                if !emitted.insert((name.to_string(), version.to_string())) {
301                    continue;
302                }
303
304                let evidence = vec![format!("{}: {}", release.crate_name, release.reason)];
305
306                findings.push(Finding {
307                    rule_id: self.metadata.id.clone(),
308                    rule_name: self.metadata.name.clone(),
309                    severity: self.metadata.default_severity,
310                    message: format!(
311                        "Dependency '{}' v{} has been yanked: {}",
312                        name, version, release.reason
313                    ),
314                    function: relative_lock.clone(),
315                    function_signature: format!("{} {}", name, version),
316                    evidence,
317                    span: None,
318                    ..Default::default()
319                });
320            }
321        }
322
323        findings
324    }
325}
326
327// ============================================================================
328// RUSTCOLA020: Cargo Auditable Metadata Rule
329// ============================================================================
330
331/// Detects binary crates missing cargo-auditable metadata.
332pub struct CargoAuditableMetadataRule {
333    metadata: RuleMetadata,
334}
335
336impl CargoAuditableMetadataRule {
337    pub fn new() -> Self {
338        Self {
339            metadata: RuleMetadata {
340                id: "RUSTCOLA020".to_string(),
341                name: "cargo-auditable-metadata".to_string(),
342                short_description: "Binary crate missing cargo auditable metadata".to_string(),
343                full_description: "Detects binary crates that do not integrate cargo auditable metadata, encouraging projects to embed supply-chain provenance in release artifacts.".to_string(),
344                help_uri: Some("https://github.com/rust-secure-code/cargo-auditable".to_string()),
345                default_severity: Severity::Medium,
346                origin: RuleOrigin::BuiltIn,
347                cwe_ids: Vec::new(),
348                fix_suggestion: None,
349                exploitability: Exploitability::default(),
350            },
351        }
352    }
353
354    fn read_manifest(crate_root: &Path) -> Option<toml::Value> {
355        let manifest_path = crate_root.join("Cargo.toml");
356        let contents = fs::read_to_string(manifest_path).ok()?;
357        toml::from_str::<toml::Value>(&contents).ok()
358    }
359
360    fn is_workspace(manifest: &toml::Value) -> bool {
361        manifest.get("workspace").is_some() && manifest.get("package").is_none()
362    }
363
364    fn is_binary_crate(manifest: &toml::Value, crate_root: &Path) -> bool {
365        if Self::is_workspace(manifest) {
366            return false;
367        }
368
369        if manifest
370            .get("bin")
371            .and_then(|value| value.as_array())
372            .map(|arr| !arr.is_empty())
373            .unwrap_or(false)
374        {
375            return true;
376        }
377
378        let src_main = crate_root.join("src").join("main.rs");
379        if src_main.exists() {
380            return true;
381        }
382
383        let bin_dir = crate_root.join("src").join("bin");
384        if let Ok(entries) = fs::read_dir(&bin_dir) {
385            for entry in entries.flatten() {
386                let path = entry.path();
387                if path.is_file()
388                    && path
389                        .extension()
390                        .and_then(OsStr::to_str)
391                        .map_or(false, |ext| ext.eq_ignore_ascii_case("rs"))
392                {
393                    return true;
394                }
395
396                if path.is_dir() && path.join("main.rs").exists() {
397                    return true;
398                }
399            }
400        }
401
402        false
403    }
404
405    fn metadata_skip(manifest: &toml::Value) -> bool {
406        manifest
407            .get("package")
408            .and_then(|pkg| pkg.get("metadata"))
409            .and_then(|metadata| match metadata {
410                toml::Value::Table(table) => table.get("rust-cola"),
411                _ => None,
412            })
413            .and_then(|rust_cola| match rust_cola {
414                toml::Value::Table(table) => table.get("skip_auditable_check"),
415                _ => None,
416            })
417            .and_then(|value| value.as_bool())
418            .unwrap_or(false)
419    }
420
421    fn has_auditable_markers(manifest: &toml::Value) -> bool {
422        let marker_keys = ["auditable", "cargo-auditable"];
423
424        let dep_tables = [
425            manifest.get("dependencies"),
426            manifest.get("dev-dependencies"),
427            manifest.get("build-dependencies"),
428        ];
429
430        if dep_tables.iter().any(|value| match value {
431            Some(toml::Value::Table(table)) => table
432                .keys()
433                .any(|key| marker_keys.iter().any(|mk| key.contains(mk))),
434            _ => false,
435        }) {
436            return true;
437        }
438
439        if manifest
440            .get("package")
441            .and_then(|pkg| pkg.get("metadata"))
442            .and_then(|metadata| match metadata {
443                toml::Value::Table(table) => Some(table),
444                _ => None,
445            })
446            .map(|metadata_table| {
447                metadata_table.keys().any(|key| marker_keys.iter().any(|mk| key.contains(mk)))
448                    || metadata_table.values().any(|value| matches!(value, toml::Value::Table(inner) if inner.keys().any(|k| k.contains("auditable"))))
449            })
450            .unwrap_or(false)
451        {
452            return true;
453        }
454
455        if let Some(toml::Value::Table(features)) = manifest.get("features") {
456            if features.iter().any(|(key, value)| {
457                key.contains("auditable")
458                    || matches!(value, toml::Value::Array(items) if items.iter().any(|item| item.as_str().map_or(false, |s| s.contains("auditable"))))
459            }) {
460                return true;
461            }
462        }
463
464        false
465    }
466
467    fn lockfile_mentions_auditable(crate_root: &Path) -> bool {
468        let lock_path = crate_root.join("Cargo.lock");
469        let Ok(contents) = fs::read_to_string(lock_path) else {
470            return false;
471        };
472
473        contents.to_lowercase().contains("auditable")
474    }
475
476    fn ci_mentions_cargo_auditable(crate_root: &Path) -> bool {
477        const MAX_ANCESTOR_SEARCH: usize = 5;
478
479        for ancestor in crate_root.ancestors().take(MAX_ANCESTOR_SEARCH) {
480            if Self::ci_markers_within(ancestor) {
481                return true;
482            }
483        }
484
485        false
486    }
487
488    fn ci_markers_within(base: &Path) -> bool {
489        let search_dirs = [".github", ".gitlab", "ci", "scripts"];
490        for dir in &search_dirs {
491            let path = base.join(dir);
492            if !path.exists() {
493                continue;
494            }
495
496            for entry in WalkDir::new(&path)
497                .max_depth(6)
498                .into_iter()
499                .filter_entry(|e| {
500                    // Allow root entry
501                    if e.depth() == 0 {
502                        return true;
503                    }
504                    let name = e.file_name().to_string_lossy();
505                    // Only filter out specific directories that aren't CI-related
506                    if e.file_type().is_dir() {
507                        return !matches!(
508                            name.as_ref(),
509                            "target" | ".git" | ".cola-cache" | "out" | "node_modules"
510                        );
511                    }
512                    true
513                })
514            {
515                let entry = match entry {
516                    Ok(e) => e,
517                    Err(_) => continue,
518                };
519
520                if !entry.file_type().is_file() {
521                    continue;
522                }
523
524                if let Ok(metadata) = entry.metadata() {
525                    if metadata.len() > 512 * 1024 {
526                        continue;
527                    }
528                }
529
530                if fs::read_to_string(entry.path())
531                    .map(|contents| contents.to_lowercase().contains("cargo auditable"))
532                    .unwrap_or(false)
533                {
534                    return true;
535                }
536            }
537        }
538
539        let marker_files = ["Makefile", "makefile", "Justfile", "justfile"];
540        for file in &marker_files {
541            let path = base.join(file);
542            if !path.exists() {
543                continue;
544            }
545
546            if fs::read_to_string(&path)
547                .map(|contents| contents.to_lowercase().contains("cargo auditable"))
548                .unwrap_or(false)
549            {
550                return true;
551            }
552        }
553
554        false
555    }
556}
557
558impl Rule for CargoAuditableMetadataRule {
559    fn metadata(&self) -> &RuleMetadata {
560        &self.metadata
561    }
562
563    fn evaluate(
564        &self,
565        package: &MirPackage,
566        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
567    ) -> Vec<Finding> {
568        let mut findings = Vec::new();
569        let crate_root = Path::new(&package.crate_root);
570
571        let Some(manifest) = Self::read_manifest(crate_root) else {
572            return findings;
573        };
574
575        if !Self::is_binary_crate(&manifest, crate_root) {
576            return findings;
577        }
578
579        if Self::metadata_skip(&manifest) {
580            return findings;
581        }
582
583        if Self::has_auditable_markers(&manifest)
584            || Self::lockfile_mentions_auditable(crate_root)
585            || Self::ci_mentions_cargo_auditable(crate_root)
586        {
587            return findings;
588        }
589
590        let package_name = manifest
591            .get("package")
592            .and_then(|pkg| pkg.get("name"))
593            .and_then(|name| name.as_str())
594            .unwrap_or(&package.crate_name);
595
596        let mut evidence = Vec::new();
597        if crate_root.join("src").join("main.rs").exists() {
598            evidence.push("Found src/main.rs binary entry point".to_string());
599        }
600
601        if crate_root.join("src").join("bin").exists() {
602            evidence.push("Found src/bin directory indicating additional binaries".to_string());
603        }
604
605        if manifest
606            .get("bin")
607            .and_then(|value| value.as_array())
608            .map(|arr| !arr.is_empty())
609            .unwrap_or(false)
610        {
611            evidence.push("Cargo.toml defines [[bin]] targets".to_string());
612        }
613
614        evidence.push(
615            "No cargo auditable dependency, metadata, lockfile entry, or CI integration detected"
616                .to_string(),
617        );
618
619        let manifest_path = crate_root.join("Cargo.toml");
620        let relative_manifest = manifest_path
621            .strip_prefix(crate_root)
622            .unwrap_or(&manifest_path)
623            .to_string_lossy()
624            .replace('\\', "/");
625
626        findings.push(Finding {
627            rule_id: self.metadata.id.clone(),
628            rule_name: self.metadata.name.clone(),
629            severity: self.metadata.default_severity,
630            message: format!(
631                "Binary crate '{}' is missing cargo auditable metadata; consider integrating cargo auditable builds",
632                package_name
633            ),
634            function: relative_manifest,
635            function_signature: package_name.to_string(),
636            evidence,
637            span: None,
638                    ..Default::default()
639        });
640
641        findings
642    }
643}
644
645// ============================================================================
646// RUSTCOLA102: Proc-Macro Side Effects Rule
647// ============================================================================
648
649/// Detects proc-macro crates with potential side effects (filesystem, network access).
650///
651/// Proc-macros run at compile time with full system access. Malicious or compromised
652/// proc-macros can exfiltrate data, download payloads, or modify the build.
653pub struct ProcMacroSideEffectsRule {
654    metadata: RuleMetadata,
655}
656
657impl ProcMacroSideEffectsRule {
658    pub fn new() -> Self {
659        Self {
660            metadata: RuleMetadata {
661                id: "RUSTCOLA102".to_string(),
662                name: "proc-macro-side-effects".to_string(),
663                short_description: "Proc-macro with suspicious side effects".to_string(),
664                full_description: "Detects proc-macro crates that use filesystem, network, or \
665                    process APIs. Proc-macros execute at compile time with full system access, \
666                    making them a supply chain attack vector. Patterns include: std::fs, \
667                    std::net, std::process::Command, reqwest, and similar."
668                    .to_string(),
669                help_uri: Some(
670                    "https://doc.rust-lang.org/reference/procedural-macros.html".to_string(),
671                ),
672                default_severity: Severity::High,
673                origin: RuleOrigin::BuiltIn,
674                cwe_ids: Vec::new(),
675                fix_suggestion: None,
676                exploitability: Exploitability::default(),
677            },
678        }
679    }
680
681    /// Suspicious patterns for proc-macros
682    fn suspicious_patterns() -> &'static [(&'static str, &'static str)] {
683        &[
684            ("std::fs::", "filesystem access in proc-macro"),
685            ("std::net::", "network access in proc-macro"),
686            ("std::process::Command", "process spawning in proc-macro"),
687            ("tokio::", "async runtime in proc-macro (unusual)"),
688            ("reqwest::", "HTTP client in proc-macro"),
689            ("hyper::", "HTTP library in proc-macro"),
690            ("curl::", "curl bindings in proc-macro"),
691            ("attohttpc::", "HTTP client in proc-macro"),
692            ("ureq::", "HTTP client in proc-macro"),
693            ("env!(", "environment variable access (may leak secrets)"),
694            ("include_bytes!", "includes external file at compile time"),
695            ("include_str!", "includes external file at compile time"),
696        ]
697    }
698}
699
700impl Rule for ProcMacroSideEffectsRule {
701    fn metadata(&self) -> &RuleMetadata {
702        &self.metadata
703    }
704
705    fn evaluate(
706        &self,
707        package: &MirPackage,
708        _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
709    ) -> Vec<Finding> {
710        if package.crate_name == "mir-extractor" {
711            return Vec::new();
712        }
713
714        let mut findings = Vec::new();
715        let crate_root = Path::new(&package.crate_root);
716
717        if !crate_root.exists() {
718            return findings;
719        }
720
721        // Check if this is a proc-macro crate
722        let cargo_toml = crate_root.join("Cargo.toml");
723        if !cargo_toml.exists() {
724            return findings;
725        }
726
727        let cargo_content = match std::fs::read_to_string(&cargo_toml) {
728            Ok(c) => c,
729            Err(_) => return findings,
730        };
731
732        // Only analyze proc-macro crates
733        let is_proc_macro = cargo_content.contains("proc-macro = true")
734            || cargo_content.contains("proc_macro = true");
735
736        if !is_proc_macro {
737            return findings;
738        }
739
740        // Scan source files for suspicious patterns
741        for entry in WalkDir::new(crate_root)
742            .into_iter()
743            .filter_entry(|e| filter_entry(e))
744        {
745            let entry = match entry {
746                Ok(e) => e,
747                Err(_) => continue,
748            };
749
750            if !entry.file_type().is_file() {
751                continue;
752            }
753
754            let path = entry.path();
755            if path.extension() != Some(std::ffi::OsStr::new("rs")) {
756                continue;
757            }
758
759            let rel_path = path
760                .strip_prefix(crate_root)
761                .unwrap_or(path)
762                .to_string_lossy()
763                .replace('\\', "/");
764
765            let content = match std::fs::read_to_string(path) {
766                Ok(c) => c,
767                Err(_) => continue,
768            };
769
770            let lines: Vec<&str> = content.lines().collect();
771
772            for (idx, line) in lines.iter().enumerate() {
773                let trimmed = line.trim();
774
775                // Skip comments
776                if trimmed.starts_with("//") {
777                    continue;
778                }
779
780                for (pattern, description) in Self::suspicious_patterns() {
781                    if trimmed.contains(pattern) {
782                        let location = format!("{}:{}", rel_path, idx + 1);
783
784                        findings.push(Finding {
785                            rule_id: self.metadata.id.clone(),
786                            rule_name: self.metadata.name.clone(),
787                            severity: self.metadata.default_severity,
788                            message: format!(
789                                "Suspicious {} in proc-macro crate. Proc-macros execute at \
790                                compile time with full system access. This could be a supply \
791                                chain attack vector. Review carefully.",
792                                description
793                            ),
794                            function: location,
795                            function_signature: String::new(),
796                            evidence: vec![trimmed.to_string()],
797                            span: None,
798                            ..Default::default()
799                        });
800                    }
801                }
802            }
803        }
804
805        findings
806    }
807}
808
809/// Register all supply chain rules with the rule engine.
810pub fn register_supply_chain_rules(engine: &mut crate::RuleEngine) {
811    engine.register_rule(Box::new(RustsecUnsoundDependencyRule::new()));
812    engine.register_rule(Box::new(YankedCrateRule::new()));
813    engine.register_rule(Box::new(CargoAuditableMetadataRule::new()));
814    engine.register_rule(Box::new(ProcMacroSideEffectsRule::new()));
815}