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}