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}