4 Commits

Author SHA1 Message Date
e48493cf6a Fix IP reporting and update the row
Some checks failed
Build IP lists / Build scanners list (binaryedge) (push) Failing after -2m8s
Build IP lists / Build scanners list (stretchoid) (push) Failing after -2m10s
Build IP lists / build-aws-cloudfront (push) Failing after -2m10s
2024-09-23 00:06:03 +02:00
d6757902f6 Upgrade dns-ptr-resolver 2024-09-22 23:32:10 +02:00
43e9176b49 Make collections search safer 2024-09-22 10:14:56 +02:00
bb52edc4c8 Fix path for files 2024-09-20 21:14:50 +02:00
3 changed files with 151 additions and 44 deletions

View File

@ -23,6 +23,8 @@ is-it-maintained-issue-resolution = { repository = "security" }
is-it-maintained-open-issues = { repository = "security" } is-it-maintained-open-issues = { repository = "security" }
maintenance = { status = "passively-maintained" } maintenance = { status = "passively-maintained" }
# docker pull clux/muslrust:stable
# docker run -v $PWD:/volume --rm -t clux/muslrust:stable cargo build --release
[[bin]] [[bin]]
name = "snow-scanner" name = "snow-scanner"
path = "src/main.rs" path = "src/main.rs"
@ -35,9 +37,11 @@ actix-files = "0.6.6"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.8" sha2 = "0.10.8"
hex = "0.4.3" hex = "0.4.3"
# mariadb-dev on Alpine
# "mysqlclient-src" "mysql_backend"
diesel = { version = "2.2.0", default-features = false, features = ["mysql", "chrono", "uuid", "r2d2"] } diesel = { version = "2.2.0", default-features = false, features = ["mysql", "chrono", "uuid", "r2d2"] }
dns-ptr-resolver = "1.2.0" dns-ptr-resolver = {git = "https://github.com/wdes/dns-ptr-resolver.git"}
hickory-client = { version = "0.24.1", default-features = false } hickory-resolver = { version = "0.24.1", default-features = false, features = ["tokio-runtime", "dns-over-h3", "dns-over-https", "dns-over-quic"]}
chrono = "0.4.38" chrono = "0.4.38"
uuid = { version = "1.10.0", default-features = false, features = ["v7", "serde", "std"] } uuid = { version = "1.10.0", default-features = false, features = ["v7", "serde", "std"] }
cidr = "0.2.2" cidr = "0.2.2"

View File

