#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![forbid(unsafe_code)]
pub mod digest;
use crate::digest::HashType;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use chrono::serde::ts_seconds_option;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub const MDB_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const MDB_API_HEADER: &str = "mdb-api-key";
pub const USER_LOGIN_URL: &str = "/v1/users/getkey";
pub const MDNS_NAME: &str = "_malwaredb._tcp.local.";
#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
pub struct GetAPIKeyRequest {
pub user: String,
pub password: String,
}
pub const USER_LOGOUT_URL: &str = "/v1/users/clearkey";
#[derive(Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
pub struct GetAPIKeyResponse {
pub key: String,
pub message: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum ServerResponse<D> {
#[serde(alias = "success")]
Success(D),
#[serde(alias = "error")]
Error(ServerError),
}
impl<D> ServerResponse<D> {
#[inline]
pub fn unwrap(self) -> D {
match self {
ServerResponse::Success(d) => d,
ServerResponse::Error(e) => panic!("forced ServerResponse::unwrap() on error: {e}"),
}
}
#[inline]
pub fn into_result(self) -> Result<D, ServerError> {
match self {
ServerResponse::Success(d) => Ok(d),
ServerResponse::Error(e) => Err(e),
}
}
#[inline]
pub const fn is_successful(&self) -> bool {
matches!(*self, ServerResponse::Success(_))
}
#[inline]
pub const fn is_err(&self) -> bool {
matches!(*self, ServerResponse::Error(_))
}
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum ServerError {
NoSamples,
NotFound,
ServerError,
Unauthorized,
Unsupported,
}
impl Display for ServerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ServerError::NoSamples => write!(f, "NoSamples"),
ServerError::NotFound => write!(f, "NotFound"),
ServerError::ServerError => write!(f, "ServerError"),
ServerError::Unauthorized => write!(f, "Unauthorized"),
ServerError::Unsupported => write!(f, "Unsupported"),
}
}
}
impl Error for ServerError {}
pub const USER_INFO_URL: &str = "/v1/users/info";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GetUserInfoResponse {
pub id: u32,
pub username: String,
pub groups: Vec<String>,
pub sources: Vec<String>,
pub is_admin: bool,
pub created: DateTime<Utc>,
pub is_readonly: bool,
}
pub const SERVER_INFO_URL: &str = "/v1/server/info";
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ServerInfo {
pub os_name: String,
pub memory_used: String,
pub mdb_version: semver::Version,
pub db_version: String,
pub db_size: String,
pub num_samples: u64,
pub num_users: u32,
pub uptime: String,
pub instance_name: String,
}
pub const SUPPORTED_FILE_TYPES_URL: &str = "/v1/server/types";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SupportedFileType {
pub name: String,
pub magic: Vec<String>,
pub is_executable: bool,
pub description: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SupportedFileTypes {
pub types: Vec<SupportedFileType>,
pub message: Option<String>,
}
pub const LIST_SOURCES_URL: &str = "/v1/sources/list";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SourceInfo {
pub id: u32,
pub name: String,
pub description: Option<String>,
pub url: Option<String>,
pub first_acquisition: DateTime<Utc>,
pub malicious: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Sources {
pub sources: Vec<SourceInfo>,
pub message: Option<String>,
}
pub const UPLOAD_SAMPLE_JSON_URL: &str = "/v1/samples/json/upload";
pub const UPLOAD_SAMPLE_CBOR_URL: &str = "/v1/samples/cbor/upload";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NewSampleB64 {
pub file_name: String,
pub source_id: u32,
pub file_contents_b64: String,
pub sha256: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NewSampleBytes {
pub file_name: String,
pub source_id: u32,
pub file_contents: Vec<u8>,
pub sha256: String,
}
pub const DOWNLOAD_SAMPLE_URL: &str = "/v1/samples/download";
pub const DOWNLOAD_SAMPLE_CART_URL: &str = "/v1/samples/download/cart";
pub const SAMPLE_REPORT_URL: &str = "/v1/samples/report";
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct VirusTotalSummary {
pub hits: u32,
pub total: u32,
#[serde(default)]
pub detail: Option<serde_json::Value>,
#[serde(default, with = "ts_seconds_option")]
pub last_analysis_date: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Report {
pub md5: String,
pub sha1: String,
pub sha256: String,
pub sha384: String,
pub sha512: String,
pub lzjd: Option<String>,
pub tlsh: Option<String>,
pub ssdeep: Option<String>,
pub humanhash: Option<String>,
pub filecommand: Option<String>,
pub bytes: u64,
pub size: String,
pub entropy: f32,
#[serde(default)]
pub vt: Option<VirusTotalSummary>,
}
impl Display for Report {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Size: {} bytes, or {}", self.bytes, self.size)?;
writeln!(f, "Entropy: {}", self.entropy)?;
if let Some(filecmd) = &self.filecommand {
writeln!(f, "File command: {filecmd}")?;
}
if let Some(vt) = &self.vt {
writeln!(f, "VT Hits: {}/{}", vt.hits, vt.total)?;
}
writeln!(f, "MD5: {}", self.md5)?;
writeln!(f, "SHA-1: {}", self.sha1)?;
writeln!(f, "SHA256: {}", self.sha256)
}
}
pub const SIMILAR_SAMPLES_URL: &str = "/v1/samples/similar";
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
pub enum SimilarityHashType {
SSDeep,
LZJD,
TLSH,
PEHash,
ImportHash,
FuzzyImportHash,
}
impl SimilarityHashType {
#[must_use]
pub fn get_table_field_simfunc(&self) -> (&'static str, Option<&'static str>) {
match self {
SimilarityHashType::SSDeep => ("file.ssdeep", Some("fuzzy_hash_compare")),
SimilarityHashType::LZJD => ("file.lzjd", Some("lzjd_compare")),
SimilarityHashType::TLSH => ("file.tlsh", Some("tlsh_compare")),
SimilarityHashType::PEHash => ("executable.pehash", None),
SimilarityHashType::ImportHash => ("executable.importhash", None),
SimilarityHashType::FuzzyImportHash => {
("executable.importhashfuzzy", Some("fuzzy_hash_compare"))
}
}
}
}
impl Display for SimilarityHashType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SimilarityHashType::SSDeep => write!(f, "SSDeep"),
SimilarityHashType::LZJD => write!(f, "LZJD"),
SimilarityHashType::TLSH => write!(f, "TLSH"),
SimilarityHashType::PEHash => write!(f, "PeHash"),
SimilarityHashType::ImportHash => write!(f, "Import Hash (IMPHASH)"),
SimilarityHashType::FuzzyImportHash => write!(f, "Fuzzy Import hash"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SimilarSamplesRequest {
pub hashes: Vec<(SimilarityHashType, String)>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SimilarSample {
pub sha256: String,
pub algorithms: Vec<(SimilarityHashType, f32)>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SimilarSamplesResponse {
pub results: Vec<SimilarSample>,
pub message: Option<String>,
}
pub const SEARCH_URL: &str = "/v1/search";
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub enum SearchType {
Continuation(uuid::Uuid),
Search(SearchRequestParameters),
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SearchRequestParameters {
pub partial_hash: Option<(PartialHashSearchType, String)>,
pub file_name: Option<String>,
pub limit: u32,
pub file_type: Option<String>,
pub magic: Option<String>,
pub labels: Option<Vec<String>>,
pub response: PartialHashSearchType,
}
impl SearchRequestParameters {
#[must_use]
#[inline]
pub fn is_valid(&self) -> bool {
if self.limit == 0 {
return false;
}
if let Some((_hash_type, partial_hash)) = &self.partial_hash {
let hex = hex::decode(partial_hash);
return hex.is_ok();
}
self.partial_hash.is_some()
|| self.file_name.is_some()
|| self.file_type.is_some()
|| self.magic.is_some()
|| self.labels.is_some()
}
}
impl Default for SearchRequestParameters {
fn default() -> Self {
Self {
partial_hash: None,
file_name: None,
limit: 100,
labels: None,
file_type: None,
magic: None,
response: PartialHashSearchType::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SearchRequest {
pub search: SearchType,
}
impl SearchRequest {
#[must_use]
#[inline]
pub fn is_valid(&self) -> bool {
if let SearchType::Search(search) = &self.search {
search.is_valid()
} else {
true
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct SearchResponse {
pub hashes: Vec<String>,
pub pagination: Option<uuid::Uuid>,
pub total_results: u64,
pub message: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
pub enum PartialHashSearchType {
Any,
MD5,
SHA1,
#[default]
SHA256,
SHA384,
SHA512,
}
impl Display for PartialHashSearchType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PartialHashSearchType::Any => write!(f, "any"),
PartialHashSearchType::MD5 => write!(f, "md5"),
PartialHashSearchType::SHA1 => write!(f, "sha1"),
PartialHashSearchType::SHA256 => write!(f, "sha256"),
PartialHashSearchType::SHA384 => write!(f, "sha384"),
PartialHashSearchType::SHA512 => write!(f, "sha512"),
}
}
}
impl TryInto<PartialHashSearchType> for &str {
type Error = String;
fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
match self {
"any" => Ok(PartialHashSearchType::Any),
"md5" => Ok(PartialHashSearchType::MD5),
"sha1" => Ok(PartialHashSearchType::SHA1),
"sha256" => Ok(PartialHashSearchType::SHA256),
"sha384" => Ok(PartialHashSearchType::SHA384),
"sha512" => Ok(PartialHashSearchType::SHA512),
x => Err(format!("Invalid hash type {x}")),
}
}
}
impl TryInto<PartialHashSearchType> for Option<&str> {
type Error = String;
fn try_into(self) -> Result<PartialHashSearchType, Self::Error> {
if let Some(hash) = self {
hash.try_into()
} else {
Ok(PartialHashSearchType::SHA256)
}
}
}
pub const YARA_SEARCH_URL: &str = "/v1/yara";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct YaraSearchRequest {
pub rules: Vec<String>,
pub response: PartialHashSearchType,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct YaraSearchRequestResponse {
pub uuid: uuid::Uuid,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct YaraSearchResponse {
pub results: HashMap<String, Vec<HashType>>,
}
pub const LIST_LABELS_URL: &str = "/v1/labels";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Label {
pub id: u64,
pub name: String,
pub parent: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Labels(pub Vec<Label>);
impl Labels {
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl Display for Labels {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
return writeln!(f, "No labels.");
}
for label in &self.0 {
let parent = if let Some(parent) = &label.parent {
format!(", parent: {parent}")
} else {
String::new()
};
writeln!(f, "{}: {}{parent}", label.id, label.name)?;
}
Ok(())
}
}