1#![doc = include_str!("../README.md")]
4#![deny(missing_docs)]
5#![deny(clippy::all)]
6#![deny(clippy::pedantic)]
7#![forbid(unsafe_code)]
8
9pub mod digest;
11
12use crate::digest::HashType;
13
14use std::collections::HashMap;
15use std::error::Error;
16use std::fmt::{Display, Formatter};
17
18use chrono::serde::ts_seconds_option;
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use zeroize::{Zeroize, ZeroizeOnDrop};
22
23pub const MDB_VERSION: &str = env!("CARGO_PKG_VERSION");
25
26pub const MDB_API_HEADER: &str = "mdb-api-key";
28
29pub const USER_LOGIN_URL: &str = "/v1/users/getkey";
31
32pub const MDNS_NAME: &str = "_malwaredb._tcp.local.";
34
35#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
37pub struct GetAPIKeyRequest {
38 pub user: String,
40
41 pub password: String,
43}
44
45pub const USER_LOGOUT_URL: &str = "/v1/users/clearkey";
47
48#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
51pub struct GetAPIKeyResponse {
52 pub key: String,
54
55 pub message: Option<String>,
57}
58
59#[derive(Clone, Debug, Deserialize, Serialize)]
68pub enum ServerResponse<D> {
69 #[serde(alias = "success")]
71 Success(D),
72
73 #[serde(alias = "error")]
75 Error(ServerError),
76}
77
78impl<D> ServerResponse<D> {
79 #[inline]
85 pub fn unwrap(self) -> D {
86 match self {
87 ServerResponse::Success(d) => d,
88 ServerResponse::Error(e) => panic!("forced ServerResponse::unwrap() on error: {e}"),
89 }
90 }
91
92 #[inline]
98 pub fn into_result(self) -> Result<D, ServerError> {
99 match self {
100 ServerResponse::Success(d) => Ok(d),
101 ServerResponse::Error(e) => Err(e),
102 }
103 }
104
105 #[inline]
107 pub const fn is_successful(&self) -> bool {
108 matches!(*self, ServerResponse::Success(_))
109 }
110
111 #[inline]
113 pub const fn is_err(&self) -> bool {
114 matches!(*self, ServerResponse::Error(_))
115 }
116}
117
118#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
120pub enum ServerError {
121 NoSamples,
123
124 NotFound,
126
127 ServerError,
129
130 Unauthorized,
132
133 Unsupported,
135}
136
137impl Display for ServerError {
138 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
139 match self {
140 ServerError::NoSamples => write!(f, "NoSamples"),
141 ServerError::NotFound => write!(f, "NotFound"),
142 ServerError::ServerError => write!(f, "ServerError"),
143 ServerError::Unauthorized => write!(f, "Unauthorized"),
144 ServerError::Unsupported => write!(f, "Unsupported"),
145 }
146 }
147}
148
149impl Error for ServerError {}
150
151pub const USER_INFO_URL: &str = "/v1/users/info";
153
154#[derive(Clone, Debug, Deserialize, Serialize)]
156pub struct GetUserInfoResponse {
157 pub id: u32,
159
160 pub username: String,
162
163 pub groups: Vec<String>,
165
166 pub sources: Vec<String>,
168
169 pub is_admin: bool,
171
172 pub created: DateTime<Utc>,
174
175 pub is_readonly: bool,
177}
178
179pub const SERVER_INFO_URL: &str = "/v1/server/info";
181
182#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
184pub struct ServerInfo {
185 pub os_name: String,
187
188 pub memory_used: String,
190
191 pub mdb_version: semver::Version,
193
194 pub db_version: String,
196
197 pub db_size: String,
199
200 pub num_samples: u64,
202
203 pub num_users: u32,
205
206 pub uptime: String,
208
209 pub instance_name: String,
211
212 pub vt_support: bool,
214
215 pub yara_enabled: bool,
217}
218
219pub const SUPPORTED_FILE_TYPES_URL: &str = "/v1/server/types";
221
222#[derive(Clone, Debug, Deserialize, Serialize)]
224pub struct SupportedFileType {
225 pub name: String,
227
228 pub magic: Vec<String>,
230
231 pub is_executable: bool,
233
234 pub description: Option<String>,
236}
237
238#[derive(Clone, Debug, Deserialize, Serialize)]
240pub struct SupportedFileTypes {
241 pub types: Vec<SupportedFileType>,
243
244 pub message: Option<String>,
246}
247
248pub const LIST_SOURCES_URL: &str = "/v1/sources/list";
250
251#[derive(Clone, Debug, Deserialize, Serialize)]
253pub struct SourceInfo {
254 pub id: u32,
256
257 pub name: String,
259
260 pub description: Option<String>,
262
263 pub url: Option<String>,
265
266 pub first_acquisition: DateTime<Utc>,
268
269 pub malicious: Option<bool>,
271}
272
273#[derive(Clone, Debug, Deserialize, Serialize)]
275pub struct Sources {
276 pub sources: Vec<SourceInfo>,
278
279 pub message: Option<String>,
281}
282
283pub const UPLOAD_SAMPLE_JSON_URL: &str = "/v1/samples/json/upload";
285
286pub const UPLOAD_SAMPLE_CBOR_URL: &str = "/v1/samples/cbor/upload";
288
289#[derive(Clone, Debug, Deserialize, Serialize)]
291pub struct NewSampleB64 {
292 pub file_name: String,
295
296 pub source_id: u32,
298
299 pub file_contents_b64: String,
301
302 pub sha256: String,
304}
305
306#[derive(Clone, Debug, Deserialize, Serialize)]
308pub struct NewSampleBytes {
309 pub file_name: String,
312
313 pub source_id: u32,
315
316 pub file_contents: Vec<u8>,
318
319 pub sha256: String,
321}
322
323pub const DOWNLOAD_SAMPLE_URL: &str = "/v1/samples/download";
327
328pub const DOWNLOAD_SAMPLE_CART_URL: &str = "/v1/samples/download/cart";
332
333pub const SAMPLE_REPORT_URL: &str = "/v1/samples/report";
336
337#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
339pub struct VirusTotalSummary {
340 pub hits: u32,
342
343 pub total: u32,
345
346 #[serde(default)]
348 pub detail: Option<serde_json::Value>,
349
350 #[serde(default, with = "ts_seconds_option")]
352 pub last_analysis_date: Option<DateTime<Utc>>,
353}
354
355#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
358pub struct Report {
359 pub md5: String,
361
362 pub sha1: String,
364
365 pub sha256: String,
367
368 pub sha384: String,
370
371 pub sha512: String,
373
374 pub lzjd: Option<String>,
377
378 pub tlsh: Option<String>,
381
382 pub ssdeep: Option<String>,
385
386 pub humanhash: Option<String>,
389
390 pub filecommand: Option<String>,
393
394 pub bytes: u64,
396
397 pub size: String,
399
400 pub entropy: f32,
402
403 #[serde(default)]
406 pub vt: Option<VirusTotalSummary>,
407}
408
409impl Display for Report {
410 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
411 writeln!(f, "Size: {} bytes, or {}", self.bytes, self.size)?;
412 writeln!(f, "Entropy: {}", self.entropy)?;
413 if let Some(filecmd) = &self.filecommand {
414 writeln!(f, "File command: {filecmd}")?;
415 }
416 if let Some(vt) = &self.vt {
417 writeln!(f, "VT Hits: {}/{}", vt.hits, vt.total)?;
418 }
419 writeln!(f, "MD5: {}", self.md5)?;
420 writeln!(f, "SHA-1: {}", self.sha1)?;
421 writeln!(f, "SHA256: {}", self.sha256)
422 }
423}
424
425pub const SIMILAR_SAMPLES_URL: &str = "/v1/samples/similar";
427
428#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
430#[non_exhaustive]
431pub enum SimilarityHashType {
432 SSDeep,
434
435 LZJD,
437
438 TLSH,
440
441 PEHash,
443
444 ImportHash,
446
447 FuzzyImportHash,
449}
450
451impl SimilarityHashType {
452 #[must_use]
456 pub fn get_table_field_simfunc(&self) -> (&'static str, Option<&'static str>) {
457 match self {
458 SimilarityHashType::SSDeep => ("file.ssdeep", Some("fuzzy_hash_compare")),
459 SimilarityHashType::LZJD => ("file.lzjd", Some("lzjd_compare")),
460 SimilarityHashType::TLSH => ("file.tlsh", Some("tlsh_compare")),
461 SimilarityHashType::PEHash => ("executable.pehash", None),
462 SimilarityHashType::ImportHash => ("executable.importhash", None),
463 SimilarityHashType::FuzzyImportHash => {
464 ("executable.importhashfuzzy", Some("fuzzy_hash_compare"))
465 }
466 }
467 }
468}
469
470impl Display for SimilarityHashType {
471 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
472 match self {
473 SimilarityHashType::SSDeep => write!(f, "SSDeep"),
474 SimilarityHashType::LZJD => write!(f, "LZJD"),
475 SimilarityHashType::TLSH => write!(f, "TLSH"),
476 SimilarityHashType::PEHash => write!(f, "PeHash"),
477 SimilarityHashType::ImportHash => write!(f, "Import Hash (IMPHASH)"),
478 SimilarityHashType::FuzzyImportHash => write!(f, "Fuzzy Import hash"),
479 }
480 }
481}
482
483#[derive(Clone, Debug, Deserialize, Serialize)]
485pub struct SimilarSamplesRequest {
486 pub hashes: Vec<(SimilarityHashType, String)>,
488}
489
490#[derive(Clone, Debug, Deserialize, Serialize)]
492pub struct SimilarSample {
493 pub sha256: String,
495
496 pub algorithms: Vec<(SimilarityHashType, f32)>,
498}
499
500#[derive(Clone, Debug, Deserialize, Serialize)]
502pub struct SimilarSamplesResponse {
503 pub results: Vec<SimilarSample>,
505
506 pub message: Option<String>,
508}
509
510pub const SEARCH_URL: &str = "/v1/search";
512
513#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
515pub enum SearchType {
516 Continuation(uuid::Uuid),
518
519 Search(SearchRequestParameters),
521}
522
523#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
530pub struct SearchRequestParameters {
531 pub partial_hash: Option<(PartialHashSearchType, String)>,
533
534 pub file_name: Option<String>,
536
537 pub limit: u32,
539
540 pub file_type: Option<String>,
542
543 pub magic: Option<String>,
545
546 pub labels: Option<Vec<String>>,
548
549 pub response: PartialHashSearchType,
552}
553
554impl SearchRequestParameters {
555 #[must_use]
560 #[inline]
561 pub fn is_valid(&self) -> bool {
562 if self.limit == 0 {
563 return false;
564 }
565
566 if let Some((_hash_type, partial_hash)) = &self.partial_hash {
567 let hex = hex::decode(partial_hash);
568 return hex.is_ok();
569 }
570
571 self.partial_hash.is_some()
572 || self.file_name.is_some()
573 || self.file_type.is_some()
574 || self.magic.is_some()
575 || self.labels.is_some()
576 }
577}
578
579impl Default for SearchRequestParameters {
581 fn default() -> Self {
582 Self {
583 partial_hash: None,
584 file_name: None,
585 limit: 100,
586 labels: None,
587 file_type: None,
588 magic: None,
589 response: PartialHashSearchType::default(),
590 }
591 }
592}
593
594#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
597pub struct SearchRequest {
598 pub search: SearchType,
600}
601
602impl SearchRequest {
603 #[must_use]
607 #[inline]
608 pub fn is_valid(&self) -> bool {
609 if let SearchType::Search(search) = &self.search {
610 search.is_valid()
611 } else {
612 true
613 }
614 }
615}
616
617#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
619pub struct SearchResponse {
620 pub hashes: Vec<String>,
622
623 pub pagination: Option<uuid::Uuid>,
625
626 pub total_results: u64,
628
629 pub message: Option<String>,
631}
632
633#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
635pub enum PartialHashSearchType {
636 Any,
638
639 MD5,
641
642 SHA1,
644
645 #[default]
647 SHA256,
648
649 SHA384,
651
652 SHA512,
654}
655
656impl Display for PartialHashSearchType {
657 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
658 match self {
659 PartialHashSearchType::Any => write!(f, "any"),
660 PartialHashSearchType::MD5 => write!(f, "md5"),
661 PartialHashSearchType::SHA1 => write!(f, "sha1"),
662 PartialHashSearchType::SHA256 => write!(f, "sha256"),
663 PartialHashSearchType::SHA384 => write!(f, "sha384"),
664 PartialHashSearchType::SHA512 => write!(f, "sha512"),
665 }
666 }
667}
668
669impl TryInto<PartialHashSearchType> for &str {
670 type Error = String;
671
672 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
673 match self {
674 "any" => Ok(PartialHashSearchType::Any),
675 "md5" => Ok(PartialHashSearchType::MD5),
676 "sha1" => Ok(PartialHashSearchType::SHA1),
677 "sha256" => Ok(PartialHashSearchType::SHA256),
678 "sha384" => Ok(PartialHashSearchType::SHA384),
679 "sha512" => Ok(PartialHashSearchType::SHA512),
680 x => Err(format!("Invalid hash type {x}")),
681 }
682 }
683}
684
685impl TryInto<PartialHashSearchType> for Option<&str> {
686 type Error = String;
687
688 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
689 if let Some(hash) = self {
690 hash.try_into()
691 } else {
692 Ok(PartialHashSearchType::SHA256)
693 }
694 }
695}
696
697pub const YARA_SEARCH_URL: &str = "/v1/yara";
701
702#[derive(Clone, Debug, Deserialize, Serialize)]
704pub struct YaraSearchRequest {
705 pub rules: Vec<String>,
707
708 pub response: PartialHashSearchType,
711}
712
713#[derive(Clone, Debug, Deserialize, Serialize)]
715pub struct YaraSearchRequestResponse {
716 pub uuid: uuid::Uuid,
718}
719
720#[derive(Clone, Debug, Deserialize, Serialize)]
722pub struct YaraSearchResponse {
723 pub results: HashMap<String, Vec<HashType>>,
725}
726
727pub const LIST_LABELS_URL: &str = "/v1/labels";
729
730#[derive(Clone, Debug, Deserialize, Serialize)]
732pub struct Label {
733 pub id: u64,
735
736 pub name: String,
738
739 pub parent: Option<String>,
741}
742
743#[derive(Clone, Debug, Default, Deserialize, Serialize)]
745pub struct Labels(pub Vec<Label>);
746
747impl Labels {
749 #[must_use]
751 pub fn len(&self) -> usize {
752 self.0.len()
753 }
754
755 #[must_use]
757 pub fn is_empty(&self) -> bool {
758 self.0.is_empty()
759 }
760}
761
762impl Display for Labels {
763 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
764 if self.is_empty() {
765 return writeln!(f, "No labels.");
766 }
767 for label in &self.0 {
768 let parent = if let Some(parent) = &label.parent {
769 format!(", parent: {parent}")
770 } else {
771 String::new()
772 };
773 writeln!(f, "{}: {}{parent}", label.id, label.name)?;
774 }
775 Ok(())
776 }
777}