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 std::error::Error;
13use std::fmt::{Display, Formatter};
14
15use chrono::serde::ts_seconds_option;
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use zeroize::{Zeroize, ZeroizeOnDrop};
19
20pub const MDB_VERSION: &str = env!("CARGO_PKG_VERSION");
22
23pub const MDB_API_HEADER: &str = "mdb-api-key";
25
26pub const USER_LOGIN_URL: &str = "/v1/users/getkey";
28
29pub const MDNS_NAME: &str = "_malwaredb._tcp.local.";
31
32#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
34pub struct GetAPIKeyRequest {
35 pub user: String,
37
38 pub password: String,
40}
41
42pub const USER_LOGOUT_URL: &str = "/v1/users/clearkey";
44
45#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
48pub struct GetAPIKeyResponse {
49 pub key: String,
51
52 pub message: Option<String>,
54}
55
56#[derive(Clone, Debug, Deserialize, Serialize)]
65pub enum ServerResponse<D> {
66 #[serde(alias = "success")]
68 Success(D),
69
70 #[serde(alias = "error")]
72 Error(ServerError),
73}
74
75impl<D> ServerResponse<D> {
76 #[inline]
82 pub fn unwrap(self) -> D {
83 match self {
84 ServerResponse::Success(d) => d,
85 ServerResponse::Error(e) => panic!("forced ServerResponse::unwrap() on error: {e}"),
86 }
87 }
88
89 #[inline]
95 pub fn into_result(self) -> Result<D, ServerError> {
96 match self {
97 ServerResponse::Success(d) => Ok(d),
98 ServerResponse::Error(e) => Err(e),
99 }
100 }
101
102 #[inline]
104 pub const fn is_successful(&self) -> bool {
105 matches!(*self, ServerResponse::Success(_))
106 }
107
108 #[inline]
110 pub const fn is_err(&self) -> bool {
111 matches!(*self, ServerResponse::Error(_))
112 }
113}
114
115#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
117pub enum ServerError {
118 NoSamples,
120
121 NotFound,
123
124 ServerError,
126
127 Unauthorized,
129}
130
131impl Display for ServerError {
132 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133 match self {
134 ServerError::NoSamples => write!(f, "NoSamples"),
135 ServerError::NotFound => write!(f, "NotFound"),
136 ServerError::ServerError => write!(f, "ServerError"),
137 ServerError::Unauthorized => write!(f, "Unauthorized"),
138 }
139 }
140}
141
142impl Error for ServerError {}
143
144pub const USER_INFO_URL: &str = "/v1/users/info";
146
147#[derive(Clone, Debug, Deserialize, Serialize)]
149pub struct GetUserInfoResponse {
150 pub id: u32,
152
153 pub username: String,
155
156 pub groups: Vec<String>,
158
159 pub sources: Vec<String>,
161
162 pub is_admin: bool,
164
165 pub created: DateTime<Utc>,
167
168 pub is_readonly: bool,
170}
171
172pub const SERVER_INFO_URL: &str = "/v1/server/info";
174
175#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
177pub struct ServerInfo {
178 pub os_name: String,
180
181 pub memory_used: String,
183
184 pub mdb_version: semver::Version,
186
187 pub db_version: String,
189
190 pub db_size: String,
192
193 pub num_samples: u64,
195
196 pub num_users: u32,
198
199 pub uptime: String,
201
202 pub instance_name: String,
204}
205
206pub const SUPPORTED_FILE_TYPES_URL: &str = "/v1/server/types";
208
209#[derive(Clone, Debug, Deserialize, Serialize)]
211pub struct SupportedFileType {
212 pub name: String,
214
215 pub magic: Vec<String>,
217
218 pub is_executable: bool,
220
221 pub description: Option<String>,
223}
224
225#[derive(Clone, Debug, Deserialize, Serialize)]
227pub struct SupportedFileTypes {
228 pub types: Vec<SupportedFileType>,
230
231 pub message: Option<String>,
233}
234
235pub const LIST_SOURCES_URL: &str = "/v1/sources/list";
237
238#[derive(Clone, Debug, Deserialize, Serialize)]
240pub struct SourceInfo {
241 pub id: u32,
243
244 pub name: String,
246
247 pub description: Option<String>,
249
250 pub url: Option<String>,
252
253 pub first_acquisition: DateTime<Utc>,
255
256 pub malicious: Option<bool>,
258}
259
260#[derive(Clone, Debug, Deserialize, Serialize)]
262pub struct Sources {
263 pub sources: Vec<SourceInfo>,
265
266 pub message: Option<String>,
268}
269
270pub const UPLOAD_SAMPLE_JSON_URL: &str = "/v1/samples/json/upload";
272
273pub const UPLOAD_SAMPLE_CBOR_URL: &str = "/v1/samples/cbor/upload";
275
276#[derive(Clone, Debug, Deserialize, Serialize)]
278pub struct NewSampleB64 {
279 pub file_name: String,
282
283 pub source_id: u32,
285
286 pub file_contents_b64: String,
288
289 pub sha256: String,
291}
292
293#[derive(Clone, Debug, Deserialize, Serialize)]
295pub struct NewSampleBytes {
296 pub file_name: String,
299
300 pub source_id: u32,
302
303 pub file_contents: Vec<u8>,
305
306 pub sha256: String,
308}
309
310pub const DOWNLOAD_SAMPLE_URL: &str = "/v1/samples/download";
314
315pub const DOWNLOAD_SAMPLE_CART_URL: &str = "/v1/samples/download/cart";
319
320pub const SAMPLE_REPORT_URL: &str = "/v1/samples/report";
323
324#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
326pub struct VirusTotalSummary {
327 pub hits: u32,
329
330 pub total: u32,
332
333 #[serde(default)]
335 pub detail: Option<serde_json::Value>,
336
337 #[serde(default, with = "ts_seconds_option")]
339 pub last_analysis_date: Option<DateTime<Utc>>,
340}
341
342#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
345pub struct Report {
346 pub md5: String,
348
349 pub sha1: String,
351
352 pub sha256: String,
354
355 pub sha384: String,
357
358 pub sha512: String,
360
361 pub lzjd: Option<String>,
364
365 pub tlsh: Option<String>,
368
369 pub ssdeep: Option<String>,
372
373 pub humanhash: Option<String>,
376
377 pub filecommand: Option<String>,
380
381 pub bytes: u64,
383
384 pub size: String,
386
387 pub entropy: f32,
389
390 #[serde(default)]
393 pub vt: Option<VirusTotalSummary>,
394}
395
396impl Display for Report {
397 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
398 writeln!(f, "Size: {} bytes, or {}", self.bytes, self.size)?;
399 writeln!(f, "Entropy: {}", self.entropy)?;
400 if let Some(filecmd) = &self.filecommand {
401 writeln!(f, "File command: {filecmd}")?;
402 }
403 if let Some(vt) = &self.vt {
404 writeln!(f, "VT Hits: {}/{}", vt.hits, vt.total)?;
405 }
406 writeln!(f, "MD5: {}", self.md5)?;
407 writeln!(f, "SHA-1: {}", self.sha1)?;
408 writeln!(f, "SHA256: {}", self.sha256)
409 }
410}
411
412pub const SIMILAR_SAMPLES_URL: &str = "/v1/samples/similar";
414
415#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
417#[non_exhaustive]
418pub enum SimilarityHashType {
419 SSDeep,
421
422 LZJD,
424
425 TLSH,
427
428 PEHash,
430
431 ImportHash,
433
434 FuzzyImportHash,
436}
437
438impl SimilarityHashType {
439 #[must_use]
443 pub fn get_table_field_simfunc(&self) -> (&'static str, Option<&'static str>) {
444 match self {
445 SimilarityHashType::SSDeep => ("file.ssdeep", Some("fuzzy_hash_compare")),
446 SimilarityHashType::LZJD => ("file.lzjd", Some("lzjd_compare")),
447 SimilarityHashType::TLSH => ("file.tlsh", Some("tlsh_compare")),
448 SimilarityHashType::PEHash => ("executable.pehash", None),
449 SimilarityHashType::ImportHash => ("executable.importhash", None),
450 SimilarityHashType::FuzzyImportHash => {
451 ("executable.importhashfuzzy", Some("fuzzy_hash_compare"))
452 }
453 }
454 }
455}
456
457impl Display for SimilarityHashType {
458 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
459 match self {
460 SimilarityHashType::SSDeep => write!(f, "SSDeep"),
461 SimilarityHashType::LZJD => write!(f, "LZJD"),
462 SimilarityHashType::TLSH => write!(f, "TLSH"),
463 SimilarityHashType::PEHash => write!(f, "PeHash"),
464 SimilarityHashType::ImportHash => write!(f, "Import Hash (IMPHASH)"),
465 SimilarityHashType::FuzzyImportHash => write!(f, "Fuzzy Import hash"),
466 }
467 }
468}
469
470#[derive(Clone, Debug, Deserialize, Serialize)]
472pub struct SimilarSamplesRequest {
473 pub hashes: Vec<(SimilarityHashType, String)>,
475}
476
477#[derive(Clone, Debug, Deserialize, Serialize)]
479pub struct SimilarSample {
480 pub sha256: String,
482
483 pub algorithms: Vec<(SimilarityHashType, f32)>,
485}
486
487#[derive(Clone, Debug, Deserialize, Serialize)]
489pub struct SimilarSamplesResponse {
490 pub results: Vec<SimilarSample>,
492
493 pub message: Option<String>,
495}
496
497pub const SEARCH_URL: &str = "/v1/search";
499
500#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
502pub enum SearchType {
503 Continuation(uuid::Uuid),
505
506 Search(SearchRequestParameters),
508}
509
510#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
517pub struct SearchRequestParameters {
518 pub partial_hash: Option<(PartialHashSearchType, String)>,
520
521 pub file_name: Option<String>,
523
524 pub limit: u32,
526
527 pub file_type: Option<String>,
529
530 pub magic: Option<String>,
532
533 pub labels: Option<Vec<String>>,
535
536 pub response: PartialHashSearchType,
539}
540
541impl SearchRequestParameters {
542 #[must_use]
547 #[inline]
548 pub fn is_valid(&self) -> bool {
549 if self.limit == 0 {
550 return false;
551 }
552
553 if let Some((_hash_type, partial_hash)) = &self.partial_hash {
554 let hex = hex::decode(partial_hash);
555 return hex.is_ok();
556 }
557
558 self.partial_hash.is_some()
559 || self.file_name.is_some()
560 || self.file_type.is_some()
561 || self.magic.is_some()
562 || self.labels.is_some()
563 }
564}
565
566impl Default for SearchRequestParameters {
568 fn default() -> Self {
569 Self {
570 partial_hash: None,
571 file_name: None,
572 limit: 100,
573 labels: None,
574 file_type: None,
575 magic: None,
576 response: PartialHashSearchType::default(),
577 }
578 }
579}
580
581#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
584pub struct SearchRequest {
585 pub search: SearchType,
587}
588
589impl SearchRequest {
590 #[must_use]
594 #[inline]
595 pub fn is_valid(&self) -> bool {
596 if let SearchType::Search(search) = &self.search {
597 search.is_valid()
598 } else {
599 true
600 }
601 }
602}
603
604#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
606pub struct SearchResponse {
607 pub hashes: Vec<String>,
609
610 pub pagination: Option<uuid::Uuid>,
612
613 pub total_results: u64,
615
616 pub message: Option<String>,
618}
619
620#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
622pub enum PartialHashSearchType {
623 Any,
625
626 MD5,
628
629 SHA1,
631
632 #[default]
634 SHA256,
635
636 SHA384,
638
639 SHA512,
641}
642
643impl Display for PartialHashSearchType {
644 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
645 match self {
646 PartialHashSearchType::Any => write!(f, "any"),
647 PartialHashSearchType::MD5 => write!(f, "md5"),
648 PartialHashSearchType::SHA1 => write!(f, "sha1"),
649 PartialHashSearchType::SHA256 => write!(f, "sha256"),
650 PartialHashSearchType::SHA384 => write!(f, "sha384"),
651 PartialHashSearchType::SHA512 => write!(f, "sha512"),
652 }
653 }
654}
655
656impl TryInto<PartialHashSearchType> for &str {
657 type Error = String;
658
659 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
660 match self {
661 "any" => Ok(PartialHashSearchType::Any),
662 "md5" => Ok(PartialHashSearchType::MD5),
663 "sha1" => Ok(PartialHashSearchType::SHA1),
664 "sha256" => Ok(PartialHashSearchType::SHA256),
665 "sha384" => Ok(PartialHashSearchType::SHA384),
666 "sha512" => Ok(PartialHashSearchType::SHA512),
667 x => Err(format!("Invalid hash type {x}")),
668 }
669 }
670}
671
672impl TryInto<PartialHashSearchType> for Option<&str> {
673 type Error = String;
674
675 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
676 if let Some(hash) = self {
677 hash.try_into()
678 } else {
679 Ok(PartialHashSearchType::SHA256)
680 }
681 }
682}
683
684pub const LIST_LABELS_URL: &str = "/v1/labels";
686
687#[derive(Clone, Debug, Deserialize, Serialize)]
689pub struct Label {
690 pub id: u64,
692
693 pub name: String,
695
696 pub parent: Option<String>,
698}
699
700#[derive(Clone, Debug, Default, Deserialize, Serialize)]
702pub struct Labels(pub Vec<Label>);
703
704impl Labels {
706 #[must_use]
708 pub fn len(&self) -> usize {
709 self.0.len()
710 }
711
712 #[must_use]
714 pub fn is_empty(&self) -> bool {
715 self.0.is_empty()
716 }
717}
718
719impl Display for Labels {
720 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
721 if self.is_empty() {
722 return writeln!(f, "No labels.");
723 }
724 for label in &self.0 {
725 let parent = if let Some(parent) = &label.parent {
726 format!(", parent: {parent}")
727 } else {
728 String::new()
729 };
730 writeln!(f, "{}: {}{parent}", label.id, label.name)?;
731 }
732 Ok(())
733 }
734}