1#![doc = include_str!("../README.md")]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![deny(missing_docs)]
6#![deny(clippy::all)]
7#![deny(clippy::pedantic)]
8
9pub mod crypto;
11
12pub mod db;
14
15pub mod http;
17
18pub mod utils;
20
21#[cfg_attr(docsrs, doc(cfg(feature = "vt")))]
23#[cfg(feature = "vt")]
24pub mod vt;
25
26#[cfg_attr(docsrs, doc(cfg(feature = "yara")))]
28#[cfg(feature = "yara")]
29pub mod yara;
30
31use crate::crypto::FileEncryption;
32use crate::db::MDBConfig;
33use malwaredb_api::ServerInfo;
34use std::collections::HashMap;
37use std::fmt::{Debug, Formatter};
38use std::io::{Cursor, Read};
39use std::net::{IpAddr, Ipv4Addr, SocketAddr};
40use std::path::PathBuf;
41#[cfg(feature = "admin")]
42use std::sync::atomic::{AtomicBool, AtomicU64};
43use std::sync::{Arc, LazyLock};
44use std::time::{Duration, SystemTime};
45
46use anyhow::{Context, Result, anyhow, bail, ensure};
47use axum_server::tls_rustls::RustlsConfig;
48use chrono::Local;
49use chrono_humanize::{Accuracy, HumanTime, Tense};
50use flate2::read::GzDecoder;
51use mdns_sd::{ServiceDaemon, ServiceInfo};
52use sha2::{Digest, Sha256};
53use tokio::net::TcpListener;
54use tracing::{trace, warn};
55
56pub const MDB_VERSION: &str = env!("CARGO_PKG_VERSION");
58
59pub static MDB_VERSION_SEMVER: LazyLock<semver::Version> =
61 LazyLock::new(|| semver::Version::parse(MDB_VERSION).unwrap());
62
63pub(crate) const DB_CLEANUP_INTERVAL: Duration = Duration::from_hours(24);
66
67pub const GZIP_MAGIC: [u8; 2] = [0x1fu8, 0x8bu8];
69
70pub const ZSTD_MAGIC: [u8; 4] = [0x28u8, 0xb5u8, 0x2fu8, 0xfdu8];
72
73pub struct StateBuilder {
75 pub port: u16,
77
78 pub directory: Option<PathBuf>,
80
81 pub max_upload: usize,
83
84 pub ip: IpAddr,
86
87 db_type: db::DatabaseType,
89
90 #[cfg(feature = "vt")]
92 vt_client: Option<malwaredb_virustotal::VirusTotalClient>,
93
94 tls_config: Option<RustlsConfig>,
96
97 mdns: bool,
99}
100
101impl StateBuilder {
102 pub async fn new(db_string: &str, pg_cert: Option<PathBuf>) -> Result<Self> {
110 let db_type = db::DatabaseType::from_string(db_string, pg_cert).await?;
111
112 Ok(Self {
113 port: 8080,
114 directory: None,
115 max_upload: 104_857_600, ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
117 db_type,
118 #[cfg(feature = "vt")]
119 vt_client: None,
120 tls_config: None,
121 mdns: false,
122 })
123 }
124
125 #[must_use]
128 pub fn port(mut self, port: u16) -> Self {
129 self.port = port;
130 self
131 }
132
133 #[must_use]
137 pub fn directory(mut self, directory: PathBuf) -> Self {
138 self.directory = Some(directory);
139 self
140 }
141
142 #[must_use]
145 pub fn max_upload(mut self, max_upload: usize) -> Self {
146 self.max_upload = max_upload;
147 self
148 }
149
150 #[must_use]
153 pub fn ip(mut self, ip: IpAddr) -> Self {
154 self.ip = ip;
155 self
156 }
157
158 #[must_use]
160 #[cfg(feature = "vt")]
161 #[cfg_attr(docsrs, doc(cfg(feature = "vt")))]
162 pub fn vt_client(mut self, vt_client: malwaredb_virustotal::VirusTotalClient) -> Self {
163 self.vt_client = Some(vt_client);
164 self
165 }
166
167 pub async fn tls(mut self, cert_file: PathBuf, key_file: PathBuf) -> Result<Self> {
174 ensure!(cert_file.exists(), "Certificate file {} does not exist!", cert_file.display());
175
176 ensure!(key_file.exists(), "Key file {} does not exist!", key_file.display());
177
178 let cert_ext_str = cert_file
179 .extension()
180 .context("failed to get certificate extension")?;
181 let key_ext_str = key_file
182 .extension()
183 .context("failed to get key extension")?;
184
185 if rustls::crypto::CryptoProvider::get_default().is_none() {
187 rustls::crypto::aws_lc_rs::default_provider()
188 .install_default()
189 .map_err(|_| anyhow!("failed to install AWS-LC crypto provider"))?;
190 }
191
192 let config = if (cert_ext_str == "pem" || cert_ext_str == "crt") && key_ext_str == "pem" {
193 RustlsConfig::from_pem_file(cert_file, key_file)
194 .await
195 .context("failed to load or parse certificate and key pem files")?
196 } else if cert_ext_str == "der" && key_ext_str == "der" {
197 let cert_contents =
198 std::fs::read(cert_file).context("failed to read certificate file")?;
199 let key_contents =
200 std::fs::read(key_file).context("failed to read private key file")?;
201 RustlsConfig::from_der(vec![cert_contents], key_contents)
202 .await
203 .context("failed to parse certificate and key der files")?
204 } else {
205 bail!(
206 "Unknown or unmatched certificate and key file extensions {} and {}",
207 cert_ext_str.display(),
208 key_ext_str.display()
209 );
210 };
211
212 self.tls_config = Some(config);
213 Ok(self)
214 }
215
216 #[must_use]
219 pub fn enable_mdns(mut self) -> Self {
220 self.mdns = true;
221 self
222 }
223
224 pub async fn into_state(self) -> Result<State> {
230 let db_config = self.db_type.get_config().await?;
231 let keys = self.db_type.get_encryption_keys().await?;
232
233 Ok(State {
234 port: self.port,
235 directory: self.directory,
236 max_upload: self.max_upload,
237 ip: self.ip,
238 db_type: Arc::new(self.db_type),
239 started: SystemTime::now(),
240 db_config,
241 keys,
242 #[cfg(feature = "vt")]
243 vt_client: self.vt_client,
244 tls_config: self.tls_config,
245 mdns: if self.mdns {
246 Some(ServiceDaemon::new()?)
247 } else {
248 None
249 },
250 })
251 }
252}
253
254pub struct State {
256 pub port: u16,
258
259 pub directory: Option<PathBuf>,
261
262 pub max_upload: usize,
264
265 pub ip: IpAddr,
267
268 pub db_type: Arc<db::DatabaseType>,
270
271 pub started: SystemTime,
273
274 pub db_config: MDBConfig,
276
277 pub(crate) keys: HashMap<u32, FileEncryption>,
279
280 #[cfg(feature = "vt")]
282 pub(crate) vt_client: Option<malwaredb_virustotal::VirusTotalClient>,
283
284 tls_config: Option<RustlsConfig>,
286
287 mdns: Option<ServiceDaemon>,
289}
290
291impl State {
292 pub async fn store_bytes(&self, data: &[u8]) -> Result<bool> {
299 if let Some(dest_path) = &self.directory {
300 let mut hasher = Sha256::new();
301 hasher.update(data);
302 let sha256 = hex::encode(hasher.finalize());
303
304 let hashed_path =
308 format!("{}/{}/{}/{}", &sha256[0..2], &sha256[2..4], &sha256[4..6], sha256);
309
310 let mut dest_path = dest_path.clone();
313 dest_path.push(hashed_path);
314
315 let mut just_the_dir = dest_path.clone();
317 just_the_dir.pop();
318 std::fs::create_dir_all(just_the_dir)?;
319
320 let data = if self.db_config.compression {
321 let buff = Cursor::new(data);
322 let mut compressed = Vec::with_capacity(data.len() / 2);
323 zstd::stream::copy_encode(buff, &mut compressed, 4)?;
324 compressed
325 } else {
326 data.to_vec()
327 };
328
329 self.db_type.clear_file_crypto(&sha256).await?; let data = if let Some(key_id) = self.db_config.default_key {
331 if let Some(key) = self.keys.get(&key_id) {
332 let nonce = key.nonce();
333 self.db_type
334 .set_file_nonce(&sha256, nonce.as_deref())
335 .await?;
336 key.encrypt(&data, nonce)?
337 } else {
338 bail!("Key not available!")
339 }
340 } else {
341 data
342 };
343
344 std::fs::write(dest_path, data)?;
345
346 Ok(true)
347 } else {
348 Ok(false)
349 }
350 }
351
352 pub async fn retrieve_bytes(&self, sha256: &String) -> Result<Vec<u8>> {
360 if let Some(dest_path) = &self.directory {
361 let path = format!("{}/{}/{}/{}", &sha256[0..2], &sha256[2..4], &sha256[4..6], sha256);
362 let contents = std::fs::read(dest_path.join(path))?;
367
368 let contents = if self.keys.is_empty() {
369 contents
371 } else {
372 let (key_id, nonce) = self.db_type.get_file_encryption_key_id(sha256).await?;
373 if let Some(key_id) = key_id {
374 if let Some(key) = self.keys.get(&key_id) {
375 key.decrypt(&contents, nonce)?
376 } else {
377 bail!("File was encrypted but we don't have tke key!")
378 }
379 } else {
380 contents
382 }
383 };
384
385 if contents.starts_with(&GZIP_MAGIC) {
386 let buff = Cursor::new(contents);
387 let mut decompressor = GzDecoder::new(buff);
388 let mut decompressed: Vec<u8> = vec![];
389 decompressor.read_to_end(&mut decompressed)?;
390 Ok(decompressed)
391 } else if contents.starts_with(&ZSTD_MAGIC) {
392 let buff = Cursor::new(contents);
393 let mut decompressed: Vec<u8> = vec![];
394 zstd::stream::copy_decode(buff, &mut decompressed)?;
395 Ok(decompressed)
396 } else {
397 Ok(contents)
398 }
399 } else {
400 bail!("files are not saved")
401 }
402 }
403
404 #[must_use]
410 pub fn since(&self) -> Duration {
411 let now = SystemTime::now();
412 now.duration_since(self.started).unwrap()
413 }
414
415 pub async fn get_info(&self) -> Result<ServerInfo> {
421 let db_info = self.db_type.db_info().await?;
422 let uptime = Local::now() - self.since();
423 let mem_size = app_memory_usage_fetcher::get_memory_usage_string().unwrap_or_default();
424
425 Ok(ServerInfo {
426 os_name: std::env::consts::OS.into(),
427 memory_used: mem_size,
428 num_samples: db_info.num_files,
429 num_users: db_info.num_users,
430 uptime: HumanTime::from(uptime).to_text_en(Accuracy::Rough, Tense::Present),
431 mdb_version: MDB_VERSION_SEMVER.clone(),
432 db_version: db_info.version,
433 db_size: db_info.size,
434 instance_name: self.db_config.name.clone(),
435 vt_support: cfg!(feature = "vt"),
436 yara_enabled: cfg!(feature = "yara"),
437 })
438 }
439
440 #[inline]
447 #[cfg(feature = "admin")]
448 #[cfg_attr(docsrs, doc(cfg(feature = "admin")))]
449 pub async fn encryption_keys(&self) -> Result<HashMap<u32, crypto::EncryptionOption>> {
450 Ok(self
451 .db_type
452 .get_encryption_keys()
453 .await?
454 .iter()
455 .map(|(id, key)| (*id, key.key_type()))
456 .collect())
457 }
458
459 pub async fn serve(
467 self,
468 #[cfg(target_family = "windows")] rx: Option<tokio::sync::mpsc::Receiver<()>>,
469 ) -> Result<()> {
470 let socket = SocketAddr::new(self.ip, self.port);
471 let arc_self = Arc::new(self);
472 let db_info = arc_self.db_type.clone();
473
474 #[cfg(feature = "yara")]
475 {
476 if arc_self.directory.is_some() {
477 start_yara_process(arc_self.clone());
478 }
479 }
480
481 tokio::spawn(async move {
482 loop {
483 match db_info.cleanup().await {
484 Ok(removed) => {
485 trace!("Pagination cleanup succeeded, {removed} searches removed");
486 }
487 Err(e) => warn!("Pagination cleanup failed: {e}"),
488 }
489
490 tokio::time::sleep(DB_CLEANUP_INTERVAL).await;
491 }
492 });
493
494 if let Some(mdns) = &arc_self.mdns {
495 let host_name = format!("{}.local.", arc_self.ip);
496 let ssl = arc_self.tls_config.is_some();
497 #[cfg(not(feature = "anonymous"))]
498 let properties = [("ssl", ssl.to_string()), ("version", MDB_VERSION.into())];
499 #[cfg(feature = "anonymous")]
500 let properties = if arc_self.db_config.anonymous_uid.is_some() {
501 [
502 ("ssl", ssl.to_string()),
503 ("version", MDB_VERSION.into()),
504 ("anonymous", "true".into()),
505 ]
506 .to_vec()
507 } else {
508 [("ssl", ssl.to_string()), ("version", MDB_VERSION.into())].to_vec()
509 };
510 let service = {
511 let mut service = ServiceInfo::new(
512 malwaredb_api::MDNS_NAME,
513 &arc_self.db_config.name,
514 &host_name,
515 &arc_self.ip,
516 arc_self.port,
517 &properties[..],
518 )?;
519 if arc_self.ip.is_unspecified() {
520 service = service.enable_addr_auto();
521 }
522 service
523 };
524 trace!("Registering MDNS service...");
525 mdns.register(service)?;
526 }
527
528 if let Some(tls_config) = arc_self.tls_config.clone() {
529 println!("Listening on https://{socket:?}");
530 let handle = axum_server::Handle::<SocketAddr>::new();
531 let server_future = axum_server::bind_rustls(socket, tls_config)
532 .serve(http::app(arc_self).into_make_service());
533 tokio::select! {
534 () = shutdown_signal(#[cfg(target_family = "windows")]rx) =>
535 handle.graceful_shutdown(Some(Duration::from_secs(30))),
536 res = server_future => res?,
537 }
538 warn!("Terminate signal received");
539 } else {
540 println!("Listening on http://{socket:?}");
541 let listener = TcpListener::bind(socket)
542 .await
543 .context(format!("failed to bind socket {socket}"))?;
544 axum::serve(listener, http::app(arc_self).into_make_service())
545 .with_graceful_shutdown(shutdown_signal(
546 #[cfg(target_family = "windows")]
547 rx,
548 ))
549 .await?;
550 warn!("Terminate signal received");
551 }
552 Ok(())
553 }
554
555 #[cfg(feature = "admin")]
566 pub async fn rewrite_files(
567 &self,
568 rewritten_counter: Arc<AtomicU64>,
569 stop_signal: Arc<AtomicBool>,
570 ) -> Result<bool> {
571 use std::sync::atomic::Ordering;
572
573 let Some(dest_path) = &self.directory else {
574 return Ok(false);
575 };
576
577 for entry in walkdir::WalkDir::new(dest_path)
578 .follow_links(false)
579 .max_depth(4)
580 .into_iter()
581 .flatten()
582 {
583 if stop_signal.load(Ordering::Relaxed) {
584 return Ok(true);
585 }
586
587 if entry.file_type().is_file() {
588 let file_name = entry
589 .file_name()
590 .to_str()
591 .ok_or_else(|| anyhow!("Invalid file name for path: {entry:?}"))?
592 .to_string();
593 ensure!(
594 file_name.len() == 64,
595 "File name must be 64 characters to be a SHA-256 hash"
596 );
597
598 let mut header = vec![0; ZSTD_MAGIC.len()];
600 let mut file = std::fs::File::open(entry.path())?;
601 file.read_exact(&mut header)?;
602
603 let do_compression = self.db_config.compression != (header == ZSTD_MAGIC);
604 let do_encryption = self.db_config.default_key
605 != self.db_type.get_file_encryption_key_id(&file_name).await?.0;
606
607 if !do_compression || !do_encryption {
609 continue;
610 }
611
612 let contents = self.retrieve_bytes(&file_name).await?;
614 self.store_bytes(&contents).await?;
615
616 rewritten_counter.fetch_add(1, Ordering::Relaxed);
617 }
618 }
619 Ok(true)
620 }
621}
622
623impl Debug for State {
624 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
625 let tls_mode = if self.tls_config.is_some() {
626 ", TLS mode"
627 } else {
628 ""
629 };
630 write!(f, "MDB state, port {}, database {:?}{tls_mode}", self.port, self.db_type)
631 }
632}
633
634#[cfg(feature = "yara")]
635#[allow(clippy::needless_pass_by_value)]
636fn start_yara_process(state: Arc<State>) {
637 let state_clone = state.clone();
638 tokio::spawn(async move {
639 let state_clone = state_clone.clone();
640 loop {
641 let tasks = match state_clone
642 .clone()
643 .db_type
644 .get_unfinished_yara_tasks()
645 .await
646 {
647 Ok(tasks) => tasks,
648 Err(e) => {
649 warn!("Failed to get Yara tasks: {e}");
650 continue;
651 }
652 };
653 for task in tasks {
654 let state_clone = state_clone.clone();
655 let (hashes, last_file_id) = match state_clone
656 .db_type
657 .user_allowed_files_by_sha256(task.user_id, task.last_file_id)
658 .await
659 {
660 Ok(hashes) => hashes,
661 Err(e) => {
662 warn!("Failed to get user allowed files: {e}");
663 continue;
664 }
665 };
666
667 if hashes.is_empty() {
668 if let Err(e) = state_clone
669 .db_type
670 .mark_yara_task_as_finished(task.id)
671 .await
672 {
673 warn!("Failed to mark yara task as finished: {e}");
674 }
675 continue;
676 }
677 tokio::spawn(async move {
678 for hash in hashes {
679 let bytes = match state_clone.clone().retrieve_bytes(&hash).await {
680 Ok(bytes) => bytes,
681 Err(e) => {
682 warn!("Failed to retrieve bytes for hash {hash}: {e}");
683 continue;
684 }
685 };
686 let matches = task.process_yara_rules(&bytes).unwrap_or_else(|e| {
687 warn!("Failed to process Yara rules: {e}");
688 Vec::new()
689 });
690
691 for match_ in matches {
692 if let Err(e) = state_clone
693 .db_type
694 .add_yara_match(task.id, &match_, &hash)
695 .await
696 {
697 warn!("Failed to add Yara match: {e}");
698 }
699 }
700 }
701 if let Err(e) = state_clone
702 .db_type
703 .yara_add_next_file_id(task.id, last_file_id)
704 .await
705 {
706 warn!("Failed to update yara task next file id: {e}");
707 }
708 });
709 }
710 tokio::time::sleep(Duration::from_secs(5)).await;
711 }
712 });
713}
714
715async fn shutdown_signal(
718 #[cfg(target_family = "windows")] mut rx: Option<tokio::sync::mpsc::Receiver<()>>,
719) {
720 let ctrl_c = async {
721 tokio::signal::ctrl_c()
722 .await
723 .expect("failed to install Ctrl+C handler");
724 };
725
726 #[cfg(unix)]
727 let terminate = async {
728 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
729 .expect("failed to install signal handler")
730 .recv()
731 .await;
732 };
733
734 #[cfg(not(unix))]
735 let terminate = std::future::pending::<()>();
736
737 #[cfg(target_family = "windows")]
738 if let Some(rx_inner) = &mut rx {
739 let terminate_rx = rx_inner.recv();
740
741 tokio::select! {
742 () = ctrl_c => {},
743 () = terminate => {},
744 Some(()) = terminate_rx => {},
745 }
746 } else {
747 tokio::select! {
748 () = ctrl_c => {},
749 () = terminate => {},
750 }
751 }
752
753 #[cfg(not(target_family = "windows"))]
754 tokio::select! {
755 () = ctrl_c => {},
756 () = terminate => {},
757 }
758}