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