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
213pub const SUPPORTED_FILE_TYPES_URL: &str = "/v1/server/types";
215
216#[derive(Clone, Debug, Deserialize, Serialize)]
218pub struct SupportedFileType {
219 pub name: String,
221
222 pub magic: Vec<String>,
224
225 pub is_executable: bool,
227
228 pub description: Option<String>,
230}
231
232#[derive(Clone, Debug, Deserialize, Serialize)]
234pub struct SupportedFileTypes {
235 pub types: Vec<SupportedFileType>,
237
238 pub message: Option<String>,
240}
241
242pub const LIST_SOURCES_URL: &str = "/v1/sources/list";
244
245#[derive(Clone, Debug, Deserialize, Serialize)]
247pub struct SourceInfo {
248 pub id: u32,
250
251 pub name: String,
253
254 pub description: Option<String>,
256
257 pub url: Option<String>,
259
260 pub first_acquisition: DateTime<Utc>,
262
263 pub malicious: Option<bool>,
265}
266
267#[derive(Clone, Debug, Deserialize, Serialize)]
269pub struct Sources {
270 pub sources: Vec<SourceInfo>,
272
273 pub message: Option<String>,
275}
276
277pub const UPLOAD_SAMPLE_JSON_URL: &str = "/v1/samples/json/upload";
279
280pub const UPLOAD_SAMPLE_CBOR_URL: &str = "/v1/samples/cbor/upload";
282
283#[derive(Clone, Debug, Deserialize, Serialize)]
285pub struct NewSampleB64 {
286 pub file_name: String,
289
290 pub source_id: u32,
292
293 pub file_contents_b64: String,
295
296 pub sha256: String,
298}
299
300#[derive(Clone, Debug, Deserialize, Serialize)]
302pub struct NewSampleBytes {
303 pub file_name: String,
306
307 pub source_id: u32,
309
310 pub file_contents: Vec<u8>,
312
313 pub sha256: String,
315}
316
317pub const DOWNLOAD_SAMPLE_URL: &str = "/v1/samples/download";
321
322pub const DOWNLOAD_SAMPLE_CART_URL: &str = "/v1/samples/download/cart";
326
327pub const SAMPLE_REPORT_URL: &str = "/v1/samples/report";
330
331#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
333pub struct VirusTotalSummary {
334 pub hits: u32,
336
337 pub total: u32,
339
340 #[serde(default)]
342 pub detail: Option<serde_json::Value>,
343
344 #[serde(default, with = "ts_seconds_option")]
346 pub last_analysis_date: Option<DateTime<Utc>>,
347}
348
349#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
352pub struct Report {
353 pub md5: String,
355
356 pub sha1: String,
358
359 pub sha256: String,
361
362 pub sha384: String,
364
365 pub sha512: String,
367
368 pub lzjd: Option<String>,
371
372 pub tlsh: Option<String>,
375
376 pub ssdeep: Option<String>,
379
380 pub humanhash: Option<String>,
383
384 pub filecommand: Option<String>,
387
388 pub bytes: u64,
390
391 pub size: String,
393
394 pub entropy: f32,
396
397 #[serde(default)]
400 pub vt: Option<VirusTotalSummary>,
401}
402
403impl Display for Report {
404 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
405 writeln!(f, "Size: {} bytes, or {}", self.bytes, self.size)?;
406 writeln!(f, "Entropy: {}", self.entropy)?;
407 if let Some(filecmd) = &self.filecommand {
408 writeln!(f, "File command: {filecmd}")?;
409 }
410 if let Some(vt) = &self.vt {
411 writeln!(f, "VT Hits: {}/{}", vt.hits, vt.total)?;
412 }
413 writeln!(f, "MD5: {}", self.md5)?;
414 writeln!(f, "SHA-1: {}", self.sha1)?;
415 writeln!(f, "SHA256: {}", self.sha256)
416 }
417}
418
419pub const SIMILAR_SAMPLES_URL: &str = "/v1/samples/similar";
421
422#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
424#[non_exhaustive]
425pub enum SimilarityHashType {
426 SSDeep,
428
429 LZJD,
431
432 TLSH,
434
435 PEHash,
437
438 ImportHash,
440
441 FuzzyImportHash,
443}
444
445impl SimilarityHashType {
446 #[must_use]
450 pub fn get_table_field_simfunc(&self) -> (&'static str, Option<&'static str>) {
451 match self {
452 SimilarityHashType::SSDeep => ("file.ssdeep", Some("fuzzy_hash_compare")),
453 SimilarityHashType::LZJD => ("file.lzjd", Some("lzjd_compare")),
454 SimilarityHashType::TLSH => ("file.tlsh", Some("tlsh_compare")),
455 SimilarityHashType::PEHash => ("executable.pehash", None),
456 SimilarityHashType::ImportHash => ("executable.importhash", None),
457 SimilarityHashType::FuzzyImportHash => {
458 ("executable.importhashfuzzy", Some("fuzzy_hash_compare"))
459 }
460 }
461 }
462}
463
464impl Display for SimilarityHashType {
465 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
466 match self {
467 SimilarityHashType::SSDeep => write!(f, "SSDeep"),
468 SimilarityHashType::LZJD => write!(f, "LZJD"),
469 SimilarityHashType::TLSH => write!(f, "TLSH"),
470 SimilarityHashType::PEHash => write!(f, "PeHash"),
471 SimilarityHashType::ImportHash => write!(f, "Import Hash (IMPHASH)"),
472 SimilarityHashType::FuzzyImportHash => write!(f, "Fuzzy Import hash"),
473 }
474 }
475}
476
477#[derive(Clone, Debug, Deserialize, Serialize)]
479pub struct SimilarSamplesRequest {
480 pub hashes: Vec<(SimilarityHashType, String)>,
482}
483
484#[derive(Clone, Debug, Deserialize, Serialize)]
486pub struct SimilarSample {
487 pub sha256: String,
489
490 pub algorithms: Vec<(SimilarityHashType, f32)>,
492}
493
494#[derive(Clone, Debug, Deserialize, Serialize)]
496pub struct SimilarSamplesResponse {
497 pub results: Vec<SimilarSample>,
499
500 pub message: Option<String>,
502}
503
504pub const SEARCH_URL: &str = "/v1/search";
506
507#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
509pub enum SearchType {
510 Continuation(uuid::Uuid),
512
513 Search(SearchRequestParameters),
515}
516
517#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
524pub struct SearchRequestParameters {
525 pub partial_hash: Option<(PartialHashSearchType, String)>,
527
528 pub file_name: Option<String>,
530
531 pub limit: u32,
533
534 pub file_type: Option<String>,
536
537 pub magic: Option<String>,
539
540 pub labels: Option<Vec<String>>,
542
543 pub response: PartialHashSearchType,
546}
547
548impl SearchRequestParameters {
549 #[must_use]
554 #[inline]
555 pub fn is_valid(&self) -> bool {
556 if self.limit == 0 {
557 return false;
558 }
559
560 if let Some((_hash_type, partial_hash)) = &self.partial_hash {
561 let hex = hex::decode(partial_hash);
562 return hex.is_ok();
563 }
564
565 self.partial_hash.is_some()
566 || self.file_name.is_some()
567 || self.file_type.is_some()
568 || self.magic.is_some()
569 || self.labels.is_some()
570 }
571}
572
573impl Default for SearchRequestParameters {
575 fn default() -> Self {
576 Self {
577 partial_hash: None,
578 file_name: None,
579 limit: 100,
580 labels: None,
581 file_type: None,
582 magic: None,
583 response: PartialHashSearchType::default(),
584 }
585 }
586}
587
588#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
591pub struct SearchRequest {
592 pub search: SearchType,
594}
595
596impl SearchRequest {
597 #[must_use]
601 #[inline]
602 pub fn is_valid(&self) -> bool {
603 if let SearchType::Search(search) = &self.search {
604 search.is_valid()
605 } else {
606 true
607 }
608 }
609}
610
611#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
613pub struct SearchResponse {
614 pub hashes: Vec<String>,
616
617 pub pagination: Option<uuid::Uuid>,
619
620 pub total_results: u64,
622
623 pub message: Option<String>,
625}
626
627#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
629pub enum PartialHashSearchType {
630 Any,
632
633 MD5,
635
636 SHA1,
638
639 #[default]
641 SHA256,
642
643 SHA384,
645
646 SHA512,
648}
649
650impl Display for PartialHashSearchType {
651 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
652 match self {
653 PartialHashSearchType::Any => write!(f, "any"),
654 PartialHashSearchType::MD5 => write!(f, "md5"),
655 PartialHashSearchType::SHA1 => write!(f, "sha1"),
656 PartialHashSearchType::SHA256 => write!(f, "sha256"),
657 PartialHashSearchType::SHA384 => write!(f, "sha384"),
658 PartialHashSearchType::SHA512 => write!(f, "sha512"),
659 }
660 }
661}
662
663impl TryInto<PartialHashSearchType> for &str {
664 type Error = String;
665
666 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
667 match self {
668 "any" => Ok(PartialHashSearchType::Any),
669 "md5" => Ok(PartialHashSearchType::MD5),
670 "sha1" => Ok(PartialHashSearchType::SHA1),
671 "sha256" => Ok(PartialHashSearchType::SHA256),
672 "sha384" => Ok(PartialHashSearchType::SHA384),
673 "sha512" => Ok(PartialHashSearchType::SHA512),
674 x => Err(format!("Invalid hash type {x}")),
675 }
676 }
677}
678
679impl TryInto<PartialHashSearchType> for Option<&str> {
680 type Error = String;
681
682 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
683 if let Some(hash) = self {
684 hash.try_into()
685 } else {
686 Ok(PartialHashSearchType::SHA256)
687 }
688 }
689}
690
691pub const YARA_SEARCH_URL: &str = "/v1/yara";
695
696#[derive(Clone, Debug, Deserialize, Serialize)]
698pub struct YaraSearchRequest {
699 pub rules: Vec<String>,
701
702 pub response: PartialHashSearchType,
705}
706
707#[derive(Clone, Debug, Deserialize, Serialize)]
709pub struct YaraSearchRequestResponse {
710 pub uuid: uuid::Uuid,
712}
713
714#[derive(Clone, Debug, Deserialize, Serialize)]
716pub struct YaraSearchResponse {
717 pub results: HashMap<String, Vec<HashType>>,
719}
720
721pub const LIST_LABELS_URL: &str = "/v1/labels";
723
724#[derive(Clone, Debug, Deserialize, Serialize)]
726pub struct Label {
727 pub id: u64,
729
730 pub name: String,
732
733 pub parent: Option<String>,
735}
736
737#[derive(Clone, Debug, Default, Deserialize, Serialize)]
739pub struct Labels(pub Vec<Label>);
740
741impl Labels {
743 #[must_use]
745 pub fn len(&self) -> usize {
746 self.0.len()
747 }
748
749 #[must_use]
751 pub fn is_empty(&self) -> bool {
752 self.0.is_empty()
753 }
754}
755
756impl Display for Labels {
757 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
758 if self.is_empty() {
759 return writeln!(f, "No labels.");
760 }
761 for label in &self.0 {
762 let parent = if let Some(parent) = &label.parent {
763 format!(", parent: {parent}")
764 } else {
765 String::new()
766 };
767 writeln!(f, "{}: {}{parent}", label.id, label.name)?;
768 }
769 Ok(())
770 }
771}