From 3636a77224b321758b4106ed31b5b3bccef61438 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 18 Mar 2019 16:12:37 -0600 Subject: [PATCH 1/3] Add monitoring for common spam patterns We've noticed some common patterns in recent spam attacks. While our response time on these has been ok, we can look for some of these common patterns and page whoever is on-call earlier than we'd otherwise notice. The exact patterns we look for is considered sensitive information, and thus not in the repo and should not be discussed publicly. Note that I've opted to look for crates that are likely spam, rather than volume. Volume is more likely to have false positives, and is better handled by more aggressive rate limiting. This assumes that we consider a spam attack to be something we always want to page for. Since we have better coverage of someone watching discord most hours, we could alternatively have this post in a private channel, and let whoever is awake determine if it's worth paging over. If someone does get paged, it's assumed that this will get resolved either by them taking action to remove the crates, or if the crate is legitimate, by updating the config vars to remove that pattern. --- src/bin/monitor.rs | 77 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/src/bin/monitor.rs b/src/bin/monitor.rs index 5fc93f47f5c..0e92679f5ed 100644 --- a/src/bin/monitor.rs +++ b/src/bin/monitor.rs @@ -11,13 +11,14 @@ extern crate serde_derive; mod on_call; -use cargo_registry::{db, util::CargoResult}; +use cargo_registry::{db, schema::*, util::CargoResult}; use diesel::prelude::*; fn main() -> CargoResult<()> { let conn = db::connect_now()?; check_stalled_background_jobs(&conn)?; + check_spam_attack(&conn)?; Ok(()) } @@ -25,7 +26,7 @@ fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> { use cargo_registry::schema::background_jobs::dsl::*; use diesel::dsl::*; - const BACKGROUND_JOB_KEY: &str = "background_jobs"; + const EVENT_KEY: &str = "background_jobs"; println!("Checking for stalled background jobs"); @@ -40,7 +41,7 @@ fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> { let event = if stalled_job_count > 0 { on_call::Event::Trigger { - incident_key: Some(BACKGROUND_JOB_KEY.into()), + incident_key: Some(EVENT_KEY.into()), description: format!( "{} jobs have been in the queue for more than {} minutes", stalled_job_count, max_job_time @@ -48,7 +49,7 @@ fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> { } } else { on_call::Event::Resolve { - incident_key: BACKGROUND_JOB_KEY.into(), + incident_key: EVENT_KEY.into(), description: Some("No stalled background jobs".into()), } }; @@ -57,6 +58,74 @@ fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> { Ok(()) } +fn check_spam_attack(conn: &PgConnection) -> CargoResult<()> { + use cargo_registry::models::krate::canon_crate_name; + use diesel::dsl::*; + use diesel::sql_types::Bool; + + const EVENT_KEY: &str = "spam_attack"; + + println!("Checking for crates indicating someone is spamming us"); + + let bad_crate_names = dotenv::var("SPAM_CRATE_NAMES"); + let bad_crate_names = bad_crate_names + .as_ref() + .map(|s| s.split(",").collect()) + .unwrap_or(Vec::new()); + let bad_author_patterns = dotenv::var("SPAM_AUTHOR_PATTERNS"); + let bad_author_patterns = bad_author_patterns + .as_ref() + .map(|s| s.split(",").collect()) + .unwrap_or(Vec::new()); + + let mut event_description = None; + + let bad_crate = crates::table + .filter(canon_crate_name(crates::name).eq(any(bad_crate_names))) + .select(crates::name) + .first::(conn) + .optional()?; + + if let Some(bad_crate) = bad_crate { + event_description = Some( + format!("Crate named {} published", bad_crate) + ); + } + + let mut query = version_authors::table + .select(version_authors::name) + .filter(false.into_sql::()) // Never return anything if we have no patterns + .into_boxed(); + for author_pattern in bad_author_patterns { + query = query.or_filter(version_authors::name.like(author_pattern)); + } + let bad_author = query.first::(conn).optional()?; + + if let Some(bad_author) = bad_author { + event_description = Some( + format!("Crate with author {} published", bad_author) + ); + } + + let event = if let Some(event_description) = event_description { + on_call::Event::Trigger { + incident_key: Some(EVENT_KEY.into()), + description: format!( + "{}, possible spam attack underway", + event_description, + ), + } + } else { + on_call::Event::Resolve { + incident_key: EVENT_KEY.into(), + description: Some("No spam crates detected".into()), + } + }; + + log_and_trigger_event(event)?; + Ok(()) +} + fn log_and_trigger_event(event: on_call::Event) -> CargoResult<()> { match event { on_call::Event::Trigger { From e9d59aab1116d174653d6bd7170dd82a6fab175c Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Tue, 19 Mar 2019 13:35:00 -0600 Subject: [PATCH 2/3] rustfmt --- src/bin/monitor.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/bin/monitor.rs b/src/bin/monitor.rs index 0e92679f5ed..796daceb6c7 100644 --- a/src/bin/monitor.rs +++ b/src/bin/monitor.rs @@ -87,9 +87,7 @@ fn check_spam_attack(conn: &PgConnection) -> CargoResult<()> { .optional()?; if let Some(bad_crate) = bad_crate { - event_description = Some( - format!("Crate named {} published", bad_crate) - ); + event_description = Some(format!("Crate named {} published", bad_crate)); } let mut query = version_authors::table @@ -102,18 +100,13 @@ fn check_spam_attack(conn: &PgConnection) -> CargoResult<()> { let bad_author = query.first::(conn).optional()?; if let Some(bad_author) = bad_author { - event_description = Some( - format!("Crate with author {} published", bad_author) - ); + event_description = Some(format!("Crate with author {} published", bad_author)); } let event = if let Some(event_description) = event_description { on_call::Event::Trigger { incident_key: Some(EVENT_KEY.into()), - description: format!( - "{}, possible spam attack underway", - event_description, - ), + description: format!("{}, possible spam attack underway", event_description,), } } else { on_call::Event::Resolve { From fb3da011332c1012afcce43644faec9d014c39c7 Mon Sep 17 00:00:00 2001 From: Justin Geibel Date: Fri, 22 Mar 2019 20:35:49 -0400 Subject: [PATCH 3/3] Make clippy happy --- src/bin/monitor.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bin/monitor.rs b/src/bin/monitor.rs index 796daceb6c7..52d68c7be3f 100644 --- a/src/bin/monitor.rs +++ b/src/bin/monitor.rs @@ -68,15 +68,15 @@ fn check_spam_attack(conn: &PgConnection) -> CargoResult<()> { println!("Checking for crates indicating someone is spamming us"); let bad_crate_names = dotenv::var("SPAM_CRATE_NAMES"); - let bad_crate_names = bad_crate_names + let bad_crate_names: Vec<_> = bad_crate_names .as_ref() - .map(|s| s.split(",").collect()) - .unwrap_or(Vec::new()); + .map(|s| s.split(',').collect()) + .unwrap_or_default(); let bad_author_patterns = dotenv::var("SPAM_AUTHOR_PATTERNS"); - let bad_author_patterns = bad_author_patterns + let bad_author_patterns: Vec<_> = bad_author_patterns .as_ref() - .map(|s| s.split(",").collect()) - .unwrap_or(Vec::new()); + .map(|s| s.split(',').collect()) + .unwrap_or_default(); let mut event_description = None;