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::fmt::{Display, Formatter};
13
14use chrono::serde::ts_seconds_option;
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use zeroize::{Zeroize, ZeroizeOnDrop};
18
19pub const MDB_VERSION: &str = env!("CARGO_PKG_VERSION");
21
22pub const MDB_API_HEADER: &str = "mdb-api-key";
24
25pub const USER_LOGIN_URL: &str = "/v1/users/getkey";
27
28#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
30pub struct GetAPIKeyRequest {
31 pub user: String,
33
34 pub password: String,
36}
37
38pub const USER_LOGOUT_URL: &str = "/v1/users/clearkey";
40
41#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
44pub struct GetAPIKeyResponse {
45 pub key: Option<String>,
47
48 pub message: Option<String>,
50}
51
52pub const USER_INFO_URL: &str = "/v1/users/info";
55
56#[derive(Clone, Debug, Deserialize, Serialize)]
58pub struct GetUserInfoResponse {
59 pub id: u32,
61
62 pub username: String,
64
65 pub groups: Vec<String>,
67
68 pub sources: Vec<String>,
70
71 pub is_admin: bool,
73
74 pub created: DateTime<Utc>,
76
77 pub is_readonly: bool,
79}
80
81pub const SERVER_INFO: &str = "/v1/server/info";
83
84#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
86pub struct ServerInfo {
87 pub os_name: String,
89
90 pub memory_used: String,
92
93 pub mdb_version: semver::Version,
95
96 pub db_version: String,
98
99 pub db_size: String,
101
102 pub num_samples: u64,
104
105 pub num_users: u32,
107
108 pub uptime: String,
110
111 pub instance_name: String,
113}
114
115pub const SUPPORTED_FILE_TYPES: &str = "/v1/server/types";
117
118#[derive(Clone, Debug, Deserialize, Serialize)]
120pub struct SupportedFileType {
121 pub name: String,
123
124 pub magic: Vec<String>,
126
127 pub is_executable: bool,
129
130 pub description: Option<String>,
132}
133
134#[derive(Clone, Debug, Deserialize, Serialize)]
136pub struct SupportedFileTypes {
137 pub types: Vec<SupportedFileType>,
139
140 pub message: Option<String>,
142}
143
144pub const LIST_SOURCES: &str = "/v1/sources/list";
146
147#[derive(Clone, Debug, Deserialize, Serialize)]
149pub struct SourceInfo {
150 pub id: u32,
152
153 pub name: String,
155
156 pub description: Option<String>,
158
159 pub url: Option<String>,
161
162 pub first_acquisition: DateTime<Utc>,
164
165 pub malicious: Option<bool>,
167}
168
169#[derive(Clone, Debug, Deserialize, Serialize)]
171pub struct Sources {
172 pub sources: Vec<SourceInfo>,
174
175 pub message: Option<String>,
177}
178
179pub const UPLOAD_SAMPLE: &str = "/v1/samples/upload";
181
182#[derive(Clone, Debug, Deserialize, Serialize)]
184pub struct NewSample {
185 pub file_name: String,
188
189 pub source_id: u32,
191
192 pub file_contents_b64: String,
194
195 pub sha256: String,
197}
198
199pub const DOWNLOAD_SAMPLE: &str = "/v1/samples/download";
203
204pub const DOWNLOAD_SAMPLE_CART: &str = "/v1/samples/download/cart";
208
209pub const SAMPLE_REPORT: &str = "/v1/samples/report";
212
213#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
215pub struct VirusTotalSummary {
216 pub hits: u32,
218
219 pub total: u32,
221
222 #[serde(default)]
224 pub detail: Option<serde_json::Value>,
225
226 #[serde(default, with = "ts_seconds_option")]
228 pub last_analysis_date: Option<DateTime<Utc>>,
229}
230
231#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
234pub struct Report {
235 pub md5: String,
237
238 pub sha1: String,
240
241 pub sha256: String,
243
244 pub sha384: String,
246
247 pub sha512: String,
249
250 pub lzjd: Option<String>,
253
254 pub tlsh: Option<String>,
257
258 pub ssdeep: Option<String>,
261
262 pub humanhash: Option<String>,
265
266 pub filecommand: Option<String>,
269
270 pub bytes: u64,
272
273 pub size: String,
275
276 pub entropy: f32,
278
279 #[serde(default)]
282 pub vt: Option<VirusTotalSummary>,
283}
284
285impl Display for Report {
286 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
287 writeln!(f, "Size: {} bytes, or {}", self.bytes, self.size)?;
288 writeln!(f, "Entropy: {}", self.entropy)?;
289 if let Some(filecmd) = &self.filecommand {
290 writeln!(f, "File command: {filecmd}")?;
291 }
292 if let Some(vt) = &self.vt {
293 writeln!(f, "VT Hits: {}/{}", vt.hits, vt.total)?;
294 }
295 writeln!(f, "MD5: {}", self.md5)?;
296 writeln!(f, "SHA-1: {}", self.sha1)?;
297 writeln!(f, "SHA256: {}", self.sha256)
298 }
299}
300
301pub const SIMILAR_SAMPLES: &str = "/v1/samples/similar";
303
304#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
306#[non_exhaustive]
307pub enum SimilarityHashType {
308 SSDeep,
310
311 LZJD,
313
314 TLSH,
316
317 PEHash,
319
320 ImportHash,
322
323 FuzzyImportHash,
325}
326
327impl SimilarityHashType {
328 #[must_use]
332 pub fn get_table_field_simfunc(&self) -> (&'static str, Option<&'static str>) {
333 match self {
334 SimilarityHashType::SSDeep => ("file.ssdeep", Some("fuzzy_hash_compare")),
335 SimilarityHashType::LZJD => ("file.lzjd", Some("lzjd_compare")),
336 SimilarityHashType::TLSH => ("file.tlsh", Some("tlsh_compare")),
337 SimilarityHashType::PEHash => ("executable.pehash", None),
338 SimilarityHashType::ImportHash => ("executable.importhash", None),
339 SimilarityHashType::FuzzyImportHash => {
340 ("executable.importhashfuzzy", Some("fuzzy_hash_compare"))
341 }
342 }
343 }
344}
345
346impl Display for SimilarityHashType {
347 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
348 match self {
349 SimilarityHashType::SSDeep => write!(f, "SSDeep"),
350 SimilarityHashType::LZJD => write!(f, "LZJD"),
351 SimilarityHashType::TLSH => write!(f, "TLSH"),
352 SimilarityHashType::PEHash => write!(f, "PeHash"),
353 SimilarityHashType::ImportHash => write!(f, "Import Hash (IMPHASH)"),
354 SimilarityHashType::FuzzyImportHash => write!(f, "Fuzzy Import hash"),
355 }
356 }
357}
358
359#[derive(Clone, Debug, Deserialize, Serialize)]
361pub struct SimilarSamplesRequest {
362 pub hashes: Vec<(SimilarityHashType, String)>,
364}
365
366#[derive(Clone, Debug, Deserialize, Serialize)]
368pub struct SimilarSample {
369 pub sha256: String,
371
372 pub algorithms: Vec<(SimilarityHashType, f32)>,
374}
375
376#[derive(Clone, Debug, Deserialize, Serialize)]
378pub struct SimilarSamplesResponse {
379 pub results: Vec<SimilarSample>,
381
382 pub message: Option<String>,
384}
385
386pub const SEARCH: &str = "/v1/search";
388
389#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
392pub struct SearchRequest {
393 pub partial_hash: Option<(PartialHashSearchType, String)>,
395
396 pub file_name: Option<String>,
398
399 pub limit: u32,
401
402 pub response: PartialHashSearchType,
405}
406
407impl SearchRequest {
408 #[must_use]
412 #[inline]
413 pub fn is_valid(&self) -> bool {
414 if let Some((_hash_type, partial_hash)) = &self.partial_hash {
415 let hex = hex::decode(partial_hash);
416 return hex.is_ok();
417 }
418
419 (self.partial_hash.is_some() || self.file_name.is_some()) && self.limit > 0
420 }
421}
422
423impl Default for SearchRequest {
424 fn default() -> Self {
425 Self {
426 partial_hash: None,
427 file_name: None,
428 limit: 100,
429 response: PartialHashSearchType::default(),
430 }
431 }
432}
433
434#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
436pub enum PartialHashSearchType {
437 Any,
439
440 MD5,
442
443 SHA1,
445
446 #[default]
448 SHA256,
449
450 SHA384,
452
453 SHA512,
455}
456
457impl Display for PartialHashSearchType {
458 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
459 match self {
460 PartialHashSearchType::Any => write!(f, "any"),
461 PartialHashSearchType::MD5 => write!(f, "md5"),
462 PartialHashSearchType::SHA1 => write!(f, "sha1"),
463 PartialHashSearchType::SHA256 => write!(f, "sha256"),
464 PartialHashSearchType::SHA384 => write!(f, "sha384"),
465 PartialHashSearchType::SHA512 => write!(f, "sha512"),
466 }
467 }
468}
469
470impl TryInto<PartialHashSearchType> for &str {
471 type Error = String;
472
473 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
474 match self {
475 "any" => Ok(PartialHashSearchType::Any),
476 "md5" => Ok(PartialHashSearchType::MD5),
477 "sha1" => Ok(PartialHashSearchType::SHA1),
478 "sha256" => Ok(PartialHashSearchType::SHA256),
479 "sha384" => Ok(PartialHashSearchType::SHA384),
480 "sha512" => Ok(PartialHashSearchType::SHA512),
481 x => Err(format!("Invalid hash type {x}")),
482 }
483 }
484}
485
486impl TryInto<PartialHashSearchType> for Option<&str> {
487 type Error = String;
488
489 fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
490 if let Some(hash) = self {
491 hash.try_into()
492 } else {
493 Ok(PartialHashSearchType::SHA256)
494 }
495 }
496}
497
498pub const LIST_LABELS: &str = "/v1/labels";
500
501#[derive(Clone, Debug, Deserialize, Serialize)]
503pub struct Label {
504 pub id: u64,
506
507 pub name: String,
509
510 pub parent: Option<String>,
512}
513
514#[derive(Clone, Debug, Default, Deserialize, Serialize)]
516pub struct Labels(pub Vec<Label>);
517
518impl Labels {
520 #[must_use]
522 pub fn len(&self) -> usize {
523 self.0.len()
524 }
525
526 #[must_use]
528 pub fn is_empty(&self) -> bool {
529 self.0.is_empty()
530 }
531}
532
533impl Display for Labels {
534 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
535 if self.is_empty() {
536 return writeln!(f, "No labels.");
537 }
538 for label in &self.0 {
539 let parent = if let Some(parent) = &label.parent {
540 format!(", parent: {parent}")
541 } else {
542 String::new()
543 };
544 writeln!(f, "{}: {}{parent}", label.id, label.name)?;
545 }
546 Ok(())
547 }
548}