1use crate::facts::Ecosystem;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Suspect {
13 pub name: String,
15 pub nearest: String,
17 pub distance: usize,
19}
20
21const POPULAR_NPM: &[&str] = &[
23 "react",
24 "lodash",
25 "express",
26 "chalk",
27 "axios",
28 "webpack",
29 "commander",
30 "request",
31 "moment",
32 "debug",
33 "async",
34 "bluebird",
35 "underscore",
36 "jquery",
37 "typescript",
38 "eslint",
39 "prettier",
40 "vue",
41 "angular",
42 "next",
43 "dotenv",
44 "uuid",
45 "cors",
46 "body-parser",
47 "mongoose",
48 "redux",
49 "jest",
50 "mocha",
51 "yargs",
52 "glob",
53 "semver",
54 "colors",
55 "node-fetch",
56 "ws",
57 "socket.io",
58 "rxjs",
59];
60
61const POPULAR_CARGO: &[&str] = &[
63 "serde",
64 "tokio",
65 "rand",
66 "log",
67 "clap",
68 "anyhow",
69 "thiserror",
70 "regex",
71 "syn",
72 "quote",
73 "libc",
74 "reqwest",
75 "hyper",
76 "futures",
77 "chrono",
78 "itertools",
79 "bytes",
80 "tracing",
81 "async-trait",
82 "rayon",
83 "base64",
84 "url",
85 "uuid",
86 "once_cell",
87 "lazy_static",
88 "bitflags",
89 "cfg-if",
90 "proc-macro2",
91 "serde_json",
92];
93
94const POPULAR_PYPI: &[&str] = &[
96 "requests",
97 "numpy",
98 "pandas",
99 "flask",
100 "django",
101 "pytest",
102 "pillow",
103 "scipy",
104 "boto3",
105 "urllib3",
106 "click",
107 "jinja2",
108 "setuptools",
109 "wheel",
110 "six",
111 "certifi",
112 "idna",
113 "cryptography",
114];
115
116const POPULAR_GO: &[&str] = &[
118 "github.com/gin-gonic/gin",
119 "github.com/gorilla/mux",
120 "github.com/stretchr/testify",
121 "github.com/spf13/cobra",
122 "github.com/spf13/viper",
123 "github.com/sirupsen/logrus",
124 "github.com/pkg/errors",
125 "github.com/google/uuid",
126 "github.com/prometheus/client_golang",
127 "github.com/aws/aws-sdk-go",
128 "golang.org/x/crypto",
129 "golang.org/x/net",
130 "golang.org/x/sys",
131 "google.golang.org/grpc",
132 "google.golang.org/protobuf",
133 "go.uber.org/zap",
134 "gorm.io/gorm",
135];
136
137const POPULAR_RUBY: &[&str] = &[
138 "rails",
139 "rake",
140 "bundler",
141 "rspec",
142 "puma",
143 "sinatra",
144 "nokogiri",
145 "devise",
146 "sidekiq",
147 "pg",
148 "mysql2",
149 "redis",
150 "faraday",
151 "rubocop",
152 "activerecord",
153 "actionpack",
154 "activesupport",
155 "json",
156 "minitest",
157 "capybara",
158 "webmock",
159 "kaminari",
160 "jbuilder",
161 "dotenv",
162 "httparty",
163 "pry",
164];
165
166const POPULAR_PHP: &[&str] = &[
167 "symfony/console",
168 "symfony/http-foundation",
169 "guzzlehttp/guzzle",
170 "monolog/monolog",
171 "laravel/framework",
172 "phpunit/phpunit",
173 "doctrine/orm",
174 "psr/log",
175 "twig/twig",
176 "ramsey/uuid",
177 "league/flysystem",
178 "fakerphp/faker",
179 "phpstan/phpstan",
180 "nikic/php-parser",
181];
182
183const POPULAR_MAVEN: &[&str] = &[
184 "com.google.guava:guava",
185 "org.apache.commons:commons-lang3",
186 "com.fasterxml.jackson.core:jackson-databind",
187 "org.slf4j:slf4j-api",
188 "junit:junit",
189 "org.springframework:spring-core",
190 "org.springframework.boot:spring-boot",
191 "com.squareup.okhttp3:okhttp",
192 "org.apache.logging.log4j:log4j-core",
193 "commons-io:commons-io",
194 "org.projectlombok:lombok",
195 "org.mockito:mockito-core",
196 "ch.qos.logback:logback-classic",
197];
198
199const POPULAR_NUGET: &[&str] = &[
200 "Newtonsoft.Json",
201 "Serilog",
202 "AutoMapper",
203 "Dapper",
204 "xunit",
205 "NUnit",
206 "Moq",
207 "FluentValidation",
208 "Polly",
209 "MediatR",
210 "Swashbuckle.AspNetCore",
211 "Microsoft.EntityFrameworkCore",
212 "NLog",
213 "RestSharp",
214 "FluentAssertions",
215];
216
217fn popular_for(eco: Ecosystem) -> &'static [&'static str] {
218 match eco {
219 Ecosystem::Npm => POPULAR_NPM,
220 Ecosystem::Cargo => POPULAR_CARGO,
221 Ecosystem::PyPI => POPULAR_PYPI,
222 Ecosystem::Go => POPULAR_GO,
223 Ecosystem::Ruby => POPULAR_RUBY,
224 Ecosystem::Php => POPULAR_PHP,
225 Ecosystem::Maven => POPULAR_MAVEN,
226 Ecosystem::NuGet => POPULAR_NUGET,
227 }
228}
229
230fn edit_distance(a: &str, b: &str) -> usize {
232 let a: Vec<char> = a.chars().collect();
233 let b: Vec<char> = b.chars().collect();
234 let mut prev: Vec<usize> = (0..=b.len()).collect();
235 let mut cur = vec![0usize; b.len() + 1];
236 for (i, &ca) in a.iter().enumerate() {
237 cur[0] = i + 1;
238 for (j, &cb) in b.iter().enumerate() {
239 let cost = if ca == cb { 0 } else { 1 };
240 cur[j + 1] = (prev[j + 1] + 1).min(cur[j] + 1).min(prev[j] + cost);
241 }
242 std::mem::swap(&mut prev, &mut cur);
243 }
244 prev[b.len()]
245}
246
247pub fn scan<'a>(names: impl IntoIterator<Item = &'a str>, ecosystem: Ecosystem) -> Vec<Suspect> {
249 let popular = popular_for(ecosystem);
250 let mut out = Vec::new();
251 for name in names {
252 let lname = name.to_lowercase();
253 if popular.iter().any(|p| *p == lname) {
255 continue;
256 }
257 if let Some((nearest, dist)) = popular
259 .iter()
260 .map(|p| (*p, edit_distance(&lname, p)))
261 .min_by_key(|(_, d)| *d)
262 {
263 if (1..=2).contains(&dist) && lname.len() >= 4 {
265 out.push(Suspect {
266 name: name.to_string(),
267 nearest: nearest.to_string(),
268 distance: dist,
269 });
270 }
271 }
272 }
273 out
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn flags_close_typosquats() {
282 let s = scan(["lodahs", "expres", "reqwests"], Ecosystem::Npm);
283 assert!(s
284 .iter()
285 .any(|x| x.name == "lodahs" && x.nearest == "lodash"));
286 assert!(s
287 .iter()
288 .any(|x| x.name == "expres" && x.nearest == "express"));
289 }
290
291 #[test]
292 fn does_not_flag_exact_popular_names() {
293 assert!(scan(["lodash", "express", "react"], Ecosystem::Npm).is_empty());
294 }
295
296 #[test]
297 fn does_not_flag_unrelated_names() {
298 assert!(scan(["my-company-internal-widget"], Ecosystem::Npm).is_empty());
300 }
301
302 #[test]
303 fn cargo_ecosystem_uses_crate_list() {
304 let s = scan(["tokjo", "serde"], Ecosystem::Cargo);
305 assert!(s.iter().any(|x| x.name == "tokjo" && x.nearest == "tokio"));
306 assert!(!s.iter().any(|x| x.name == "serde"));
308 }
309}