Re-work the code to have one central implementation of scanners

This commit is contained in:
2025-05-18 17:29:04 +02:00
parent 816a9f1aaa
commit 9e4496de19
5 changed files with 141 additions and 99 deletions

View File

@ -7,7 +7,7 @@ use rocket::futures::channel::mpsc as rocket_mpsc;
use rocket::futures::StreamExt; use rocket::futures::StreamExt;
use rocket::tokio; use rocket::tokio;
use snow_scanner_worker::detection::validate_ip; use snow_scanner_worker::detection::validate_ip;
use snow_scanner_worker::scanners::Scanners; use snow_scanner_worker::scanners::ScannerNode;
use crate::Scanner; use crate::Scanner;
@ -58,11 +58,11 @@ impl EventBus {
return; return;
} }
let name = Name::from_str(name.as_str()).unwrap(); let name = Name::from_str(name.as_str()).unwrap();
let scanner: Result<Scanners, String> = name.clone().try_into(); let scanner: Result<ScannerNode, String> = name.clone().try_into();
match scanner { match scanner {
Ok(scanner_type) => { Ok(scanner_type) => {
match Scanner::find_or_new(ip, scanner_type.to_owned(), Some(name), db) match Scanner::find_or_new(ip, scanner_type.info.to_owned(), Some(name), db)
.await .await
{ {
Ok(scanner) => { Ok(scanner) => {

View File

@ -33,11 +33,16 @@ use rocket_ws::WebSocket;
use server::Server; use server::Server;
use weighted_rs::Weight; use weighted_rs::Weight;
use snow_scanner_worker::detection::{get_dns_client, get_dns_server_config, validate_ip};
use snow_scanner_worker::modules::{Network, WorkerMessages};
use snow_scanner_worker::scanners::ScannerMethods; use snow_scanner_worker::scanners::ScannerMethods;
use snow_scanner_worker::scanners::Scanners;
use snow_scanner_worker::utils::get_dns_rr; use snow_scanner_worker::utils::get_dns_rr;
use snow_scanner_worker::{
detection::{get_dns_client, get_dns_server_config, validate_ip},
scanners::STRETCHOID,
};
use snow_scanner_worker::{
modules::{Network, WorkerMessages},
scanners::{ScannerData, ScannerNode},
};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::{ use std::{
@ -123,7 +128,7 @@ impl FromFormField<'_> for SafeIpAddr {
async fn handle_ip( async fn handle_ip(
query_address: IpAddr, query_address: IpAddr,
) -> Result<(IpAddr, Option<Scanners>, ResolvedResult), String> { ) -> Result<(IpAddr, Option<ScannerNode>, ResolvedResult), String> {
let ptr_result: Result<ResolvedResult, String> = std::thread::spawn(move || { let ptr_result: Result<ResolvedResult, String> = std::thread::spawn(move || {
let mut rr_dns_servers = get_dns_rr(); let mut rr_dns_servers = get_dns_rr();
let client = get_dns_client(&get_dns_server_config(&rr_dns_servers.next().unwrap())); let client = get_dns_client(&get_dns_server_config(&rr_dns_servers.next().unwrap()));
@ -139,7 +144,7 @@ async fn handle_ip(
match ptr_result { match ptr_result {
Ok(result) => { Ok(result) => {
let scanner: Result<Scanners, String> = result.query.clone().try_into(); let scanner: Result<ScannerNode, String> = result.query.clone().try_into();
match scanner { match scanner {
Ok(scanner_type) => { Ok(scanner_type) => {
@ -263,17 +268,17 @@ pub struct ReportParams {
ip: SafeIpAddr, ip: SafeIpAddr,
} }
fn reply_contents_for_scanner_found(scanner: Scanner) -> HtmlContents { fn reply_contents_for_scanner_found(scanner: Scanner, scanner_type: ScannerData) -> HtmlContents {
HtmlContents(match scanner.last_checked_at { HtmlContents(match scanner.last_checked_at {
Some(date) => format!( Some(date) => format!(
"Reported a {}! <b>{}</b> known as {} since {date}.", "Reported a {}! <b>{}</b> known as {} since {date}.",
scanner.scanner_name.funny_name(), scanner_type.funny_name,
scanner.ip, scanner.ip,
scanner.ip_ptr.unwrap_or("".to_string()) scanner.ip_ptr.unwrap_or("".to_string())
), ),
None => format!( None => format!(
"Reported a {}! <b>{}</b> known as {}.", "Reported a {}! <b>{}</b> known as {}.",
scanner.scanner_name.funny_name(), scanner_type.funny_name,
scanner.ip, scanner.ip,
scanner.ip_ptr.unwrap_or("".to_string()) scanner.ip_ptr.unwrap_or("".to_string())
), ),
@ -286,13 +291,16 @@ async fn handle_report(mut db: DbConn, form: Form<ReportParams>) -> MultiReply {
Ok((query_address, scanner_type, result)) => match scanner_type { Ok((query_address, scanner_type, result)) => match scanner_type {
Some(scanner_type) => match Scanner::find_or_new( Some(scanner_type) => match Scanner::find_or_new(
query_address, query_address,
scanner_type, scanner_type.info,
result.result.clone(), result.result.clone(),
&mut db, &mut db,
) )
.await .await
{ {
Ok(scanner) => MultiReply::Content(reply_contents_for_scanner_found(scanner)), Ok(scanner) => MultiReply::Content(reply_contents_for_scanner_found(
scanner,
scanner_type.info,
)),
Err(err) => MultiReply::Error(ServerError(format!( Err(err) => MultiReply::Error(ServerError(format!(
"The IP {} resolved as {} could not be saved, server error: {err}.", "The IP {} resolved as {} could not be saved, server error: {err}.",
form.ip.addr, form.ip.addr,
@ -365,7 +373,7 @@ async fn handle_get_collection(
#[get("/scanners/<scanner_name>")] #[get("/scanners/<scanner_name>")]
async fn handle_list_scanners( async fn handle_list_scanners(
mut db: DbConn, mut db: DbConn,
scanner_name: Scanners, scanner_name: ScannerNode,
app_configs: &State<AppConfigs>, app_configs: &State<AppConfigs>,
) -> MultiReply { ) -> MultiReply {
let static_data_dir: String = app_configs.static_data_dir.clone(); let static_data_dir: String = app_configs.static_data_dir.clone();
@ -385,7 +393,7 @@ async fn handle_list_scanners(
}; };
} }
let scanners_list = match Scanner::list_names(scanner_name, &mut db).await { let scanners_list = match Scanner::list_names(scanner_name.info, &mut db).await {
Ok(data) => Ok(data), Ok(data) => Ok(data),
Err(err) => Err(err), Err(err) => Err(err),
}; };
@ -511,7 +519,7 @@ async fn report_counts<'a>(rocket: Rocket<rocket::Build>) -> Rocket<rocket::Buil
span_error!("failed to connect to MySQL database" => error!("{e}")); span_error!("failed to connect to MySQL database" => error!("{e}"));
panic!("aborting launch"); panic!("aborting launch");
}); });
match Scanner::list_names(Scanners::Stretchoid, &mut DbConnection(conn)).await { match Scanner::list_names(STRETCHOID, &mut DbConnection(conn)).await {
Ok(d) => info!("Found {} Stretchoid scanners", d.len()), Ok(d) => info!("Found {} Stretchoid scanners", d.len()),
Err(err) => error!("Unable to fetch Stretchoid scanners: {err}"), Err(err) => error!("Unable to fetch Stretchoid scanners: {err}"),
} }

View File

@ -1,9 +1,10 @@
use std::net::IpAddr; use std::net::IpAddr;
use crate::{DbConn, Scanners}; use crate::DbConn;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use hickory_resolver::Name; use hickory_resolver::Name;
use rocket_db_pools::diesel::{dsl::insert_into, prelude::*, result::Error as DieselError}; use rocket_db_pools::diesel::{dsl::insert_into, prelude::*, result::Error as DieselError};
use snow_scanner_worker::scanners::ScannerData;
use crate::schema::scan_tasks::dsl::scan_tasks; use crate::schema::scan_tasks::dsl::scan_tasks;
use crate::schema::scanners::dsl::scanners; use crate::schema::scanners::dsl::scanners;
@ -14,7 +15,7 @@ use crate::schema::scanners::dsl::scanners;
pub struct Scanner { pub struct Scanner {
pub ip: String, pub ip: String,
pub ip_type: u8, pub ip_type: u8,
pub scanner_name: Scanners, pub scanner_name: String,
pub ip_ptr: Option<String>, pub ip_ptr: Option<String>,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>, pub updated_at: Option<NaiveDateTime>,
@ -25,7 +26,7 @@ pub struct Scanner {
impl Scanner { impl Scanner {
pub async fn find_or_new( pub async fn find_or_new(
query_address: IpAddr, query_address: IpAddr,
scanner_name: Scanners, scanner_data: ScannerData<'static>,
ptr: Option<Name>, ptr: Option<Name>,
conn: &mut DbConn, conn: &mut DbConn,
) -> Result<Scanner, DieselError> { ) -> Result<Scanner, DieselError> {
@ -45,7 +46,7 @@ impl Scanner {
Scanner { Scanner {
ip: query_address.to_string(), ip: query_address.to_string(),
ip_type: ip_type, ip_type: ip_type,
scanner_name: scanner_name.clone(), scanner_name: scanner_data.value.to_string(),
ip_ptr: match ptr { ip_ptr: match ptr {
Some(ptr) => Some(ptr.to_string()), Some(ptr) => Some(ptr.to_string()),
None => None, None => None,
@ -79,15 +80,16 @@ impl Scanner {
} }
pub async fn list_names( pub async fn list_names(
scanner_name: Scanners, scanner_data: ScannerData<'static>,
conn: &mut DbConn, conn: &mut DbConn,
) -> Result<Vec<String>, DieselError> { ) -> Result<Vec<String>, DieselError> {
use crate::schema::scanners; use crate::schema::scanners;
use crate::schema::scanners::ip; use crate::schema::scanners::ip;
use crate::schema::scanners::scanner_name;
scanners scanners
.select(ip) .select(ip)
.filter(scanners::scanner_name.eq(scanner_name.to_string())) .filter(scanner_name.eq(scanner_data.value))
.order((scanners::ip_type.desc(), scanners::created_at.desc())) .order((scanners::ip_type.desc(), scanners::created_at.desc()))
.load::<String>(conn) .load::<String>(conn)
.await .await

View File

@ -12,14 +12,72 @@ use std::str::FromStr;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::io::Write; use std::io::Write;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScannerData<'a> {
pub static_file_name: Option<&'a str>,
pub funny_name: &'a str,
pub display_name: &'a str,
pub value: &'a str,
pub dns_prefix: Option<&'a str>,
}
pub const STRETCHOID: ScannerData = ScannerData {
static_file_name: None,
funny_name: "stretchoid agent",
display_name: "stretchoid",
value: "stretchoid",
dns_prefix: Some("stretchoid.com."),
};
pub const BINARYEDGE: ScannerData = ScannerData {
static_file_name: None,
funny_name: "binaryedge ninja",
display_name: "binaryedge",
value: "binaryedge",
dns_prefix: Some("binaryedge.ninja."),
};
pub const SHADOWSERVER: ScannerData = ScannerData {
static_file_name: None,
funny_name: "cloudy shadowserver",
display_name: "shadowserver",
value: "shadowserver",
dns_prefix: Some("shadowserver.org."),
};
pub fn get_scanners() -> Vec<ScannerData<'static>> {
vec![
STRETCHOID,
BINARYEDGE,
SHADOWSERVER,
ScannerData {
static_file_name: Some("censys.txt"),
funny_name: "Censys node",
display_name: "censys",
value: "censys",
dns_prefix: None,
},
ScannerData {
static_file_name: Some("internet-measurement.com.txt"),
funny_name: "internet measurement probe",
display_name: "internet-measurement.com",
value: "internet-measurement.com",
dns_prefix: None,
},
ScannerData {
static_file_name: Some("anssi.txt"),
funny_name: "French ANSSI probe",
display_name: "anssi",
value: "anssi",
dns_prefix: None,
},
]
}
pub type ScannerNode = ScannersWrapper<ScannerData<'static>>;
#[derive(Debug, Clone, Copy, FromSqlRow, PartialEq)] #[derive(Debug, Clone, Copy, FromSqlRow, PartialEq)]
pub enum Scanners { pub struct ScannersWrapper<ScannerData> {
Stretchoid, pub info: ScannerData,
Binaryedge,
Shadowserver,
Censys,
InternetMeasurement,
Anssi,
} }
pub trait ScannerMethods { pub trait ScannerMethods {
@ -28,33 +86,21 @@ pub trait ScannerMethods {
fn funny_name(self: &Self) -> &str; fn funny_name(self: &Self) -> &str;
} }
impl ScannerMethods for Scanners { impl ScannerMethods for ScannerNode {
fn is_static(self: &Self) -> bool { fn is_static(self: &Self) -> bool {
self.static_file_name().is_some() self.static_file_name().is_some()
} }
fn static_file_name(self: &Self) -> Option<&str> { fn static_file_name(self: &Self) -> Option<&str> {
match self { self.info.static_file_name
Self::Censys => Some("censys.txt"),
Self::InternetMeasurement => Some("internet-measurement.com.txt"),
Self::Anssi => Some("anssi.txt"),
_ => None,
}
} }
fn funny_name(self: &Self) -> &str { fn funny_name(self: &Self) -> &str {
match self { self.info.funny_name
Self::Stretchoid => "stretchoid agent",
Self::Binaryedge => "binaryedge ninja",
Self::Censys => "Censys node",
Self::InternetMeasurement => "internet measurement probe",
Self::Shadowserver => "cloudy shadowserver",
_ => (*self).into(),
}
} }
} }
impl FromParam<'_> for Scanners { impl FromParam<'_> for ScannerNode {
type Error = String; type Error = String;
fn from_param(param: &'_ str) -> Result<Self, Self::Error> { fn from_param(param: &'_ str) -> Result<Self, Self::Error> {
@ -62,7 +108,7 @@ impl FromParam<'_> for Scanners {
} }
} }
impl<'de> Deserialize<'de> for Scanners { impl<'de> Deserialize<'de> for ScannerNode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@ -76,27 +122,20 @@ impl<'de> Deserialize<'de> for Scanners {
} }
} }
impl ToString for Scanners { impl ToString for ScannerNode {
fn to_string(&self) -> String { fn to_string(&self) -> String {
let res: &str = (*self).into(); let res: &str = (*self).into();
res.to_string() res.to_string()
} }
} }
impl Into<&str> for Scanners { impl Into<&str> for ScannerNode {
fn into(self) -> &'static str { fn into(self) -> &'static str {
match self { self.info.display_name
Self::Stretchoid => "stretchoid",
Self::Binaryedge => "binaryedge",
Self::Censys => "censys",
Self::InternetMeasurement => "internet-measurement.com",
Self::Shadowserver => "shadowserver",
Self::Anssi => "anssi",
}
} }
} }
impl serialize::ToSql<Text, Mysql> for Scanners { impl serialize::ToSql<Text, Mysql> for ScannerNode {
fn to_sql(&self, out: &mut serialize::Output<Mysql>) -> serialize::Result { fn to_sql(&self, out: &mut serialize::Output<Mysql>) -> serialize::Result {
let res: &str = (*self).into(); let res: &str = (*self).into();
out.write_all(res.as_bytes())?; out.write_all(res.as_bytes())?;
@ -105,11 +144,11 @@ impl serialize::ToSql<Text, Mysql> for Scanners {
} }
} }
impl deserialize::FromSql<Text, Mysql> for Scanners { impl deserialize::FromSql<Text, Mysql> for ScannerNode {
fn from_sql(bytes: MysqlValue) -> deserialize::Result<Self> { fn from_sql(bytes: MysqlValue) -> deserialize::Result<Self> {
let value = <String as deserialize::FromSql<Text, Mysql>>::from_sql(bytes)?; let value = <String as deserialize::FromSql<Text, Mysql>>::from_sql(bytes)?;
let value = &value as &str; let value = &value as &str;
let value: Result<Scanners, String> = value.try_into(); let value: Result<ScannerNode, String> = value.try_into();
match value { match value {
Ok(d) => Ok(d), Ok(d) => Ok(d),
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
@ -118,50 +157,38 @@ impl deserialize::FromSql<Text, Mysql> for Scanners {
} }
// Used for FromSql & FromParam & Deserialize // Used for FromSql & FromParam & Deserialize
impl TryInto<Scanners> for &str { impl TryInto<ScannerNode> for &str {
type Error = String; type Error = String;
fn try_into(self) -> Result<Scanners, Self::Error> { fn try_into(self) -> Result<ScannerNode, Self::Error> {
match self.replace(".txt", "").as_str() { let value: String = self.replace(".txt", "").as_str().to_string();
"stretchoid" => Ok(Scanners::Stretchoid), match get_scanners()
"binaryedge" => Ok(Scanners::Binaryedge), .iter()
"internet-measurement.com" => Ok(Scanners::InternetMeasurement), .find(|scanner| scanner.value.eq(&value))
"shadowserver" => Ok(Scanners::Shadowserver), {
"censys" => Ok(Scanners::Censys), Some(scanner) => Ok(ScannersWrapper { info: *scanner }),
"anssi" => Ok(Scanners::Anssi), None => Err(format!("Invalid value: {value}")),
value => Err(format!("Invalid value: {value}")),
} }
} }
} }
// Used by the DNS logic // Used by the DNS logic
impl TryInto<Scanners> for Name { impl TryInto<ScannerNode> for Name {
type Error = String; type Error = String;
fn try_into(self) -> Result<Scanners, Self::Error> { fn try_into(self) -> Result<ScannerNode, Self::Error> {
match self { let short_name = self.trim_to(2);
ref name match get_scanners()
if name .iter()
.trim_to(2) .filter(|scanner| scanner.dns_prefix.is_some())
.eq_case(&Name::from_str("binaryedge.ninja.").expect("Should parse")) => .find(|scanner| {
{ short_name.eq_case(
Ok(Scanners::Binaryedge) &Name::from_str(scanner.dns_prefix.expect("Should have a DNS prefix"))
} .expect("Should parse"),
ref name )
if name }) {
.trim_to(2) Some(scanner) => Ok(ScannersWrapper { info: *scanner }),
.eq_case(&Name::from_str("stretchoid.com.").expect("Should parse")) => None => Err(format!("Invalid hostname: {self}")),
{
Ok(Scanners::Stretchoid)
}
ref name
if name
.trim_to(2)
.eq_case(&Name::from_str("shadowserver.org.").expect("Should parse")) =>
{
Ok(Scanners::Shadowserver)
}
ref name => Err(format!("Invalid hostname: {name}")),
} }
} }
} }
@ -175,8 +202,8 @@ mod test {
fn test_detect_scanner_from_name() { fn test_detect_scanner_from_name() {
let ptr = Name::from_str("scan-47e.shadowserver.org.").unwrap(); let ptr = Name::from_str("scan-47e.shadowserver.org.").unwrap();
let res: Result<Scanners, String> = ptr.try_into(); let res: Result<ScannerNode, String> = ptr.try_into();
assert_eq!(res.unwrap(), Scanners::Shadowserver); assert_eq!(res.unwrap().info, SHADOWSERVER);
} }
} }

View File

@ -4,7 +4,7 @@ use chrono::{Duration, NaiveDateTime, Utc};
use cidr::IpCidr; use cidr::IpCidr;
use dns_ptr_resolver::{get_ptr, ResolvedResult}; use dns_ptr_resolver::{get_ptr, ResolvedResult};
use log2::*; use log2::*;
use scanners::Scanners; use scanners::ScannerNode;
use tungstenite::stream::MaybeTlsStream; use tungstenite::stream::MaybeTlsStream;
use tungstenite::{connect, Error, Message, WebSocket}; use tungstenite::{connect, Error, Message, WebSocket};
use weighted_rs::Weight; use weighted_rs::Weight;
@ -160,7 +160,7 @@ impl Worker {
let client = get_dns_client(&get_dns_server_config(&rr_dns_servers.next().unwrap())); let client = get_dns_client(&get_dns_server_config(&rr_dns_servers.next().unwrap()));
match get_ptr(addr, client) { match get_ptr(addr, client) {
Ok(result) => { Ok(result) => {
let scanner: Result<Scanners, String> = result.query.clone().try_into(); let scanner: Result<ScannerNode, String> = result.query.clone().try_into();
match scanner { match scanner {
Ok(scanner_name) => { Ok(scanner_name) => {
@ -181,7 +181,12 @@ impl Worker {
} }
} }
fn report_detection(&mut self, scanner_name: Scanners, addr: IpAddr, result: ResolvedResult) { fn report_detection(
&mut self,
scanner_name: ScannerNode,
addr: IpAddr,
result: ResolvedResult,
) {
info!("Detected {:?} for {addr}", scanner_name); info!("Detected {:?} for {addr}", scanner_name);
let request = WorkerMessages::ScannerFoundResponse { let request = WorkerMessages::ScannerFoundResponse {
name: result.result.unwrap().to_string(), name: result.result.unwrap().to_string(),