1use std::fmt::{Debug, Formatter};
4use std::path::{Path, PathBuf};
5
6use crate::get_config_path;
7use malwaredb_types::exec::pe32::EXE;
8
9use anyhow::{bail, ensure, Context, Result};
10use base64::engine::general_purpose;
11use base64::Engine;
12use fuzzyhash::FuzzyHash;
13use malwaredb_api::{PartialHashSearchType, SearchRequest};
14use malwaredb_lzjd::{LZDict, Murmur3HashState};
15use reqwest::Certificate;
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use tlsh_fixed::TlshBuilder;
19use tracing::{error, info};
20use zeroize::{Zeroize, ZeroizeOnDrop};
21
22#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
24pub struct MdbClient {
25 pub url: String,
27
28 api_key: String,
30
31 #[zeroize(skip)]
34 #[serde(default, with = "crate::option_cert_path_serialization")]
35 cert: Option<(Certificate, PathBuf)>,
36}
37
38impl MdbClient {
39 pub fn new(url: String, api_key: String, cert_path: Option<PathBuf>) -> Result<Self> {
46 let mut url = url;
47 let url = if url.ends_with('/') {
48 url.pop();
49 url
50 } else {
51 url
52 };
53
54 let cert = if let Some(path) = cert_path {
55 Some((crate::path_load_cert(&path)?, path))
56 } else {
57 None
58 };
59
60 Ok(Self { url, api_key, cert })
61 }
62
63 #[inline]
65 fn client(&self) -> reqwest::Result<reqwest::blocking::Client> {
66 let builder = reqwest::blocking::ClientBuilder::new()
67 .gzip(true)
68 .zstd(true)
69 .use_rustls_tls()
70 .user_agent(concat!("mdb_client/", env!("CARGO_PKG_VERSION")));
71
72 if let Some(cert) = &self.cert {
73 builder.add_root_certificate(cert.0.clone()).build()
74 } else {
75 builder.build()
76 }
77 }
78
79 pub fn login(
86 url: String,
87 username: String,
88 password: String,
89 save: bool,
90 cert_path: Option<PathBuf>,
91 ) -> Result<Self> {
92 let mut url = url;
93 let url = if url.ends_with('/') {
94 url.pop();
95 url
96 } else {
97 url
98 };
99
100 let api_request = malwaredb_api::GetAPIKeyRequest {
101 user: username,
102 password,
103 };
104
105 let builder = reqwest::blocking::ClientBuilder::new()
106 .gzip(true)
107 .zstd(true)
108 .use_rustls_tls()
109 .user_agent(concat!("mdb_client/", env!("CARGO_PKG_VERSION")));
110
111 let cert = if let Some(path) = cert_path {
112 Some((crate::path_load_cert(&path)?, path))
113 } else {
114 None
115 };
116
117 let client = if let Some(cert) = &cert {
118 builder.add_root_certificate(cert.0.clone()).build()
119 } else {
120 builder.build()
121 }?;
122
123 let res = client
124 .post(format!("{url}{}", malwaredb_api::USER_LOGIN_URL))
125 .json(&api_request)
126 .send()?
127 .json::<malwaredb_api::GetAPIKeyResponse>()?;
128
129 if let Some(key) = &res.key {
130 let client = MdbClient {
131 url,
132 api_key: key.clone(),
133 cert,
134 };
135
136 if save {
137 if let Err(e) = client.save() {
138 error!("Login successful but failed to save config: {e}");
139 bail!("Login successful but failed to save config: {e}");
140 }
141 }
142 Ok(client)
143 } else {
144 if let Some(msg) = &res.message {
145 error!("Login failed, response: {msg}");
146 }
147 bail!("server error or bad credentials");
148 }
149 }
150
151 pub fn reset_key(&self) -> Result<()> {
157 let response = self
158 .client()?
159 .get(format!("{}{}", self.url, malwaredb_api::USER_LOGOUT_URL))
160 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
161 .send()
162 .context("server error, or invalid API key")?;
163 if !response.status().is_success() {
164 bail!("failed to reset API key, was it correct?");
165 }
166 Ok(())
167 }
168
169 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
176 let name = path.as_ref().display();
177 let config =
178 std::fs::read_to_string(&path).context(format!("failed to read config file {name}"))?;
179 let cfg: MdbClient =
180 toml::from_str(&config).context(format!("failed to parse config file {name}"))?;
181 Ok(cfg)
182 }
183
184 pub fn load() -> Result<Self> {
191 let path = get_config_path(false)?;
192 if path.exists() {
193 return Self::from_file(path);
194 }
195 bail!("config file not found")
196 }
197
198 pub fn save(&self) -> Result<()> {
204 let toml = toml::to_string(self)?;
205 let path = get_config_path(true)?;
206 std::fs::write(&path, toml)
207 .context(format!("failed to write mdb config to {}", path.display()))
208 }
209
210 pub fn delete(&self) -> Result<()> {
217 let path = get_config_path(false)?;
218 if path.exists() {
219 std::fs::remove_file(&path).context(format!(
220 "failed to delete client config file {}",
221 path.display()
222 ))?;
223 }
224 Ok(())
225 }
226
227 pub fn server_info(&self) -> Result<malwaredb_api::ServerInfo> {
235 self.client()?
236 .get(format!("{}{}", self.url, malwaredb_api::SERVER_INFO))
237 .send()?
238 .json::<malwaredb_api::ServerInfo>()
239 .context("failed to receive or decode server info")
240 }
241
242 pub fn supported_types(&self) -> Result<malwaredb_api::SupportedFileTypes> {
248 self.client()?
249 .get(format!(
250 "{}{}",
251 self.url,
252 malwaredb_api::SUPPORTED_FILE_TYPES
253 ))
254 .send()?
255 .json::<malwaredb_api::SupportedFileTypes>()
256 .context("failed to receive or decode server-supported file types")
257 }
258
259 pub fn whoami(&self) -> Result<malwaredb_api::GetUserInfoResponse> {
266 self.client()?
267 .get(format!("{}{}", self.url, malwaredb_api::USER_INFO_URL))
268 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
269 .send()?
270 .json::<malwaredb_api::GetUserInfoResponse>()
271 .context("failed to receive or decode user info, or invalid API key")
272 }
273
274 pub fn labels(&self) -> Result<malwaredb_api::Labels> {
281 self.client()?
282 .get(format!("{}{}", self.url, malwaredb_api::LIST_LABELS))
283 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
284 .send()?
285 .json::<malwaredb_api::Labels>()
286 .context("failed to receive or decode available labels, or invalid API key")
287 }
288
289 pub fn sources(&self) -> Result<malwaredb_api::Sources> {
296 self.client()?
297 .get(format!("{}{}", self.url, malwaredb_api::LIST_SOURCES))
298 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
299 .send()?
300 .json::<malwaredb_api::Sources>()
301 .context("failed to receive or decode available labels, or invalid API key")
302 }
303
304 pub fn submit(
311 &self,
312 contents: impl AsRef<[u8]>,
313 file_name: String,
314 source_id: u32,
315 ) -> Result<bool> {
316 let mut hasher = Sha256::new();
317 hasher.update(&contents);
318 let result = hasher.finalize();
319
320 let encoded = general_purpose::STANDARD.encode(contents);
321
322 let payload = malwaredb_api::NewSample {
323 file_name,
324 source_id,
325 file_contents_b64: encoded,
326 sha256: hex::encode(result),
327 };
328
329 match self
330 .client()?
331 .post(format!("{}{}", self.url, malwaredb_api::UPLOAD_SAMPLE))
332 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
333 .json(&payload)
334 .send()
335 {
336 Ok(res) => {
337 if !res.status().is_success() {
338 info!("Code {} sending {}", res.status(), payload.file_name);
339 }
340 Ok(res.status().is_success())
341 }
342 Err(e) => {
343 let status: String = e
344 .status()
345 .map(|s| s.as_str().to_string())
346 .unwrap_or_default();
347 error!("Error{status} sending {}: {e}", payload.file_name);
348 bail!(e.to_string())
349 }
350 }
351 }
352
353 pub fn partial_search(
359 &self,
360 partial_hash: Option<(PartialHashSearchType, String)>,
361 name: Option<String>,
362 response: PartialHashSearchType,
363 limit: u32,
364 ) -> Result<Vec<String>> {
365 let query = SearchRequest {
366 partial_hash,
367 file_name: name,
368 response,
369 limit,
370 };
371
372 ensure!(
373 query.is_valid(),
374 "Query isn't valid: hash isn't hexidecimal or both the hashes and file name are empty"
375 );
376
377 self.client()?
378 .post(format!("{}{}", self.url, malwaredb_api::SEARCH))
379 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
380 .json(&query)
381 .send()?
382 .json::<Vec<String>>()
383 .context("failed to receive or decode hash list, or invalid API key")
384 }
385
386 pub fn retrieve(&self, hash: &str, cart: bool) -> Result<Vec<u8>> {
393 let api_endpoint = if cart {
394 format!("{}{hash}", malwaredb_api::DOWNLOAD_SAMPLE_CART)
395 } else {
396 format!("{}{hash}", malwaredb_api::DOWNLOAD_SAMPLE)
397 };
398
399 let res = self
400 .client()?
401 .get(format!("{}{api_endpoint}", self.url))
402 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
403 .send()?;
404
405 if !res.status().is_success() {
406 bail!("Received code {}", res.status());
407 }
408
409 let body = res.bytes()?;
410 Ok(body.to_vec())
411 }
412
413 pub fn report(&self, hash: &str) -> Result<malwaredb_api::Report> {
420 self.client()?
421 .get(format!(
422 "{}{}/{hash}",
423 self.url,
424 malwaredb_api::SAMPLE_REPORT
425 ))
426 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
427 .send()?
428 .json::<malwaredb_api::Report>()
429 .context("failed to receive or decode sample report, or invalid API key")
430 }
431
432 pub fn similar(&self, contents: &[u8]) -> Result<malwaredb_api::SimilarSamplesResponse> {
440 let mut hashes = vec![];
441 let ssdeep_hash = FuzzyHash::new(contents);
442
443 let build_hasher = Murmur3HashState::default();
444 let lzjd_str =
445 LZDict::from_bytes_stream(contents.iter().copied(), &build_hasher).to_string();
446 hashes.push((malwaredb_api::SimilarityHashType::LZJD, lzjd_str));
447 hashes.push((
448 malwaredb_api::SimilarityHashType::SSDeep,
449 ssdeep_hash.to_string(),
450 ));
451
452 let mut builder = TlshBuilder::new(
453 tlsh_fixed::BucketKind::Bucket256,
454 tlsh_fixed::ChecksumKind::ThreeByte,
455 tlsh_fixed::Version::Version4,
456 );
457
458 builder.update(contents);
459 if let Ok(hasher) = builder.build() {
460 hashes.push((malwaredb_api::SimilarityHashType::TLSH, hasher.hash()));
461 }
462
463 if let Ok(exe) = EXE::from(contents) {
464 if let Some(imports) = exe.imports {
465 hashes.push((
466 malwaredb_api::SimilarityHashType::ImportHash,
467 hex::encode(imports.hash()),
468 ));
469 hashes.push((
470 malwaredb_api::SimilarityHashType::FuzzyImportHash,
471 imports.fuzzy_hash(),
472 ));
473 }
474 }
475
476 let request = malwaredb_api::SimilarSamplesRequest { hashes };
477
478 self.client()?
479 .post(format!("{}{}", self.url, malwaredb_api::SIMILAR_SAMPLES))
480 .header(malwaredb_api::MDB_API_HEADER, &self.api_key)
481 .json(&request)
482 .send()?
483 .json::<malwaredb_api::SimilarSamplesResponse>()
484 .context("failed to receive or decode similarity response, or invalid API key")
485 }
486}
487
488impl Debug for MdbClient {
489 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
490 use crate::MDB_VERSION;
491
492 writeln!(f, "MDB Client v{MDB_VERSION}: {}", self.url)
493 }
494}