Skip to content

Commit 26adc56

Browse files
authored
Merge pull request #7571 from LawnGnome/typosquat-suffixes
typosquat: add suffix checks
2 parents 886b7d6 + b6ce52d commit 26adc56

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

src/typosquat/cache.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use typomania::{
77
Harness,
88
};
99

10-
use super::{config, database::TopCrates};
10+
use super::{checks::Suffixes, config, database::TopCrates};
1111

1212
static NOTIFICATION_EMAILS_ENV: &str = "TYPOSQUAT_NOTIFICATION_EMAILS";
1313

@@ -72,6 +72,10 @@ impl Cache {
7272
.with_check(Typos::new(config::TYPOS.iter().map(|(c, typos)| {
7373
(*c, typos.iter().map(|ss| ss.to_string()).collect())
7474
})))
75+
.with_check(Suffixes::new(
76+
config::SUFFIX_SEPARATORS.iter(),
77+
config::SUFFIXES.iter(),
78+
))
7579
.build(top),
7680
),
7781
})

src/typosquat/checks.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use typomania::{
2+
checks::{Check, Squat},
3+
Corpus, Package,
4+
};
5+
6+
/// A typomania check that checks if commonly used suffixes have been added or removed.
7+
pub struct Suffixes {
8+
separators: Vec<String>,
9+
suffixes: Vec<String>,
10+
}
11+
12+
impl Suffixes {
13+
pub fn new<Sep, Suf>(separators: Sep, suffixes: Suf) -> Self
14+
where
15+
Sep: Iterator,
16+
Sep::Item: ToString,
17+
Suf: Iterator,
18+
Suf::Item: ToString,
19+
{
20+
Self {
21+
separators: separators.map(|s| s.to_string()).collect(),
22+
suffixes: suffixes.map(|s| s.to_string()).collect(),
23+
}
24+
}
25+
}
26+
27+
impl Check for Suffixes {
28+
fn check(
29+
&self,
30+
corpus: &dyn Corpus,
31+
name: &str,
32+
package: &dyn Package,
33+
) -> typomania::Result<Vec<Squat>> {
34+
let mut squats = Vec::new();
35+
36+
for separator in self.separators.iter() {
37+
for suffix in self.suffixes.iter() {
38+
let combo = format!("{separator}{suffix}");
39+
40+
// If the package being examined ends in this separator and suffix combo, then we
41+
// should see if it exists in the popular crate corpus.
42+
if let Some(stem) = name.strip_suffix(&combo) {
43+
if corpus.possible_squat(stem, name, package)? {
44+
squats.push(Squat::Custom {
45+
message: format!("adds the {combo} suffix"),
46+
package: stem.to_string(),
47+
})
48+
}
49+
}
50+
51+
// Alternatively, let's see if adding the separator and suffix combo to the package
52+
// results in something popular; eg somebody trying to squat `foo` with `foo-rs`.
53+
let suffixed = format!("{name}{combo}");
54+
if corpus.possible_squat(&suffixed, name, package)? {
55+
squats.push(Squat::Custom {
56+
message: format!("removes the {combo} suffix"),
57+
package: suffixed,
58+
});
59+
}
60+
}
61+
}
62+
63+
Ok(squats)
64+
}
65+
}
66+
67+
#[cfg(test)]
68+
mod tests {
69+
use std::collections::{HashMap, HashSet};
70+
71+
use googletest::prelude::*;
72+
use typomania::{AuthorSet, Harness};
73+
74+
use super::*;
75+
76+
#[test]
77+
fn test_suffixes() -> anyhow::Result<()> {
78+
let popular = TestCorpus::default()
79+
.with_package(TestPackage::new("foo", "foo", ["Alice", "Bob"]))
80+
.with_package(TestPackage::new("bar-rs", "Rust bar", ["Charlie"]))
81+
.with_package(TestPackage::new("quux_sys", "libquux", ["Alice"]));
82+
83+
let harness = Harness::empty_builder()
84+
.with_check(Suffixes::new(
85+
["-", "_"].iter(),
86+
["core", "rs", "sys"].iter(),
87+
))
88+
.build(popular);
89+
90+
// Try some packages that shouldn't be squatting anything.
91+
for package in [
92+
TestPackage::new("bar", "shared author", ["Charlie"]),
93+
TestPackage::new("baz", "unrelated package", ["Bob"]),
94+
TestPackage::new("foo-rs", "shared author", ["Alice"]),
95+
]
96+
.into_iter()
97+
{
98+
let name = package.name.clone();
99+
let squats = harness.check_package(&name, Box::new(package))?;
100+
assert_that!(squats, empty());
101+
}
102+
103+
// Now try some packages that should be.
104+
for package in [
105+
TestPackage::new("foo-rs", "no shared author", ["Charlie"]),
106+
TestPackage::new("quux", "libquux", ["Charlie"]),
107+
TestPackage::new("quux_sys_rs", "libquux... for Rust?", ["Charlie"]),
108+
]
109+
.into_iter()
110+
{
111+
let name = package.name.clone();
112+
let squats = harness.check_package(&name, Box::new(package))?;
113+
assert_that!(squats, not(empty()));
114+
}
115+
116+
Ok(())
117+
}
118+
119+
struct TestPackage {
120+
name: String,
121+
description: String,
122+
authors: HashSet<String>,
123+
}
124+
125+
impl TestPackage {
126+
fn new(name: &str, description: &str, authors: impl AsRef<[&'static str]>) -> Self {
127+
Self {
128+
name: name.to_string(),
129+
description: description.to_string(),
130+
authors: authors.as_ref().iter().map(|a| a.to_string()).collect(),
131+
}
132+
}
133+
}
134+
135+
impl Package for TestPackage {
136+
fn authors(&self) -> &dyn AuthorSet {
137+
self
138+
}
139+
140+
fn description(&self) -> Option<&str> {
141+
Some(&self.description)
142+
}
143+
144+
fn shared_authors(&self, other: &dyn AuthorSet) -> bool {
145+
self.authors.iter().any(|author| other.contains(author))
146+
}
147+
}
148+
149+
impl AuthorSet for TestPackage {
150+
fn contains(&self, author: &str) -> bool {
151+
self.authors.contains(author)
152+
}
153+
}
154+
155+
#[derive(Default)]
156+
struct TestCorpus(HashMap<String, TestPackage>);
157+
158+
impl TestCorpus {
159+
fn with_package(mut self, package: TestPackage) -> Self {
160+
self.0.insert(package.name.clone(), package);
161+
self
162+
}
163+
}
164+
165+
impl Corpus for TestCorpus {
166+
fn contains_name(&self, name: &str) -> typomania::Result<bool> {
167+
Ok(self.0.contains_key(name))
168+
}
169+
170+
fn get(&self, name: &str) -> typomania::Result<Option<&dyn Package>> {
171+
Ok(self.0.get(name).map(|tp| tp as &dyn Package))
172+
}
173+
}
174+
}

src/typosquat/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
pub(super) static CRATE_NAME_ALPHABET: &str =
66
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_";
77

8+
/// Commonly used separators when building crate names.
9+
pub(super) static SUFFIX_SEPARATORS: &[&str] = &["-", "_"];
10+
11+
/// Commonly used suffixes when building crate names.
12+
pub(super) static SUFFIXES: &[&str] = &["api", "cli", "core", "lib", "rs", "rust", "sys"];
13+
814
/// The number of crates to consider in the "top crates" corpus.
915
pub(super) static TOP_CRATES: i64 = 3000;
1016

src/typosquat/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod cache;
2+
mod checks;
23
mod config;
34
mod database;
45

0 commit comments

Comments
 (0)