Skip to main content

agentshield/analysis/
supply_chain.rs

1//! Supply chain analysis: lockfile presence, version pinning, typosquat detection.
2
3use crate::ir::dependency_surface::{DependencyIssue, DependencyIssueType, DependencySurface};
4
5/// Well-known Python package names for typosquat comparison.
6const POPULAR_PYTHON_PACKAGES: &[&str] = &[
7    "requests",
8    "flask",
9    "django",
10    "numpy",
11    "pandas",
12    "scipy",
13    "boto3",
14    "fastapi",
15    "uvicorn",
16    "httpx",
17    "aiohttp",
18    "pillow",
19    "pydantic",
20    "sqlalchemy",
21    "celery",
22    "redis",
23    "psycopg2",
24    "pytest",
25    "setuptools",
26    "cryptography",
27    "paramiko",
28    "pyyaml",
29    "jinja2",
30    "beautifulsoup4",
31    "selenium",
32    "scrapy",
33    "tensorflow",
34    "pytorch",
35    "transformers",
36    "langchain",
37    "openai",
38    "anthropic",
39];
40
41/// Well-known npm package names for typosquat comparison.
42const POPULAR_NPM_PACKAGES: &[&str] = &[
43    "express",
44    "react",
45    "lodash",
46    "axios",
47    "chalk",
48    "commander",
49    "next",
50    "typescript",
51    "webpack",
52    "eslint",
53    "prettier",
54    "jest",
55    "mongoose",
56    "sequelize",
57    "prisma",
58    "fastify",
59    "socket.io",
60    "dotenv",
61    "cors",
62    "jsonwebtoken",
63    "bcrypt",
64    "nodemailer",
65    "openai",
66    "langchain",
67    "zod",
68    "drizzle-orm",
69];
70
71/// Check dependencies for typosquat candidates.
72///
73/// Returns issues for any dependency whose name is within Levenshtein
74/// distance 1-2 of a known popular package but is not an exact match.
75pub fn check_typosquats(deps: &DependencySurface) -> Vec<DependencyIssue> {
76    let mut issues = Vec::new();
77
78    let all_popular: Vec<&str> = POPULAR_PYTHON_PACKAGES
79        .iter()
80        .chain(POPULAR_NPM_PACKAGES.iter())
81        .copied()
82        .collect();
83
84    for dep in &deps.dependencies {
85        let name = dep.name.to_lowercase();
86        for &popular in &all_popular {
87            if name == popular {
88                continue;
89            }
90            let distance = levenshtein::levenshtein(&name, popular);
91            if distance > 0 && distance <= 2 {
92                issues.push(DependencyIssue {
93                    issue_type: DependencyIssueType::PossibleTyposquat,
94                    package_name: dep.name.clone(),
95                    description: format!(
96                        "Package '{}' is similar to popular package '{}' (edit distance {})",
97                        dep.name, popular, distance
98                    ),
99                });
100            }
101        }
102    }
103
104    issues
105}