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