@ -19,9 +19,10 @@ use uuid::Uuid;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use hickory_client::client::SyncClient; use hickory_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts};
use hickory_client::rr::Name; use hickory_resolver::{Name, Resolver};
use hickory_client::tcp::TcpClientConnection; use std::net::IpAddr;
use std::time::Duration;
use diesel::serialize::IsNull; use diesel::serialize::IsNull;
use diesel::{serialize, MysqlConnection}; use diesel::{serialize, MysqlConnection};
@ -143,34 +144,58 @@ fn detect_scanner(ptr_result: &ResolvedResult) -> Result<Scanners, ()> {
async fn handle_ip(pool: web::Data<DbPool>, ip: String) -> Result<Scanner, Option<ResolvedResult>> { async fn handle_ip(pool: web::Data<DbPool>, ip: String) -> Result<Scanner, Option<ResolvedResult>> {
let query_address = ip.parse().expect("To parse"); let query_address = ip.parse().expect("To parse");
let client = get_dns_client(); let ptr_result: Result<ResolvedResult, ()> = std::thread::spawn(move || {
let ptr_result: ResolvedResult = if let Ok(res) = get_ptr(query_address, client) { let client = get_dns_client();
res let ptr_result: ResolvedResult = if let Ok(res) = get_ptr(query_address, client) {
} else { res
return Err(None); } else {
}; return Err(());
};
Ok(ptr_result)
})
.join()
.unwrap();
match detect_scanner(&ptr_result) { if ptr_result.is_err() {
return Err(None);
}
let result = ptr_result.unwrap();
match detect_scanner(&result) {
Ok(scanner_name) => { Ok(scanner_name) => {
let ip_type = if ip.contains(':') { 6 } else { 4 }; let ip_type = if ip.contains(':') { 6 } else { 4 };
let scanner = Scanner {
ip: ip,
ip_type: ip_type,
scanner_name: scanner_name.clone(),
ip_ptr: match ptr_result.result {
Some(ptr) => Some(ptr.to_string()),
None => None,
},
created_at: Utc::now().naive_utc(),
updated_at: None,
last_seen_at: None,
last_checked_at: None,
};
// use web::block to offload blocking Diesel queries without blocking server thread // use web::block to offload blocking Diesel queries without blocking server thread
web::block(move || { web::block(move || {
// note that obtaining a connection from the pool is also potentially blocking // note that obtaining a connection from the pool is also potentially blocking
let conn = &mut pool.get().unwrap(); let conn = &mut pool.get().unwrap();
let scanner_row_result = Scanner::find(ip.clone(), ip_type, conn);
let scanner_row = match scanner_row_result {
Ok(scanner_row) => scanner_row,
Err(_) => return Err(None),
};
let scanner = if let Some(mut scanners) = scanner_row {
scanners.last_seen_at = Some(Utc::now().naive_utc());
scanners.last_checked_at = Some(Utc::now().naive_utc());
scanners.updated_at = Some(Utc::now().naive_utc());
scanners
} else {
Scanner {
ip: ip,
ip_type: ip_type,
scanner_name: scanner_name.clone(),
ip_ptr: match result.result {
Some(ptr) => Some(ptr.to_string()),
None => None,
},
created_at: Utc::now().naive_utc(),
updated_at: None,
last_seen_at: None,
last_checked_at: None,
}
};
match scanner.save(conn) { match scanner.save(conn) {
Ok(scanner) => Ok(scanner), Ok(scanner) => Ok(scanner),
@ -181,7 +206,7 @@ async fn handle_ip(pool: web::Data<DbPool>, ip: String) -> Result<Scanner, Optio
.unwrap() .unwrap()
} }
Err(_) => Err(Some(ptr_result)), Err(_) => Err(Some(result)),
} }
} }
@ -257,14 +282,30 @@ pub struct ReportParams {
async fn handle_report(pool: web::Data<DbPool>, params: web::Form<ReportParams>) -> HttpResponse { async fn handle_report(pool: web::Data<DbPool>, params: web::Form<ReportParams>) -> HttpResponse {
match handle_ip(pool, params.ip.clone()).await { match handle_ip(pool, params.ip.clone()).await {
Ok(scanner) => html_contents(match scanner.scanner_name { Ok(scanner) => html_contents(match scanner.scanner_name {
Scanners::Binaryedge => format!( Scanners::Binaryedge => match scanner.last_checked_at {
"Reported an escaped ninja! <b>{}</a> known as {:?}.", Some(date) => format!(
scanner.ip, scanner.ip_ptr "Reported a binaryedge ninja! <b>{}</b> known as {} since {date}.",
), scanner.ip,
Scanners::Stretchoid => format!( scanner.ip_ptr.unwrap_or("".to_string())
"Reported a stretchoid agent! <b>{}</a> known as {:?}.", ),
scanner.ip, scanner.ip_ptr None => format!(
), "Reported a binaryedge ninja! <b>{}</b> known as {}.",
scanner.ip,
scanner.ip_ptr.unwrap_or("".to_string())
),
},
Scanners::Stretchoid => match scanner.last_checked_at {
Some(date) => format!(
"Reported a stretchoid agent! <b>{}</b> known as {} since {date}.",
scanner.ip,
scanner.ip_ptr.unwrap_or("".to_string())
),
None => format!(
"Reported a stretchoid agent! <b>{}</b> known as {}.",
scanner.ip,
scanner.ip_ptr.unwrap_or("".to_string())
),
},
_ => format!("Not supported"), _ => format!("Not supported"),
}), }),
@ -279,8 +320,33 @@ async fn handle_report(pool: web::Data<DbPool>, params: web::Form<ReportParams>)
} }
} }
struct SecurePath {
pub data: String,
}
impl<'de> Deserialize<'de> for SecurePath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <Vec<String>>::deserialize(deserializer)?;
let k: String = s[0].to_string();
// A-Z a-z 0-9
// . - _
if k.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
{
return Ok(SecurePath { data: k });
}
Err(serde::de::Error::custom(format!(
"Invalid value: {}",
k.to_string()
)))
}
}
async fn handle_get_collection( async fn handle_get_collection(
path: web::Path<(String, String)>, path: web::Path<(SecurePath, SecurePath)>,
req: HttpRequest, req: HttpRequest,
static_data_dir: actix_web::web::Data<String>, static_data_dir: actix_web::web::Data<String>,
) -> actix_web::Result<HttpResponse> { ) -> actix_web::Result<HttpResponse> {
@ -289,8 +355,9 @@ async fn handle_get_collection(
let mut path: PathBuf = PathBuf::new(); let mut path: PathBuf = PathBuf::new();
let static_data_dir: String = static_data_dir.into_inner().to_string(); let static_data_dir: String = static_data_dir.into_inner().to_string();
path.push(static_data_dir); path.push(static_data_dir);
path.push(vendor_name.to_string()); path.push("collections");
path.push(file_name.to_string()); path.push(vendor_name.data);
path.push(file_name.data);
match NamedFile::open(path) { match NamedFile::open(path) {
Ok(file) => Ok(file.into_response(&req)), Ok(file) => Ok(file.into_response(&req)),
Err(err) => Ok(HttpResponse::NotFound() Err(err) => Ok(HttpResponse::NotFound()
@ -310,6 +377,7 @@ async fn handle_list_scanners(
if scanner_name.is_static() { if scanner_name.is_static() {
let mut path: PathBuf = PathBuf::new(); let mut path: PathBuf = PathBuf::new();
path.push(static_data_dir); path.push(static_data_dir);
path.push("scanners");
path.push(scanner_name.to_string()); path.push(scanner_name.to_string());
return match NamedFile::open(path) { return match NamedFile::open(path) {
@ -414,17 +482,27 @@ fn get_connection(database_url: &str) -> DbPool {
// Refer to the `r2d2` documentation for more methods to use // Refer to the `r2d2` documentation for more methods to use
// when building a connection pool // when building a connection pool
Pool::builder() Pool::builder()
.max_size(30) .max_size(5)
.test_on_check_out(true) .test_on_check_out(true)
.build(manager) .build(manager)
.expect("Could not build connection pool") .expect("Could not build connection pool")
} }
fn get_dns_client() -> SyncClient<TcpClientConnection> { fn get_dns_client() -> Resolver {
let server = "1.1.1.1:53".parse().expect("To parse"); let server_ip = "1.1.1.1";
let dns_conn =
TcpClientConnection::with_timeout(server, std::time::Duration::new(5, 0)).unwrap(); let server = NameServerConfigGroup::from_ips_clear(
SyncClient::new(dns_conn) &[IpAddr::from_str(server_ip).unwrap()],
53, // Port 53
true,
);
let config = ResolverConfig::from_parts(None, vec![], server);
let mut options = ResolverOpts::default();
options.timeout = Duration::from_secs(5);
options.attempts = 1; // One try
Resolver::new(config, options).unwrap()
} }
fn plain_contents(data: String) -> HttpResponse { fn plain_contents(data: String) -> HttpResponse {
@ -468,13 +546,22 @@ async fn main() -> std::io::Result<()> {
"mysql://localhost".to_string() "mysql://localhost".to_string()
}; };
let pool = get_connection(db_url.as_str());
// note that obtaining a connection from the pool is also potentially blocking
let conn = &mut pool.get().unwrap();
let names = Scanner::list_names(Scanners::Stretchoid, conn);
match names {
Ok(names) => println!("Found {} Stretchoid scanners", names.len()),
Err(err) => eprintln!("Unable to get names: {}", err),
};
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
let static_data_dir: String = match env::var("STATIC_DATA_DIR") { let static_data_dir: String = match env::var("STATIC_DATA_DIR") {
Ok(val) => val, Ok(val) => val,
Err(_) => "../data/".to_string(), Err(_) => "../data/".to_string(),
}; };
let pool = get_connection(db_url.as_str());
App::new() App::new()
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))
.app_data(actix_web::web::Data::new(static_data_dir)) .app_data(actix_web::web::Data::new(static_data_dir))

View File

@ -22,6 +22,22 @@ pub struct Scanner {
} }
impl Scanner { impl Scanner {
pub fn find(
ip_address: String,
ip_type: u8,
conn: &mut MysqlConnection,
) -> Result<Option<Scanner>, DieselError> {
use crate::schema::scanners;
scanners
.select(Scanner::as_select())
.filter(scanners::ip.eq(ip_address))
.filter(scanners::ip_type.eq(ip_type))
.order((scanners::ip_type.desc(), scanners::created_at.desc()))
.first(conn)
.optional()
}
pub fn list_names( pub fn list_names(
scanner_name: Scanners, scanner_name: Scanners,
conn: &mut MysqlConnection, conn: &mut MysqlConnection,