Skip to main content

malwaredb/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![doc = include_str!("../README.md")]
4#![deny(clippy::all)]
5#![deny(clippy::pedantic)]
6#![forbid(unsafe_code)]
7
8/// `CaRT` file I/O
9pub mod cart;
10
11/// Python wrapper types for some Malware DB API types
12pub mod types;
13
14use crate::types::{
15    DiscoveredServer, Label, SearchResults, ServerInfo, Source, SupportedFileType, UserInfo,
16    YaraResult,
17};
18use malwaredb_client::blocking::MdbClient;
19
20use std::borrow::Cow;
21use std::path::PathBuf;
22
23use anyhow::{Result, anyhow};
24use pyo3::prelude::*;
25use uuid::Uuid;
26
27/// MDB version
28pub const VERSION: &str = concat!(env!("MDB_VERSION"), " ", env!("MDB_BUILD_DATE"));
29
30/// Malware DB client
31#[pyclass(frozen)]
32pub struct MalwareDBClient {
33    inner: MdbClient,
34}
35
36#[pymethods]
37impl MalwareDBClient {
38    /// Load a configuration from a file if it can be found
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if the configuration file can't be found or isn't valid.
43    #[new]
44    pub fn new() -> PyResult<Self> {
45        Ok(MalwareDBClient {
46            inner: MdbClient::load()?,
47        })
48    }
49
50    /// Login with a username and password
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the server URL, username, or password were incorrect, or if the server
55    /// was unreachable.
56    #[staticmethod]
57    pub fn login(
58        url: String,
59        username: String,
60        password: String,
61        save: bool,
62        cert_path: Option<PathBuf>,
63    ) -> PyResult<Self> {
64        Ok(MalwareDBClient {
65            inner: MdbClient::login(url, username, password, save, cert_path)?,
66        })
67    }
68
69    /// Connect if an API key is already known
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the certificate was not in the expected DER or PEM format or could not be parsed.
74    #[staticmethod]
75    pub fn connect(url: String, api_key: String, cert_path: Option<PathBuf>) -> PyResult<Self> {
76        Ok(MalwareDBClient {
77            inner: MdbClient::new(url, api_key, cert_path)?,
78        })
79    }
80
81    /// Use multicast DNS (also known as Bonjour, Zeroconf) to discover Malware DB servers on your network
82    ///
83    /// # Errors
84    ///
85    /// A network problem could result in an error.
86    #[staticmethod]
87    pub fn discover() -> Result<Vec<DiscoveredServer>> {
88        malwaredb_client::discover_servers().map(|s| s.into_iter().map(Into::into).collect())
89    }
90
91    /// Connect using a specific configuration file
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the configuration file cannot be read, possibly because it
96    /// doesn't exist or due to a permission error or a parsing error.
97    #[staticmethod]
98    pub fn from_file(path: PathBuf) -> Result<Self> {
99        Ok(MalwareDBClient {
100            inner: MdbClient::from_file(path)?,
101        })
102    }
103
104    /// Get the server's URL
105    #[getter]
106    #[must_use]
107    pub fn url(&self) -> String {
108        self.inner.url.clone()
109    }
110
111    /// Get the bytes of a sample from the database, optionally as a `CaRT` file.
112    ///
113    /// # Errors
114    ///
115    /// This may return an error if the server is unreachable or if the user is not logged in
116    /// or not properly authorized to connect.
117    #[pyo3(signature = (hash, cart = false))]
118    pub fn get_file_bytes(&self, hash: &str, cart: bool) -> Result<Cow<'_, [u8]>> {
119        self.inner.retrieve(hash, cart).map(Cow::from)
120    }
121
122    /// Submit a file to the database, which requires the file name and source ID. Returns true if stored.
123    ///
124    /// # Errors
125    ///
126    /// This may return an error if the server is unreachable or if the user is not logged in
127    /// or not properly authorized to connect.
128    pub fn submit_file(
129        &self,
130        contents: Vec<u8>,
131        file_name: String,
132        source_id: u32,
133    ) -> Result<bool> {
134        self.inner.submit(contents, file_name, source_id)
135    }
136
137    /// Search by partial hash, partial file name, file type, file command output, or labels;
138    /// returning a list of hashes by specified hash type
139    ///
140    /// # Errors
141    ///
142    /// * Invalid hash types will result in an error
143    /// * This may return an error if the server isn't reachable or if the user is not logged in
144    /// or the request isn't valid
145    #[allow(clippy::too_many_arguments)]
146    #[pyo3(signature = (hash = None, hash_type = "sha256", file_name = None, labels = None, file_type = None, magic = None, response_hash = "sha256", limit = 100))]
147    pub fn search(
148        &self,
149        hash: Option<String>,
150        hash_type: &str,
151        file_name: Option<String>,
152        labels: Option<Vec<String>>,
153        file_type: Option<String>,
154        magic: Option<String>,
155        response_hash: &str,
156        limit: u32,
157    ) -> Result<SearchResults> {
158        let hash_type = hash_type.try_into().map_err(|e: String| anyhow!(e))?;
159        let response_hash = response_hash.try_into().map_err(|e: String| anyhow!(e))?;
160        self.inner
161            .partial_search_labels_type(
162                hash.map(|h| (hash_type, h)),
163                file_name,
164                response_hash,
165                labels,
166                file_type,
167                magic,
168                limit,
169            )
170            .map(Into::into)
171    }
172
173    /// Get sources available to the user
174    ///
175    /// # Errors
176    ///
177    /// This may return an error if the server isn't reachable or if the user is not logged in.
178    pub fn get_sources(&self) -> Result<Vec<Source>> {
179        let sources = self
180            .inner
181            .sources()?
182            .sources
183            .into_iter()
184            .map(Into::into)
185            .collect();
186        Ok(sources)
187    }
188
189    /// Get information about the server
190    ///
191    /// # Errors
192    ///
193    /// This may return an error if the server isn't reachable.
194    pub fn server_info(&self) -> Result<ServerInfo> {
195        Ok(self.inner.server_info()?.into())
196    }
197
198    /// Get supported file types; Malware DB only accepts file types it knows about
199    ///
200    /// # Errors
201    ///
202    /// This may return an error if the server isn't reachable or the user is not logged in.
203    pub fn get_supported_file_types(&self) -> Result<Vec<SupportedFileType>> {
204        let supported_types = self
205            .inner
206            .supported_types()?
207            .types
208            .into_iter()
209            .map(Into::into)
210            .collect();
211        Ok(supported_types)
212    }
213
214    /// Get information about the user
215    ///
216    /// # Errors
217    ///
218    /// This may return an error if the server isn't reachable or if the user is not logged in
219    /// or not properly authorized to connect.
220    pub fn whoami(&self) -> Result<UserInfo> {
221        self.inner.whoami().map(Into::into)
222    }
223
224    /// Get labels
225    ///
226    /// # Errors
227    ///
228    /// This may return an error if the server isn't reachable or if the user is not logged in
229    /// or not properly authorized to connect.
230    pub fn labels(&self) -> Result<Vec<Label>> {
231        self.inner
232            .labels()
233            .map(|labels| labels.0.into_iter().map(Into::into).collect())
234    }
235
236    /// Yara rule search
237    ///
238    /// # Errors
239    ///
240    /// This may return an error if the server isn't reachable or if the user is not logged in
241    /// or not properly authorized to connect.
242    pub fn yara_search(&self, query: &str) -> Result<Uuid> {
243        Ok(self.inner.yara_search(query)?.uuid)
244    }
245
246    /// Yara search result via UUID
247    ///
248    /// # Errors
249    ///
250    /// This may return an error if the server isn't reachable or if the user is not logged in
251    /// or not properly authorized to connect.
252    pub fn yara_result(&self, uuid: Uuid) -> Result<YaraResult> {
253        self.inner.yara_result(uuid).map(Into::into)
254    }
255}
256
257/// Only used by this crate directly to register the module. If this crate is used as a module,
258/// that other crate must register the Rust types with that new Python module.
259#[cfg(not(feature = "rust_lib"))]
260#[pymodule]
261fn malwaredb(m: &Bound<'_, PyModule>) -> PyResult<()> {
262    if let Err(log_error) = pyo3_log::try_init() {
263        eprintln!("Failed to enable logging: {log_error}");
264    }
265
266    m.add_class::<MalwareDBClient>()?;
267    m.add_class::<Label>()?;
268    m.add_class::<ServerInfo>()?;
269    m.add_class::<Source>()?;
270    m.add_class::<SupportedFileType>()?;
271    m.add_class::<UserInfo>()?;
272    cart::register_cart_module(m)?;
273    m.add("__version__", VERSION)?;
274    Ok(())
275}