1use super::State;
4use malwaredb_api::digest::HashType;
5use malwaredb_api::{
6 GetAPIKeyRequest, GetAPIKeyResponse, GetUserInfoResponse, Labels, MDB_API_HEADER, NewSampleB64,
7 NewSampleBytes, Report, SearchRequest, SearchResponse, ServerError, ServerInfo, ServerResponse,
8 SimilarSamplesRequest, SimilarSamplesResponse, Sources, SupportedFileTypes, YaraSearchRequest,
9 YaraSearchRequestResponse, YaraSearchResponse,
10};
11
12use std::fmt::{Display, Formatter};
13use std::io::Cursor;
14use std::iter::once;
15use std::sync::Arc;
16
17use axum::body::Bytes;
18use axum::extract::{DefaultBodyLimit, Path, Request};
19use axum::http::{StatusCode, header};
20use axum::middleware::Next;
21use axum::response::{IntoResponse, Response};
22use axum::routing::{get, post};
23use axum::{Extension, Json, Router, middleware};
24use axum_cbor::Cbor;
25use base64::{Engine as _, engine::general_purpose};
26use constcat::concat;
27use http::{HeaderMap, HeaderName, HeaderValue};
28use sha2::{Digest, Sha256};
29use tower_http::compression::CompressionLayer;
30use tower_http::decompression::DecompressionLayer;
31use tower_http::limit::RequestBodyLimitLayer;
32use tower_http::sensitive_headers::SetSensitiveHeadersLayer;
33use uuid::Uuid;
34
35mod receive;
36
37const FAVICON_URL: &str = "/favicon.ico";
38
39pub fn app(state: Arc<State>) -> Router {
41 const UPLOAD_OVERHEAD: usize = std::mem::size_of::<Json<NewSampleB64>>() * 2;
43
44 let compression_layer = CompressionLayer::new()
45 .br(true)
46 .deflate(true)
47 .gzip(true)
48 .zstd(true);
49
50 let decompression_layer = DecompressionLayer::new()
51 .br(true)
52 .deflate(true)
53 .gzip(true)
54 .zstd(true);
55
56 let size_limit_layer = RequestBodyLimitLayer::new(UPLOAD_OVERHEAD + state.max_upload);
57
58 Router::new()
59 .route("/", get(health))
60 .route(FAVICON_URL, get(favicon))
61 .route(malwaredb_api::SERVER_INFO_URL, get(get_mdb_info))
62 .route(malwaredb_api::USER_LOGIN_URL, post(user_login))
63 .route(malwaredb_api::USER_LOGOUT_URL, get(user_logout))
64 .route(malwaredb_api::USER_INFO_URL, get(get_user_groups_sources))
65 .route(malwaredb_api::SUPPORTED_FILE_TYPES_URL, get(get_supported_types))
66 .route(malwaredb_api::LIST_LABELS_URL, get(get_labels))
67 .route(malwaredb_api::LIST_SOURCES_URL, get(get_sources))
68 .route(malwaredb_api::UPLOAD_SAMPLE_JSON_URL, post(upload_new_sample_json))
69 .route(malwaredb_api::UPLOAD_SAMPLE_CBOR_URL, post(upload_new_sample_cbor))
70 .route(malwaredb_api::SEARCH_URL, post(sample_search))
71 .route(concat!(malwaredb_api::DOWNLOAD_SAMPLE_URL, "/{hash}"), get(download_sample))
72 .route(
73 concat!(malwaredb_api::DOWNLOAD_SAMPLE_CART_URL, "/{hash}"),
74 get(download_sample_cart),
75 )
76 .route(concat!(malwaredb_api::SAMPLE_REPORT_URL, "/{hash}"), get(sample_report))
77 .route(malwaredb_api::SIMILAR_SAMPLES_URL, post(find_similar))
78 .route(malwaredb_api::YARA_SEARCH_URL, post(yara_search))
79 .route(concat!(malwaredb_api::YARA_SEARCH_URL, "/{uuid}"), get(yara_search_get))
80 .layer(DefaultBodyLimit::max(state.max_upload))
81 .layer(compression_layer)
82 .layer(decompression_layer)
83 .layer(size_limit_layer)
84 .layer(SetSensitiveHeadersLayer::new(once(HeaderName::from_static(MDB_API_HEADER))))
85 .route_layer(middleware::from_fn(response_header_middleware))
86 .layer(Extension(state))
87}
88
89struct UserInfo {
91 pub id: u32,
92}
93
94#[inline]
97async fn response_header_middleware(
98 Extension(state): Extension<Arc<State>>,
99 headers: HeaderMap,
100 mut req: Request,
101 next: Next,
102) -> Result<Response, HttpError> {
103 const ALWAYS_ALLOWED_ENDPOINTS: [&str; 4] = [
104 "/",
105 FAVICON_URL,
106 malwaredb_api::SERVER_INFO_URL,
107 malwaredb_api::USER_LOGIN_URL,
108 ];
109
110 if !ALWAYS_ALLOWED_ENDPOINTS.contains(&req.uri().path()) {
111 let uid = if let Some(key) = headers.get(MDB_API_HEADER) {
112 let key = key
113 .to_str()
114 .map_err(|_| HttpError(ServerError::Unauthorized, StatusCode::NOT_ACCEPTABLE))?;
115 state.db_type.get_uid(key).await.map_err(|e| {
116 tracing::warn!("Failed to get user ID from API key: {e}");
117 HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED)
118 })?
119 } else {
120 #[cfg(feature = "anonymous")]
121 {
122 if let Some(anon_uid) = state.db_config.anonymous_uid {
123 tracing::info!("Using anonymous user ID: {anon_uid}");
124 anon_uid
125 } else {
126 return Err(HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED));
127 }
128 }
129 #[cfg(not(feature = "anonymous"))]
130 return Err(crate::http::HttpError(
131 ServerError::Unauthorized,
132 StatusCode::UNAUTHORIZED,
133 ));
134 };
135
136 req.extensions_mut().insert(Arc::new(UserInfo { id: uid }));
137 }
138
139 let mut response = next.run(req).await; response
144 .headers_mut()
145 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
146
147 Ok(response)
148}
149
150#[inline]
151async fn health() -> StatusCode {
152 StatusCode::OK
153}
154
155#[inline]
156async fn favicon() -> Response {
157 const ICON: Bytes = Bytes::from_static(include_bytes!("../../MDB_Logo.ico"));
158
159 let mut bytes = ICON.into_response();
160 bytes
161 .headers_mut()
162 .insert(header::CONTENT_TYPE, HeaderValue::from_static("image/vnd.microsoft.icon"));
163
164 bytes
165}
166
167#[inline]
168async fn get_mdb_info(
169 Extension(state): Extension<Arc<State>>,
170) -> Result<Json<ServerResponse<ServerInfo>>, HttpError> {
171 let server_info = ServerResponse::Success(state.get_info().await?);
172 Ok(Json(server_info))
173}
174
175#[inline]
176async fn user_login(
177 Extension(state): Extension<Arc<State>>,
178 Json(payload): Json<GetAPIKeyRequest>,
179) -> Result<Json<ServerResponse<GetAPIKeyResponse>>, HttpError> {
180 let api_key = state
181 .db_type
182 .authenticate(&payload.user, &payload.password)
183 .await?;
184
185 Ok(Json(ServerResponse::Success(GetAPIKeyResponse {
186 key: api_key,
187 message: None,
188 })))
189}
190
191#[inline]
192async fn user_logout(
193 Extension(state): Extension<Arc<State>>,
194 Extension(user): Extension<Arc<UserInfo>>,
195) -> Result<StatusCode, HttpError> {
196 state.db_type.reset_own_api_key(user.id).await?;
197 Ok(StatusCode::OK)
198}
199
200#[inline]
201async fn get_user_groups_sources(
202 Extension(state): Extension<Arc<State>>,
203 Extension(user): Extension<Arc<UserInfo>>,
204) -> Result<Json<ServerResponse<GetUserInfoResponse>>, HttpError> {
205 let groups_sources = ServerResponse::Success(state.db_type.get_user_info(user.id).await?);
206 Ok(Json(groups_sources))
207}
208
209#[inline]
210async fn get_supported_types(
211 Extension(state): Extension<Arc<State>>,
212) -> Result<Json<ServerResponse<SupportedFileTypes>>, HttpError> {
213 let data_types = state.db_type.get_known_data_types().await?;
214 let file_types = ServerResponse::Success(SupportedFileTypes {
215 types: data_types.into_iter().map(Into::into).collect(),
216 message: None,
217 });
218 Ok(Json(file_types))
219}
220
221#[inline]
222async fn get_labels(
223 Extension(state): Extension<Arc<State>>,
224) -> Result<Json<ServerResponse<Labels>>, HttpError> {
225 let labels = ServerResponse::Success(state.db_type.get_labels().await?);
226 Ok(Json(labels))
227}
228
229#[inline]
230async fn get_sources(
231 Extension(state): Extension<Arc<State>>,
232 Extension(user): Extension<Arc<UserInfo>>,
233) -> Result<Json<ServerResponse<Sources>>, HttpError> {
234 let sources = ServerResponse::Success(state.db_type.get_user_sources(user.id).await?);
235 Ok(Json(sources))
236}
237
238#[inline]
239async fn upload_new_sample_json(
240 Extension(state): Extension<Arc<State>>,
241 Extension(user): Extension<Arc<UserInfo>>,
242 Json(payload): Json<NewSampleB64>,
243) -> Result<StatusCode, HttpError> {
244 let allowed = state
245 .db_type
246 .allowed_user_source(user.id, payload.source_id)
247 .await?;
248
249 if !allowed {
250 return Err(HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED));
251 }
252
253 let received_hash = hex::decode(&payload.sha256)?;
254 let bytes = general_purpose::STANDARD.decode(&payload.file_contents_b64)?;
255
256 let mut hasher = Sha256::new();
257 hasher.update(&bytes);
258 let result = hasher.finalize();
259
260 if result[..] != received_hash[..] {
261 return Err(HttpError(ServerError::Unauthorized, StatusCode::NOT_ACCEPTABLE));
262 }
263
264 receive::incoming_sample(state.clone(), bytes, user.id, payload.source_id, payload.file_name)
265 .await?;
266
267 Ok(StatusCode::OK)
268}
269
270#[inline]
271async fn upload_new_sample_cbor(
272 Extension(state): Extension<Arc<State>>,
273 Extension(user): Extension<Arc<UserInfo>>,
274 Cbor(payload): Cbor<NewSampleBytes>,
275) -> Result<StatusCode, HttpError> {
276 let allowed = state
277 .db_type
278 .allowed_user_source(user.id, payload.source_id)
279 .await?;
280
281 if !allowed {
282 return Err(HttpError(ServerError::Unauthorized, StatusCode::UNAUTHORIZED));
283 }
284
285 let received_hash = hex::decode(&payload.sha256)?;
286 let bytes = payload.file_contents;
287 let mut hasher = Sha256::new();
288 hasher.update(&bytes);
289 let result = hasher.finalize();
290
291 if result[..] != received_hash[..] {
292 return Err(HttpError(ServerError::Unauthorized, StatusCode::NOT_ACCEPTABLE));
293 }
294
295 receive::incoming_sample(state.clone(), bytes, user.id, payload.source_id, payload.file_name)
296 .await?;
297
298 Ok(StatusCode::OK)
299}
300
301#[inline]
302async fn sample_search(
303 Extension(state): Extension<Arc<State>>,
304 Extension(user): Extension<Arc<UserInfo>>,
305 Json(payload): Json<SearchRequest>,
306) -> Result<Json<ServerResponse<SearchResponse>>, HttpError> {
307 let hashes = ServerResponse::Success(state.db_type.partial_search(user.id, payload).await?);
308 Ok(Json(hashes))
309}
310
311#[inline]
312async fn download_sample(
313 Path(hash): Path<String>,
314 Extension(user): Extension<Arc<UserInfo>>,
315 Extension(state): Extension<Arc<State>>,
316) -> Result<Response, HttpError> {
317 if state.directory.is_none() {
318 return Err(NO_SAMPLES_STORED_ERROR);
319 }
320
321 let hash = HashType::try_from(hash.as_str())?;
322 let sha256 = state.db_type.retrieve_sample(user.id, &hash).await?;
323
324 let hash = HashType::try_from(sha256.as_str())?;
326
327 let contents = state.retrieve_bytes(&sha256).await?;
328
329 let mut bytes = Bytes::from(contents).into_response();
330 let name_header_value = format!("attachment; filename=\"{sha256}\"");
331 bytes.headers_mut().insert(
332 header::CONTENT_DISPOSITION,
333 HeaderValue::from_str(&name_header_value)
334 .unwrap_or(HeaderValue::from_static("Unknown.bin")),
335 );
336
337 bytes
338 .headers_mut()
339 .insert("content-digest", HeaderValue::from_str(&hash.content_digest_header())?);
340
341 Ok(bytes)
342}
343
344#[inline]
345async fn download_sample_cart(
346 Path(hash): Path<String>,
347 Extension(user): Extension<Arc<UserInfo>>,
348 Extension(state): Extension<Arc<State>>,
349) -> Result<Response, HttpError> {
350 if state.directory.is_none() {
351 return Err(NO_SAMPLES_STORED_ERROR);
352 }
353
354 let hash = HashType::try_from(hash.as_str())?;
355 let sha256 = state.db_type.retrieve_sample(user.id, &hash).await?;
356 let report = state.db_type.get_sample_report(user.id, &hash).await?;
357
358 let contents = state.retrieve_bytes(&sha256).await?;
359 let contents_cursor = Cursor::new(contents);
360 let mut output_cursor = Cursor::new(vec![]);
361
362 let mut output_metadata = cart_container::JsonMap::new();
363 output_metadata.insert("sha384".into(), report.sha384.into());
364 output_metadata.insert("sha512".into(), report.sha512.into());
365 output_metadata.insert("entropy".into(), report.entropy.into());
366 if let Some(filecmd) = report.filecommand {
367 output_metadata.insert("file".into(), filecmd.into());
368 }
369
370 cart_container::pack_stream(
371 contents_cursor,
372 &mut output_cursor,
373 Some(output_metadata),
374 None,
375 cart_container::digesters::default_digesters(), None,
377 )?;
378
379 let mut hasher = Sha256::new();
380 hasher.update(output_cursor.get_ref());
381 let hash_b64 = general_purpose::STANDARD.encode(hasher.finalize());
382
383 let mut bytes = Bytes::from(output_cursor.into_inner()).into_response();
384 let name_header_value = format!("attachment; filename=\"{sha256}.cart\"");
385 bytes.headers_mut().insert(
386 header::CONTENT_DISPOSITION,
387 HeaderValue::from_str(&name_header_value)
388 .unwrap_or(HeaderValue::from_static("Unknown.cart")),
389 );
390 bytes
391 .headers_mut()
392 .insert("content-digest", HeaderValue::from_str(&format!("sha-256=:{hash_b64}:"))?);
393
394 Ok(bytes)
395}
396
397#[inline]
398async fn sample_report(
399 Path(hash): Path<String>,
400 Extension(user): Extension<Arc<UserInfo>>,
401 Extension(state): Extension<Arc<State>>,
402) -> Result<Json<ServerResponse<Report>>, HttpError> {
403 let hash = HashType::try_from(hash.as_str())?;
404 let report =
405 ServerResponse::<Report>::Success(state.db_type.get_sample_report(user.id, &hash).await?);
406 Ok(Json(report))
407}
408
409#[inline]
410async fn find_similar(
411 Extension(state): Extension<Arc<State>>,
412 Extension(user): Extension<Arc<UserInfo>>,
413 Json(payload): Json<SimilarSamplesRequest>,
414) -> Result<Json<ServerResponse<SimilarSamplesResponse>>, HttpError> {
415 let results = state
416 .db_type
417 .find_similar_samples(user.id, &payload.hashes)
418 .await?;
419
420 Ok(Json(ServerResponse::Success(SimilarSamplesResponse {
421 results,
422 message: None,
423 })))
424}
425
426#[inline]
427#[allow(unused_variables)]
428async fn yara_search(
429 Extension(state): Extension<Arc<State>>,
430 Extension(user): Extension<Arc<UserInfo>>,
431 Json(payload): Json<YaraSearchRequest>,
432) -> Result<Json<ServerResponse<YaraSearchRequestResponse>>, HttpError> {
433 #[cfg(feature = "yara")]
434 {
435 if state.directory.is_some() {
436 let yara_string = payload.rules.join("\n");
437 let rules = crate::yara::compile_yara_rules(payload)?;
438 let yara_bytes = rules.serialize()?;
439
440 let search_uuid = state
441 .db_type
442 .add_yara_search(user.id, &yara_string, &yara_bytes)
443 .await?;
444
445 Ok(Json(ServerResponse::Success(YaraSearchRequestResponse { uuid: search_uuid })))
446 } else {
447 Err(NO_SAMPLES_STORED_ERROR)
448 }
449 }
450
451 #[cfg(not(feature = "yara"))]
452 {
453 tracing::warn!("Received Yara search request, but Yara support is not enabled");
454 Err(HttpError(ServerError::Unsupported, StatusCode::INTERNAL_SERVER_ERROR))
455 }
456}
457
458#[inline]
459#[allow(unused_variables)]
460async fn yara_search_get(
461 Path(uuid): Path<Uuid>,
462 Extension(state): Extension<Arc<State>>,
463 Extension(user): Extension<Arc<UserInfo>>,
464) -> Result<Json<ServerResponse<YaraSearchResponse>>, HttpError> {
465 #[cfg(feature = "yara")]
466 {
467 if state.directory.is_some() {
468 Ok(Json(ServerResponse::Success(state.db_type.get_yara_results(uuid, user.id).await?)))
469 } else {
470 Err(NO_SAMPLES_STORED_ERROR)
471 }
472 }
473 #[cfg(not(feature = "yara"))]
474 {
475 tracing::warn!("Received Yara search request, but Yara support is not enabled");
476 Err(HttpError(ServerError::Unsupported, StatusCode::INTERNAL_SERVER_ERROR))
477 }
478}
479
480const NO_SAMPLES_STORED_ERROR: HttpError =
484 HttpError(ServerError::NoSamples, StatusCode::NOT_ACCEPTABLE);
485
486pub struct HttpError(pub ServerError, pub StatusCode);
488
489impl IntoResponse for HttpError {
491 fn into_response(self) -> Response {
492 let response: ServerResponse<String> = ServerResponse::Error(self.0);
493 match serde_json::to_string(&response) {
494 Ok(json) => (self.1, json).into_response(),
495 Err(_) => self.1.into_response(),
496 }
497 }
498}
499
500impl Display for HttpError {
501 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
502 write!(f, "{}", self.0)
503 }
504}
505
506impl<E> From<E> for HttpError
508where
509 E: Into<anyhow::Error>,
510{
511 fn from(_err: E) -> Self {
512 Self(ServerError::ServerError, StatusCode::INTERNAL_SERVER_ERROR)
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::StateBuilder;
520 use crate::crypto::{EncryptionOption, FileEncryption};
521 use crate::db::DatabaseType;
522 use malwaredb_api::PartialHashSearchType;
523
524 use std::collections::HashMap;
525 use std::path::PathBuf;
526 use std::sync::{Once, RwLock};
527 use std::time::{Instant, SystemTime};
528 use std::{env, fs};
529
530 use anyhow::Context;
531 use axum::body::Body;
532 use axum::http::Request;
533 use chrono::Local;
534 use constcat::concat;
535 use deadpool_postgres::tokio_postgres::{Config, NoTls};
536 use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
537 use http::header::CONTENT_TYPE;
538 use http_body_util::BodyExt;
539 use rstest::rstest;
540 use tower::ServiceExt;
541 use uuid::Uuid;
543
544 const ADMIN_UNAME: &str = "admin";
545 const ADMIN_PASSWORD: &str = "password12345";
546 const SAMPLE_BYTES: &[u8] = include_bytes!("../../../types/testdata/elf/elf_haiku_x86");
547
548 static TRACING: Once = Once::new();
549
550 fn init_tracing() {
551 tracing_subscriber::fmt()
552 .with_max_level(tracing::Level::TRACE)
553 .init();
554 }
555
556 async fn state(compress: bool, encrypt: bool) -> (Arc<State>, u32) {
557 TRACING.call_once(init_tracing);
558
559 let mut db_file = env::temp_dir();
561 db_file.push(format!("testing_sqlite_{}.db", Uuid::new_v4()));
562 if std::path::Path::new(&db_file).exists() {
563 fs::remove_file(&db_file)
564 .context(format!("failed to delete old SQLite file {db_file:?}"))
565 .unwrap();
566 }
567
568 let db_type =
569 DatabaseType::from_string(&format!("file:{}", db_file.to_str().unwrap()), None)
570 .await
571 .context(format!("failed to create SQLite instance for {db_file:?}"))
572 .unwrap();
573 if compress {
574 db_type.enable_compression().await.unwrap();
575 }
576 let keys = if encrypt {
577 let key = FileEncryption::from(EncryptionOption::Xor);
578 let key_id = db_type.add_file_encryption_key(&key).await.unwrap();
579 let mut keys = HashMap::new();
580 keys.insert(key_id, key);
581 keys
582 } else {
583 HashMap::new()
584 };
585 let db_config = db_type.get_config().await.unwrap();
586
587 let state = State {
588 port: 8080,
589 directory: Some(
590 tempfile::TempDir::with_prefix("mdb-temp-samples")
591 .unwrap()
592 .path()
593 .into(),
594 ),
595 max_upload: 10 * 1024 * 1024,
596 ip: "127.0.0.1".parse().unwrap(),
597 db_type: Arc::new(db_type),
598 db_config,
599 keys,
600 started: SystemTime::now(),
601 #[cfg(feature = "vt")]
602 vt_client: None,
603 tls_config: None,
604 mdns: None,
605 };
606
607 state
608 .db_type
609 .set_password(ADMIN_UNAME, ADMIN_PASSWORD)
610 .await
611 .context("Failed to set admin password")
612 .unwrap();
613
614 let source_id = state
615 .db_type
616 .create_source("temp-source", None, None, Local::now(), true, Some(false))
617 .await
618 .unwrap();
619
620 state
621 .db_type
622 .add_group_to_source(0, source_id)
623 .await
624 .unwrap();
625
626 (Arc::new(state), source_id)
627 }
628
629 async fn state_and_token(pem_file: bool, postgres: bool) -> (State, u32, String) {
631 TRACING.call_once(init_tracing);
632
633 let mut builder = if postgres {
634 const CONNECTION_STRING: &str = "user=malwaredbtesting password=malwaredbtesting dbname=malwaredbtesting host=localhost sslmode=disable";
635
636 let pg_config = CONNECTION_STRING.parse::<Config>().unwrap();
637 let mgr_config = ManagerConfig {
638 recycling_method: RecyclingMethod::Fast,
639 };
640 let mgr = Manager::from_config(pg_config, NoTls, mgr_config);
641 let pool = Pool::builder(mgr)
642 .max_size(num_cpus::get().min(16))
643 .build()
644 .unwrap();
645
646 let client = pool.get().await.unwrap();
647 client
648 .batch_execute("DROP SCHEMA public CASCADE; CREATE SCHEMA public;")
649 .await
650 .unwrap();
651
652 StateBuilder::new(concat!("postgres ", CONNECTION_STRING), None)
653 .await
654 .unwrap()
655 } else {
656 let mut db_file = env::temp_dir();
657 db_file.push(format!("testing_sqlite_{}.db", Uuid::new_v4()));
658 if std::path::Path::new(&db_file).exists() {
659 fs::remove_file(&db_file)
660 .context(format!("failed to delete old SQLite file {db_file:?}"))
661 .unwrap();
662 }
663
664 StateBuilder::new(&format!("file:{}", db_file.display()), None)
665 .await
666 .unwrap()
667 };
668
669 builder = builder.directory(PathBuf::from(
670 tempfile::TempDir::with_prefix("mdb-temp-samples")
671 .unwrap()
672 .path(),
673 ));
674
675 if pem_file {
676 builder = builder.port(8443);
677 builder = builder
678 .tls(
679 "../../testdata/server_ca_cert.pem".into(),
680 "../../testdata/server_key.pem".into(),
681 )
682 .await
683 .unwrap();
684 } else {
685 builder = builder.port(8444);
686 builder = builder
687 .tls(
688 "../../testdata/server_cert.der".into(),
689 "../../testdata/server_key.der".into(),
690 )
691 .await
692 .unwrap();
693 }
694
695 let state = builder.into_state().await.unwrap();
696
697 state
698 .db_type
699 .set_password(ADMIN_UNAME, ADMIN_PASSWORD)
700 .await
701 .context("Failed to set admin password")
702 .unwrap();
703
704 let source_id = state
705 .db_type
706 .create_source("temp-source", None, None, Local::now(), true, Some(false))
707 .await
708 .unwrap();
709
710 state
711 .db_type
712 .add_group_to_source(0, source_id)
713 .await
714 .unwrap();
715
716 let token = state
717 .db_type
718 .authenticate(ADMIN_UNAME, ADMIN_PASSWORD)
719 .await
720 .unwrap();
721
722 (state, source_id, token)
723 }
724
725 async fn get_key(state: Arc<State>) -> String {
726 TRACING.call_once(init_tracing);
727
728 let key_request = serde_json::to_string(&GetAPIKeyRequest {
729 user: ADMIN_UNAME.into(),
730 password: ADMIN_PASSWORD.into(),
731 })
732 .context("Failed to convert API key request to JSON")
733 .unwrap();
734
735 let request = Request::builder()
736 .method("POST")
737 .uri(malwaredb_api::USER_LOGIN_URL)
738 .header(CONTENT_TYPE, "application/json")
739 .body(Body::from(key_request))
740 .unwrap();
741
742 let response = app(state)
743 .oneshot(request)
744 .await
745 .context("failed to send/receive login request")
746 .unwrap();
747
748 assert_eq!(response.status(), StatusCode::OK);
749 let bytes = response
750 .into_body()
751 .collect()
752 .await
753 .expect("failed to collect response body to bytes")
754 .to_bytes();
755 let json_response = String::from_utf8(bytes.to_ascii_lowercase())
756 .context("failed to convert response to string")
757 .unwrap();
758
759 let response: ServerResponse<GetAPIKeyResponse> = serde_json::from_str(&json_response)
760 .context("failed to convert json response to object")
761 .unwrap();
762
763 if let ServerResponse::Success(response) = response {
764 let key = response.key.clone();
765 assert_eq!(key.len(), 64);
766
767 key
768 } else {
769 panic!("failed to get API key response")
770 }
771 }
772
773 #[tokio::test]
774 async fn about_self() {
775 let (state, _) = state(false, false).await;
776 let api_key = get_key(state.clone()).await;
777
778 let request = Request::builder()
779 .method("GET")
780 .uri(malwaredb_api::USER_INFO_URL)
781 .header(MDB_API_HEADER, &api_key)
782 .body(Body::empty())
783 .unwrap();
784
785 let response = app(state.clone())
786 .oneshot(request)
787 .await
788 .context("failed to send/receive login request")
789 .unwrap();
790
791 assert_eq!(response.status(), StatusCode::OK);
792 let bytes = response
793 .into_body()
794 .collect()
795 .await
796 .expect("failed to collect response body to bytes")
797 .to_bytes();
798 let json_response = String::from_utf8(bytes.to_ascii_lowercase())
799 .context("failed to convert response to string")
800 .unwrap();
801
802 let response: ServerResponse<GetUserInfoResponse> = serde_json::from_str(&json_response)
803 .context("failed to convert json response to object")
804 .unwrap();
805
806 let response = response.unwrap();
807 assert_eq!(response.id, 0);
808 assert!(response.is_admin);
809 assert!(!response.is_readonly);
810 assert_eq!(response.username, "admin");
811
812 let request = Request::builder()
814 .method("GET")
815 .uri(malwaredb_api::LIST_LABELS_URL)
816 .header(MDB_API_HEADER, &api_key)
817 .body(Body::empty())
818 .unwrap();
819
820 let response = app(state)
821 .oneshot(request)
822 .await
823 .context("failed to send/receive login request")
824 .unwrap();
825
826 assert_eq!(response.status(), StatusCode::OK);
827 let bytes = response
828 .into_body()
829 .collect()
830 .await
831 .expect("failed to collect response body to bytes")
832 .to_bytes();
833 let json_response = String::from_utf8(bytes.to_ascii_lowercase())
834 .context("failed to convert response to string")
835 .unwrap();
836
837 let response: ServerResponse<Labels> = serde_json::from_str(&json_response)
838 .context("failed to convert json response to object")
839 .unwrap();
840
841 let response = response.unwrap();
842 assert!(response.is_empty());
843 }
844
845 #[rstest]
846 #[case::elf_encrypt_cart(include_bytes!("../../../types/testdata/elf/elf_haiku_x86.cart"), false, true, true, false)]
847 #[case::pe32(include_bytes!("../../../types/testdata/exe/pe64_win32_gui_x86_64_gnu.exe"), false, false, false, false)]
848 #[case::pdf_encrypt(include_bytes!("../../../types/testdata/pdf/test.pdf"), false, true, false, false)]
849 #[case::rtf(include_bytes!("../../../types/testdata/rtf/hello.rtf"), false, false, false, false)]
850 #[case::elf_compress_encrypt(include_bytes!("../../../types/testdata/elf/elf_haiku_x86"), true, true, false, false)]
851 #[case::pe32_compress(include_bytes!("../../../types/testdata/exe/pe64_win32_gui_x86_64_gnu.exe"), true, false, false, false)]
852 #[case::pdf_compress_encrypt(include_bytes!("../../../types/testdata/pdf/test.pdf"), true, true, false, false)]
853 #[case::rtf_compress(include_bytes!("../../../types/testdata/rtf/hello.rtf"), true, false, false, false)]
854 #[case::icon_unknown_type_proxy(include_bytes!("../../../../MDB_Logo.ico"), false, false, false, true)]
855 #[tokio::test]
856 async fn submit_sample(
857 #[case] contents: &[u8],
858 #[case] compress: bool,
859 #[case] encrypt: bool,
860 #[case] cart: bool,
861 #[case] should_fail: bool,
862 ) {
863 let (state, source_id) = state(compress, encrypt).await;
864 let api_key = get_key(state.clone()).await;
865
866 let file_contents_b64 = general_purpose::STANDARD.encode(contents);
867 let mut hasher = Sha256::new();
868 hasher.update(contents);
869 let sha256 = hex::encode(hasher.finalize());
870
871 let upload = serde_json::to_string(&NewSampleB64 {
872 file_name: "some_sample".into(),
873 source_id,
874 file_contents_b64,
875 sha256: sha256.clone(),
876 })
877 .context("failed to create upload structure")
878 .unwrap();
879
880 let request = Request::builder()
881 .method("POST")
882 .uri(malwaredb_api::UPLOAD_SAMPLE_JSON_URL)
883 .header(CONTENT_TYPE, "application/json")
884 .header(MDB_API_HEADER, &api_key)
885 .body(Body::from(upload))
886 .unwrap();
887
888 let response = app(state.clone())
889 .oneshot(request)
890 .await
891 .context("failed to send/receive upload request/response")
892 .unwrap();
893
894 if should_fail {
895 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
896 return;
897 }
898
899 assert_eq!(response.status(), StatusCode::OK);
900
901 let sha256 = if cart {
902 let mut input_buffer = Cursor::new(contents);
905 let mut output_buffer = Cursor::new(vec![]);
906 let (_, footer) =
907 cart_container::unpack_stream(&mut input_buffer, &mut output_buffer, None)
908 .expect("failed to decode CaRT file");
909 let footer = footer.expect("CaRT should have had a footer");
910 let sha256 = footer
911 .get("sha256")
912 .expect("CaRT footer should have had an entry for SHA-256")
913 .to_string();
914 sha256.replace('"', "") } else {
916 sha256
917 };
918
919 if let Some(dir) = &state.directory {
920 let mut sample_path = dir.clone();
921 sample_path.push(format!(
922 "{}/{}/{}/{}",
923 &sha256[0..2],
924 &sha256[2..4],
925 &sha256[4..6],
926 sha256
927 ));
928 eprintln!("Submitted sample should exist at {sample_path:?}.");
929 assert!(sample_path.exists());
930
931 if compress {
932 let sample_size_on_disk = sample_path.metadata().unwrap().len();
933 eprintln!("Original size: {}, compressed: {}", contents.len(), sample_size_on_disk);
934 assert!(sample_size_on_disk < contents.len() as u64);
935 }
936 } else {
937 panic!("Directory was set for the state, but is now `None`");
938 }
939
940 let request = Request::builder()
941 .method("GET")
942 .uri(format!("{}/{sha256}", malwaredb_api::SAMPLE_REPORT_URL))
943 .header(MDB_API_HEADER, &api_key)
944 .body(Body::empty())
945 .unwrap();
946
947 let response = app(state.clone())
948 .oneshot(request)
949 .await
950 .context("failed to send/receive upload request/response")
951 .unwrap();
952
953 println!("Response headers: {:?}", response.headers());
954
955 let bytes = response
956 .into_body()
957 .collect()
958 .await
959 .expect("failed to collect response body to bytes")
960 .to_bytes();
961 let json_response = String::from_utf8(bytes.to_ascii_lowercase())
962 .context("failed to convert response to string")
963 .unwrap();
964
965 let report: ServerResponse<Report> = serde_json::from_str(&json_response)
966 .context("failed to convert json response to object")
967 .unwrap();
968 let report = report.unwrap();
969
970 assert_eq!(report.sha256, sha256);
971 println!("Report: {report}");
972
973 let request = Request::builder()
974 .method("GET")
975 .uri(format!("{}/{sha256}", malwaredb_api::DOWNLOAD_SAMPLE_CART_URL))
976 .header(MDB_API_HEADER, api_key.clone())
977 .body(Body::empty())
978 .unwrap();
979
980 let response = app(state.clone())
981 .oneshot(request)
982 .await
983 .context("failed to send/receive upload request/response for CaRT")
984 .unwrap();
985
986 println!("CaRT Response headers: {:?}", response.headers());
987
988 let bytes = response
989 .into_body()
990 .collect()
991 .await
992 .expect("failed to collect response body to bytes")
993 .to_bytes();
994
995 let bytes = bytes.to_vec();
996 let bytes_input = Cursor::new(bytes);
997 let output = Cursor::new(vec![]);
998 match cart_container::unpack_stream(bytes_input, output, None) {
999 Ok((header, _)) => {
1000 let header = header.unwrap();
1001 assert_eq!(
1002 header.get("sha384"),
1003 Some(&serde_json::to_value(report.sha384).unwrap())
1004 );
1005 }
1006 Err(e) => panic!("{e}"),
1007 }
1008
1009 #[cfg(feature = "admin")]
1010 {
1011 use std::sync::atomic::{AtomicBool, AtomicU64};
1012
1013 let (new_key_id, keys) = {
1014 let new_key = FileEncryption::from(EncryptionOption::AES128);
1015 let new_key_id = state
1016 .db_type
1017 .add_file_encryption_key(&new_key)
1018 .await
1019 .unwrap();
1020 let mut keys = state.keys.clone();
1021 keys.insert(new_key_id, new_key);
1022 (new_key_id, keys)
1023 };
1024
1025 let state = State {
1026 port: state.port,
1027 directory: state.directory.clone(),
1028 max_upload: state.max_upload,
1029 ip: state.ip,
1030 db_type: state.db_type.clone(),
1031 started: state.started,
1032 db_config: crate::db::MDBConfig {
1033 name: state.db_config.name.clone(),
1034 compression: !state.db_config.compression,
1035 send_samples_to_vt: false,
1036 keep_unknown_files: false,
1037 default_key: Some(new_key_id),
1038 #[cfg(feature = "anonymous")]
1039 anonymous_uid: None,
1040 },
1041 keys,
1042 #[cfg(feature = "vt")]
1043 vt_client: state.vt_client.clone(),
1044 tls_config: state.tls_config.clone(),
1045 mdns: state.mdns.clone(),
1046 };
1047
1048 let rewrite_counter = Arc::new(AtomicU64::new(0));
1049 let stop_signal = Arc::new(AtomicBool::new(false));
1050
1051 state
1052 .rewrite_files(rewrite_counter.clone(), stop_signal)
1053 .await
1054 .unwrap();
1055 eprintln!(
1056 "Rewrote {} files",
1057 rewrite_counter.load(std::sync::atomic::Ordering::Relaxed)
1058 );
1059
1060 let mut sample_path = state.directory.unwrap().clone();
1061 sample_path.push(format!(
1062 "{}/{}/{}/{}",
1063 &sha256[0..2],
1064 &sha256[2..4],
1065 &sha256[4..6],
1066 sha256
1067 ));
1068 eprintln!("Submitted sample should exist at {sample_path:?}.");
1069 assert!(sample_path.exists());
1070
1071 let contents = fs::read(&sample_path).unwrap();
1072 let sha256_disk = hex::encode(Sha256::digest(&contents));
1073 assert_ne!(sha256_disk, sha256, "New key added so disk hash should never match");
1074 }
1075 }
1076
1077 #[rstest]
1082 #[case::ssl_pem_sqlite(true, false)]
1083 #[case::ssl_der_sqlite(false, false)]
1084 #[ignore = "don't run this in CI"]
1085 #[case::ssl_der_postgres(false, true)]
1086 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1087 async fn client_integration(#[case] pem: bool, #[case] postgres: bool) {
1088 TRACING.call_once(init_tracing);
1089
1090 let (state, source_id, token) = state_and_token(pem, postgres).await;
1093 let state_port = state.port; let server = tokio::spawn(async move {
1096 state
1097 .serve()
1098 .await
1099 .expect("MalwareDB failed to .serve() in tokio::spawn()");
1100 });
1101 assert!(!server.is_finished());
1102
1103 tokio::time::sleep(std::time::Duration::new(1, 0)).await;
1105
1106 println!("SemVer of MalwareDB: {:?}", *crate::MDB_VERSION_SEMVER);
1107 assert_eq!(
1108 *malwaredb_client::MDB_VERSION_SEMVER,
1109 *crate::MDB_VERSION_SEMVER,
1110 "SemVer parsing of MDB version failed"
1111 );
1112
1113 let mdb_client = malwaredb_client::MdbClient::new(
1114 format!("https://127.0.0.1:{state_port}"),
1115 token.clone(),
1116 Some("../../testdata/ca_cert.pem".into()),
1117 )
1118 .unwrap();
1119
1120 assert!(!mdb_client.supported_types().await.unwrap().types.is_empty());
1121 assert!(mdb_client.server_info().await.is_ok());
1122
1123 let start = Instant::now();
1124 assert!(
1125 mdb_client
1126 .submit(SAMPLE_BYTES, String::from("elf_haiku_x86"), source_id)
1127 .await
1128 .context("failed to upload test file")
1129 .unwrap()
1130 );
1131 let duration = start.elapsed();
1132 println!("Initial upload and database record creation via base64 took {duration:?}");
1133
1134 let start = Instant::now();
1135 mdb_client
1136 .submit(SAMPLE_BYTES, String::from("elf_haiku_x86"), source_id)
1137 .await
1138 .context("failed to upload test file")
1139 .unwrap();
1140 let duration = start.elapsed();
1141 println!("Upload again via base64 took {duration:?}");
1142
1143 let start = Instant::now();
1144 mdb_client
1145 .submit_as_cbor(SAMPLE_BYTES, String::from("elf_haiku_x86"), source_id)
1146 .await
1147 .context("failed to upload test file")
1148 .unwrap();
1149 let duration = start.elapsed();
1150 println!("Upload again via cbor took {duration:?}");
1151
1152 let report = mdb_client
1153 .report("de10ba5e5402b46ea975b5cb8a45eb7df9e81dc81012fd4efd145ed2dce3a740")
1154 .await
1155 .expect("failed to get report for file just submitted");
1156 assert_eq!(report.md5, "82123011556b0e68801bee7bd71bb345");
1157
1158 let similar = mdb_client
1159 .similar(SAMPLE_BYTES)
1160 .await
1161 .expect("failed to query for files similar to what was just submitted");
1162 assert_eq!(similar.results.len(), 1);
1163
1164 let search = mdb_client
1165 .partial_search(
1166 Some((PartialHashSearchType::MD5, String::from(&report.md5[0..10]))),
1167 None,
1168 PartialHashSearchType::Any,
1169 10,
1170 )
1171 .await
1172 .unwrap();
1173 assert_eq!(search.hashes.len(), 1);
1174
1175 let search = mdb_client
1176 .partial_search(
1177 Some((PartialHashSearchType::Any, "AAAA".into())),
1178 None,
1179 PartialHashSearchType::Any,
1180 10,
1181 )
1182 .await
1183 .unwrap();
1184 assert!(search.hashes.is_empty());
1185
1186 #[cfg(feature = "yara")]
1187 {
1188 let elf_yara = "rule elf_file {
1189 condition:
1190 uint32(0) == 0x464c457f
1191 }";
1192
1193 let mut counter = 0;
1194 let mut successful = false;
1195
1196 let yara_result = mdb_client.yara_search(elf_yara).await.unwrap();
1197 while counter < 5 {
1198 if let Ok(yara_response) = mdb_client.yara_result(yara_result.uuid).await
1199 && !yara_response.results.is_empty()
1200 {
1201 println!("Yara search upload response: {:?}", yara_response.results);
1202 assert_eq!(yara_response.results.get("elf_file").unwrap().len(), 1);
1203 assert_eq!(yara_response.results.len(), 1);
1204 successful = true;
1205 break;
1206 }
1207 tokio::time::sleep(std::time::Duration::new(2, 0)).await;
1208 counter += 1;
1209 }
1210
1211 assert!(successful);
1212 }
1213
1214 mdb_client.reset_key().await.expect("failed to reset key");
1215 server.abort();
1216 }
1217
1218 #[allow(clippy::too_many_lines)]
1219 #[test]
1220 #[ignore = "don't run this in CI"]
1221 fn client_integration_blocking() {
1222 TRACING.call_once(init_tracing);
1223 let token = Arc::new(RwLock::new(String::new()));
1224 let token_clone = token.clone();
1225
1226 let thread = std::thread::spawn(move || {
1232 let rt = tokio::runtime::Builder::new_multi_thread()
1233 .enable_all()
1234 .build()
1235 .unwrap();
1236
1237 let mut db_file = env::temp_dir();
1238 db_file.push(format!("testing_sqlite_{}.db", Uuid::new_v4()));
1239 if std::path::Path::new(&db_file).exists() {
1240 fs::remove_file(&db_file)
1241 .context(format!("failed to delete old SQLite file {db_file:?}"))
1242 .unwrap();
1243 }
1244
1245 let (db_type, db_config) = rt.block_on(async {
1246 let db_type =
1247 DatabaseType::from_string(&format!("file:{}", db_file.to_str().unwrap()), None)
1248 .await
1249 .context(format!("failed to create SQLite instance for {db_file:?}"))
1250 .unwrap();
1251
1252 let db_config = db_type.get_config().await.unwrap();
1253 (db_type, db_config)
1254 });
1255
1256 let state = State {
1257 port: 9090,
1258 directory: Some(
1259 tempfile::TempDir::with_prefix("mdb-temp-samples")
1260 .unwrap()
1261 .path()
1262 .into(),
1263 ),
1264 max_upload: 10 * 1024 * 1024,
1265 ip: "127.0.0.1".parse().unwrap(),
1266 db_type: Arc::new(db_type),
1267 db_config,
1268 keys: HashMap::default(),
1269 started: SystemTime::now(),
1270 #[cfg(feature = "vt")]
1271 vt_client: None,
1272 tls_config: None,
1273 mdns: None,
1274 };
1275
1276 rt.block_on(async {
1277 state
1278 .db_type
1279 .set_password(ADMIN_UNAME, ADMIN_PASSWORD)
1280 .await
1281 .context("Failed to set admin password")
1282 .unwrap();
1283
1284 let source_id = state
1285 .db_type
1286 .create_source("temp-source", None, None, Local::now(), true, Some(false))
1287 .await
1288 .unwrap();
1289
1290 state
1291 .db_type
1292 .add_group_to_source(0, source_id)
1293 .await
1294 .unwrap();
1295
1296 let token_string = state
1297 .db_type
1298 .authenticate(ADMIN_UNAME, ADMIN_PASSWORD)
1299 .await
1300 .unwrap();
1301
1302 if let Ok(mut token_lock) = token_clone.write() {
1303 *token_lock = token_string;
1304 }
1305
1306 state
1307 .serve()
1308 .await
1309 .expect("MalwareDB failed to .serve() in tokio::spawn()");
1310 });
1311 });
1312 std::thread::sleep(std::time::Duration::from_secs(1));
1313
1314 let mdb_client = malwaredb_client::blocking::MdbClient::new(
1315 String::from("http://127.0.0.1:9090"),
1316 token.read().unwrap().clone(),
1317 None,
1318 )
1319 .unwrap();
1320
1321 let types = match mdb_client.supported_types() {
1322 Ok(types) => types,
1323 Err(e) => panic!("{e}"),
1324 };
1325 assert!(!types.types.is_empty());
1326
1327 assert!(
1328 mdb_client
1329 .submit(SAMPLE_BYTES, String::from("elf_haiku_x86"), 1)
1330 .context("failed to upload test file")
1331 .unwrap()
1332 );
1333
1334 let report = mdb_client
1335 .report("de10ba5e5402b46ea975b5cb8a45eb7df9e81dc81012fd4efd145ed2dce3a740")
1336 .expect("failed to get report for file just submitted");
1337 assert_eq!(report.md5, "82123011556b0e68801bee7bd71bb345");
1338
1339 let similar = mdb_client
1340 .similar(SAMPLE_BYTES)
1341 .expect("failed to query for files similar to what was just submitted");
1342 assert_eq!(similar.results.len(), 1);
1343
1344 let search = mdb_client
1345 .partial_search(
1346 Some((PartialHashSearchType::Any, "AAAA".into())),
1347 None,
1348 PartialHashSearchType::Any,
1349 10,
1350 )
1351 .unwrap();
1352 assert!(search.hashes.is_empty());
1353
1354 mdb_client.reset_key().expect("failed to reset key");
1355 drop(thread); }
1357}