Skip to content

Commit 3636a77

Browse files
committed
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.
1 parent ade9a8e commit 3636a77

File tree

1 file changed

+73
-4
lines changed

1 file changed

+73
-4
lines changed

src/bin/monitor.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,22 @@ extern crate serde_derive;
1111

1212
mod on_call;
1313

14-
use cargo_registry::{db, util::CargoResult};
14+
use cargo_registry::{db, schema::*, util::CargoResult};
1515
use diesel::prelude::*;
1616

1717
fn main() -> CargoResult<()> {
1818
let conn = db::connect_now()?;
1919

2020
check_stalled_background_jobs(&conn)?;
21+
check_spam_attack(&conn)?;
2122
Ok(())
2223
}
2324

2425
fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> {
2526
use cargo_registry::schema::background_jobs::dsl::*;
2627
use diesel::dsl::*;
2728

28-
const BACKGROUND_JOB_KEY: &str = "background_jobs";
29+
const EVENT_KEY: &str = "background_jobs";
2930

3031
println!("Checking for stalled background jobs");
3132

@@ -40,15 +41,15 @@ fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> {
4041

4142
let event = if stalled_job_count > 0 {
4243
on_call::Event::Trigger {
43-
incident_key: Some(BACKGROUND_JOB_KEY.into()),
44+
incident_key: Some(EVENT_KEY.into()),
4445
description: format!(
4546
"{} jobs have been in the queue for more than {} minutes",
4647
stalled_job_count, max_job_time
4748
),
4849
}
4950
} else {
5051
on_call::Event::Resolve {
51-
incident_key: BACKGROUND_JOB_KEY.into(),
52+
incident_key: EVENT_KEY.into(),
5253
description: Some("No stalled background jobs".into()),
5354
}
5455
};
@@ -57,6 +58,74 @@ fn check_stalled_background_jobs(conn: &PgConnection) -> CargoResult<()> {
5758
Ok(())
5859
}
5960

61+
fn check_spam_attack(conn: &PgConnection) -> CargoResult<()> {
62+
use cargo_registry::models::krate::canon_crate_name;
63+
use diesel::dsl::*;
64+
use diesel::sql_types::Bool;
65+
66+
const EVENT_KEY: &str = "spam_attack";
67+
68+
println!("Checking for crates indicating someone is spamming us");
69+
70+
let bad_crate_names = dotenv::var("SPAM_CRATE_NAMES");
71+
let bad_crate_names = bad_crate_names
72+
.as_ref()
73+
.map(|s| s.split(",").collect())
74+
.unwrap_or(Vec::new());
75+
let bad_author_patterns = dotenv::var("SPAM_AUTHOR_PATTERNS");
76+
let bad_author_patterns = bad_author_patterns
77+
.as_ref()
78+
.map(|s| s.split(",").collect())
79+
.unwrap_or(Vec::new());
80+
81+
let mut event_description = None;
82+
83+
let bad_crate = crates::table
84+
.filter(canon_crate_name(crates::name).eq(any(bad_crate_names)))
85+
.select(crates::name)
86+
.first::<String>(conn)
87+
.optional()?;
88+
89+
if let Some(bad_crate) = bad_crate {
90+
event_description = Some(
91+
format!("Crate named {} published", bad_crate)
92+
);
93+
}
94+
95+
let mut query = version_authors::table
96+
.select(version_authors::name)
97+
.filter(false.into_sql::<Bool>()) // Never return anything if we have no patterns
98+
.into_boxed();
99+
for author_pattern in bad_author_patterns {
100+
query = query.or_filter(version_authors::name.like(author_pattern));
101+
}
102+
let bad_author = query.first::<String>(conn).optional()?;
103+
104+
if let Some(bad_author) = bad_author {
105+
event_description = Some(
106+
format!("Crate with author {} published", bad_author)
107+
);
108+
}
109+
110+
let event = if let Some(event_description) = event_description {
111+
on_call::Event::Trigger {
112+
incident_key: Some(EVENT_KEY.into()),
113+
description: format!(
114+
"{}, possible spam attack underway",
115+
event_description,
116+
),
117+
}
118+
} else {
119+
on_call::Event::Resolve {
120+
incident_key: EVENT_KEY.into(),
121+
description: Some("No spam crates detected".into()),
122+
}
123+
};
124+
125+
log_and_trigger_event(event)?;
126+
Ok(())
127+
}
128+
60129
fn log_and_trigger_event(event: on_call::Event) -> CargoResult<()> {
61130
match event {
62131
on_call::Event::Trigger {

0 commit comments

Comments
 (0)