rta-core 0.3.0

Core analyzers and report model for the Rust Technical Audit Toolkit
Documentation
use std::collections::BTreeSet;

use crate::{analyzers::Analyzer, collector::RepositorySnapshot, model::DependencyHealth};

pub struct DependencyAnalyzer;

impl Analyzer<DependencyHealth> for DependencyAnalyzer {
    fn analyze(&self, snapshot: &RepositorySnapshot) -> DependencyHealth {
        let mut direct = BTreeSet::new();
        let mut critical_dependencies = Vec::new();
        let mut outdated_indicators = Vec::new();
        let mut maintenance_risks = Vec::new();

        for manifest in &snapshot.manifests {
            for (name, version) in manifest
                .dependencies
                .iter()
                .chain(manifest.dev_dependencies.iter())
                .chain(manifest.build_dependencies.iter())
            {
                direct.insert(name.clone());
                if is_critical(name) {
                    critical_dependencies.push(format!("{name} in {}", manifest.relative_path));
                }
                if version.contains('*') || version.contains("path") || version.contains("git") {
                    maintenance_risks.push(format!(
                        "{name} uses a non-registry or broad version declaration in {}",
                        manifest.relative_path
                    ));
                }
                if version.starts_with("0.") || version.contains("\"0.") {
                    outdated_indicators.push(format!(
                        "{name} is pinned to pre-1.0 API surface in {}",
                        manifest.relative_path
                    ));
                }
            }
        }

        let total_dependencies = direct.len();
        if total_dependencies > 80 {
            maintenance_risks
                .push("Very high direct dependency count for due diligence review.".into());
        }

        let mut score = 100_i32;
        score -= (maintenance_risks.len() as i32 * 8).min(32);
        score -= (outdated_indicators.len() as i32 * 4).min(24);
        score -= if total_dependencies > 60 { 15 } else { 0 };
        score -= if critical_dependencies.len() > 12 {
            8
        } else {
            0
        };

        DependencyHealth {
            total_dependencies,
            direct_dependencies: total_dependencies,
            critical_dependencies,
            outdated_indicators,
            maintenance_risks,
            score: score.clamp(0, 100) as u8,
        }
    }
}

fn is_critical(name: &str) -> bool {
    matches!(
        name,
        "tokio"
            | "serde"
            | "sqlx"
            | "diesel"
            | "axum"
            | "actix-web"
            | "hyper"
            | "reqwest"
            | "openssl"
            | "ring"
            | "rustls"
            | "tonic"
            | "prost"
    )
}

#[cfg(test)]
mod tests {
    use std::{collections::BTreeMap, path::PathBuf};

    use crate::{
        analyzers::Analyzer,
        collector::{CargoManifest, RepositorySnapshot},
    };

    use super::DependencyAnalyzer;

    #[test]
    fn empty_repository_has_no_dependency_risk() {
        let report = DependencyAnalyzer.analyze(&snapshot(Vec::new()));

        assert_eq!(report.direct_dependencies, 0);
        assert_eq!(report.score, 100);
    }

    #[test]
    fn typical_manifest_detects_direct_and_critical_dependencies() {
        let report = DependencyAnalyzer.analyze(&snapshot(vec![manifest(
            "Cargo.toml",
            "app",
            [("serde", "1"), ("tokio", "1")],
            [],
            [],
        )]));

        assert_eq!(report.direct_dependencies, 2);
        assert_eq!(report.critical_dependencies.len(), 2);
        assert_eq!(report.score, 100);
    }

    #[test]
    fn extreme_dependency_count_is_penalized() {
        let dependencies = (0..81)
            .map(|index| (format!("dep_{index}"), "1".to_string()))
            .collect::<BTreeMap<_, _>>();
        let report = DependencyAnalyzer.analyze(&snapshot(vec![CargoManifest {
            relative_path: "Cargo.toml".into(),
            package_name: Some("app".into()),
            workspace_members: Vec::new(),
            dependencies,
            dev_dependencies: BTreeMap::new(),
            build_dependencies: BTreeMap::new(),
        }]));

        assert_eq!(report.direct_dependencies, 81);
        assert!(report.score < 90);
        assert!(report
            .maintenance_risks
            .iter()
            .any(|risk| risk.contains("Very high direct dependency count")));
    }

    #[test]
    fn adversarial_dependency_names_do_not_match_critical_substrings() {
        let report = DependencyAnalyzer.analyze(&snapshot(vec![manifest(
            "Cargo.toml",
            "app",
            [("serde_fake", "1"), ("tokioish", "1")],
            [],
            [],
        )]));

        assert!(report.critical_dependencies.is_empty());
    }

    fn snapshot(manifests: Vec<CargoManifest>) -> RepositorySnapshot {
        RepositorySnapshot {
            root: PathBuf::from("/tmp/repo"),
            files: Vec::new(),
            manifests,
        }
    }

    fn manifest<const D: usize, const DEV: usize, const B: usize>(
        relative_path: &str,
        package_name: &str,
        dependencies: [(&str, &str); D],
        dev_dependencies: [(&str, &str); DEV],
        build_dependencies: [(&str, &str); B],
    ) -> CargoManifest {
        CargoManifest {
            relative_path: relative_path.into(),
            package_name: Some(package_name.into()),
            workspace_members: Vec::new(),
            dependencies: dependency_map(dependencies),
            dev_dependencies: dependency_map(dev_dependencies),
            build_dependencies: dependency_map(build_dependencies),
        }
    }

    fn dependency_map<const N: usize>(dependencies: [(&str, &str); N]) -> BTreeMap<String, String> {
        dependencies
            .into_iter()
            .map(|(name, version)| (name.to_string(), version.to_string()))
            .collect()
    }
}