|
| 1 | +use std::sync::Arc; |
| 2 | + |
| 3 | +use diesel::PgConnection; |
| 4 | +use typomania::Package; |
| 5 | + |
| 6 | +use crate::{ |
| 7 | + worker::{ |
| 8 | + swirl::BackgroundJob, |
| 9 | + typosquat::{Cache, Crate}, |
| 10 | + Environment, |
| 11 | + }, |
| 12 | + Emails, |
| 13 | +}; |
| 14 | + |
| 15 | +/// A job to check the name of a newly published crate against the most popular crates to see if |
| 16 | +/// the new crate might be typosquatting an existing, popular crate. |
| 17 | +#[derive(Serialize, Deserialize, Debug)] |
| 18 | +pub struct CheckTyposquat { |
| 19 | + name: String, |
| 20 | +} |
| 21 | + |
| 22 | +impl CheckTyposquat { |
| 23 | + pub fn new(name: &str) -> Self { |
| 24 | + Self { name: name.into() } |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +impl BackgroundJob for CheckTyposquat { |
| 29 | + const JOB_NAME: &'static str = "check_typosquat"; |
| 30 | + |
| 31 | + type Context = Arc<Environment>; |
| 32 | + |
| 33 | + #[instrument(skip(env), err)] |
| 34 | + fn run(&self, env: &Self::Context) -> anyhow::Result<()> { |
| 35 | + let mut conn = env.connection_pool.get()?; |
| 36 | + let cache = env.typosquat_cache(&mut conn)?; |
| 37 | + check(&env.emails, cache, &mut conn, &self.name) |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +fn check( |
| 42 | + emails: &Emails, |
| 43 | + cache: &Cache, |
| 44 | + conn: &mut PgConnection, |
| 45 | + name: &str, |
| 46 | +) -> anyhow::Result<()> { |
| 47 | + if let Some(harness) = cache.get_harness() { |
| 48 | + info!(name, "Checking new crate for potential typosquatting"); |
| 49 | + |
| 50 | + let krate: Box<dyn Package> = Box::new(Crate::from_name(conn, name)?); |
| 51 | + let squats = harness.check_package(name, krate)?; |
| 52 | + if !squats.is_empty() { |
| 53 | + // Well, well, well. For now, the only action we'll take is to e-mail people who |
| 54 | + // hopefully care to check into things more closely. |
| 55 | + info!(?squats, "Found potential typosquatting"); |
| 56 | + |
| 57 | + for email in cache.iter_emails() { |
| 58 | + if let Err(e) = emails.send_possible_typosquat_notification(email, name, &squats) { |
| 59 | + error!(?e, ?email, "Failed to send possible typosquat notification"); |
| 60 | + } |
| 61 | + } |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + Ok(()) |
| 66 | +} |
| 67 | + |
| 68 | +#[cfg(test)] |
| 69 | +mod tests { |
| 70 | + use crate::{test_util::pg_connection, worker::typosquat::test_util::Faker}; |
| 71 | + |
| 72 | + use super::*; |
| 73 | + |
| 74 | + #[test] |
| 75 | + fn integration() -> anyhow::Result<()> { |
| 76 | + let emails = Emails::new_in_memory(); |
| 77 | + let mut faker = Faker::new(pg_connection()); |
| 78 | + |
| 79 | + // Set up a user and a popular crate to match against. |
| 80 | + let user = faker.user("a")?; |
| 81 | + faker.crate_and_version("my-crate", "It's awesome", &user, 100)?; |
| 82 | + |
| 83 | + // Prime the cache so it only includes the crate we just created. |
| 84 | + let cache = Cache::new(vec!["admin@example.com".to_string()], faker.borrow_conn())?; |
| 85 | + |
| 86 | + // Now we'll create new crates: one problematic, one not so. |
| 87 | + let other_user = faker.user("b")?; |
| 88 | + let (angel, _version) = faker.crate_and_version( |
| 89 | + "innocent-crate", |
| 90 | + "I'm just a simple, innocent crate", |
| 91 | + &other_user, |
| 92 | + 0, |
| 93 | + )?; |
| 94 | + let (demon, _version) = faker.crate_and_version( |
| 95 | + "mycrate", |
| 96 | + "I'm even more innocent, obviously", |
| 97 | + &other_user, |
| 98 | + 0, |
| 99 | + )?; |
| 100 | + |
| 101 | + // OK, we're done faking stuff. |
| 102 | + let mut conn = faker.into_conn(); |
| 103 | + |
| 104 | + // Run the check with a crate that shouldn't cause problems. |
| 105 | + check(&emails, &cache, &mut conn, &angel.name)?; |
| 106 | + assert!(emails.mails_in_memory().unwrap().is_empty()); |
| 107 | + |
| 108 | + // Now run the check with a less innocent crate. |
| 109 | + check(&emails, &cache, &mut conn, &demon.name)?; |
| 110 | + let sent_mail = emails.mails_in_memory().unwrap(); |
| 111 | + assert!(!sent_mail.is_empty()); |
| 112 | + let sent = sent_mail.into_iter().next().unwrap(); |
| 113 | + assert_eq!(&sent.to, "admin@example.com"); |
| 114 | + |
| 115 | + Ok(()) |
| 116 | + } |
| 117 | +} |
0 commit comments