use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use struct_metadata::Described;
#[cfg(feature = "rand")]
use rand::RngExt;
use crate::types::{ClassificationString, ExpandingClassification, JsonMap, ServiceName, Sha256, Sid, Text, UpperString, Wildcard};
use crate::{ElasticMeta, Readable};
#[derive(Serialize, Deserialize, Debug, Described, Clone)]
#[metadata_type(ElasticMeta)]
#[metadata(index=false, store=false)]
pub struct TraceEvent {
#[serde(default="default_now")]
pub timestamp: DateTime<Utc>,
pub event_type: String,
pub service: Option<ServiceName>,
pub file: Option<Sha256>,
pub message: Option<String>,
}
fn default_now() -> DateTime<Utc> { Utc::now() }
#[derive(Serialize, Deserialize, Debug, Described, Clone)]
#[metadata_type(ElasticMeta)]
#[metadata(index=true, store=true)]
pub struct Submission {
#[serde(default)]
pub archive_ts: Option<DateTime<Utc>>,
#[serde(default)]
pub archived: bool,
#[serde(flatten)]
pub classification: ExpandingClassification,
#[serde(default)]
#[metadata(store=false, index=false)]
pub tracing_events: Vec<TraceEvent>,
pub error_count: i32,
#[metadata(store=false)]
pub errors: Vec<String>,
#[serde(default)]
#[metadata(store=false)]
pub expiry_ts: Option<DateTime<Utc>>,
pub file_count: i32,
pub files: Vec<File>,
pub max_score: i32,
#[serde(default)]
#[metadata(store=false, mapping="flattenedobject", copyto="__text__")]
pub metadata: HashMap<String, Wildcard>,
pub params: SubmissionParams,
#[metadata(store=false)]
pub results: Vec<Wildcard>,
#[metadata(copyto="__text__")]
pub sid: Sid,
pub state: SubmissionState,
#[serde(default)]
pub to_be_deleted: bool,
#[serde(default)]
pub times: Times,
#[serde(default)]
pub verdict: Verdict,
#[serde(default)]
#[metadata(index=false)]
pub from_archive: bool,
#[serde(default)]
#[metadata(index=false, store=false)]
pub scan_key: Option<String>,
}
#[cfg(feature = "rand")]
impl rand::distr::Distribution<Submission> for rand::distr::StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Submission {
Submission {
archive_ts: None,
archived: rng.random(),
classification: ExpandingClassification::try_unrestricted().unwrap(),
tracing_events: Default::default(),
error_count: 0,
errors: vec![],
expiry_ts: None,
file_count: 1,
files: vec![rng.random()],
max_score: rng.random(),
metadata: Default::default(),
params: SubmissionParams::new(ClassificationString::try_unrestricted().unwrap()),
results: vec![],
sid: rng.random(),
state: SubmissionState::Submitted,
to_be_deleted: false,
times: Times {
completed: None,
submitted: Utc::now(),
},
verdict: Verdict {
malicious: vec![],
non_malicious: vec![],
},
from_archive: false,
scan_key: None,
}
}
}
impl Readable for Submission {
fn set_from_archive(&mut self, from_archive: bool) {
self.from_archive = from_archive
}
}
#[derive(Serialize, Deserialize, Debug, Described, Clone)]
#[metadata_type(ElasticMeta)]
#[metadata(index=true, store=false)]
pub struct SubmissionParams {
pub classification: ClassificationString,
#[serde(default)]
pub deep_scan: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[metadata(store=true, copyto="__text__")]
pub description: Option<Text>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filetype_override: Option<String>,
#[serde(default)]
pub generate_alert: bool,
#[serde(default)]
pub groups: Vec<UpperString>,
#[serde(default)]
pub ignore_cache: bool,
#[serde(default)]
pub ignore_recursion_prevention: bool,
#[serde(default)]
pub ignore_filtering: bool,
#[serde(default)]
pub ignore_size: bool,
#[serde(default)]
pub never_drop: bool,
#[serde(default)]
pub malicious: bool,
#[serde(default="default_max_extracted")]
pub max_extracted: i32,
#[serde(default="default_max_supplementary")]
pub max_supplementary: i32,
#[serde(default="default_priority")]
pub priority: u16,
#[serde(default)]
pub quota_item: bool,
#[serde(default)]
pub services: ServiceSelection,
#[serde(default)]
#[metadata(index=false, store=false)]
pub service_spec: HashMap<ServiceName, JsonMap>,
#[metadata(store=true, copyto="__text__")]
pub submitter: String,
#[serde(default)]
pub trace: bool,
#[serde(default)]
pub ttl: i32,
#[serde(rename="type", default="default_type")]
pub submission_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[metadata(index=false)]
pub initial_data: Option<Text>,
#[serde(default)]
pub auto_archive: bool,
#[serde(default)]
pub delete_after_archive: bool,
#[serde(default)]
pub psid: Option<Sid>,
#[serde(default)]
pub use_archive_alternate_dtl: bool,
}
fn default_max_extracted() -> i32 { 100 }
fn default_max_supplementary() -> i32 { 100 }
fn default_priority() -> u16 { 1000 }
fn default_type() -> String { "USER".to_owned() }
impl SubmissionParams {
pub fn new(classification: ClassificationString) -> Self {
Self {
classification,
deep_scan: false,
description: None,
filetype_override: None,
generate_alert: false,
groups: vec![],
ignore_cache: false,
ignore_recursion_prevention: false,
ignore_filtering: false,
ignore_size: false,
never_drop: false,
malicious: false,
max_extracted: default_max_extracted(),
max_supplementary: default_max_supplementary(),
priority: default_priority(),
quota_item: false,
services: Default::default(),
service_spec: Default::default(),
submitter: "USER".to_owned(),
trace: false,
ttl: 30,
submission_type: default_type(),
initial_data: None,
auto_archive: false,
delete_after_archive: false,
psid: None,
use_archive_alternate_dtl: false,
}
}
pub fn set_description(mut self, text: &str) -> Self {
self.description = Some(text.into()); self
}
pub fn set_ignore_cache(mut self, value: bool) -> Self {
self.ignore_cache = value; self
}
pub fn set_services_selected(mut self, selected: &[&str]) -> Self {
self.services.selected = selected.iter().map(|s|ServiceName::from_string(s.to_string())).collect(); self
}
pub fn set_submitter(mut self, submitter: &str) -> Self {
self.submitter = submitter.to_owned(); self
}
pub fn set_groups(mut self, groups: &[&str]) -> Self {
self.groups = groups.iter().map(|s|s.parse().unwrap()).collect(); self
}
pub fn set_max_extracted(mut self, max_extracted: i32) -> Self {
self.max_extracted = max_extracted; self
}
pub fn set_generate_alert(mut self, alert: bool) -> Self {
self.generate_alert = alert; self
}
pub fn set_initial_data(mut self, initial_data: Option<Text>) -> Self {
self.initial_data = initial_data; self
}
fn get_hashing_keys(&self) -> Vec<(String, serde_json::Value)> {
[
("classification", json!(self.classification)),
("deep_scan", json!(self.deep_scan)),
("ignore_cache", json!(self.ignore_cache)),
("ignore_recursion_prevention", json!(self.ignore_recursion_prevention)),
("ignore_filtering", json!(self.ignore_filtering)),
("ignore_size", json!(self.ignore_size)),
("max_extracted", json!(self.max_extracted)),
("max_supplementary", json!(self.max_supplementary)),
].into_iter().map(|(key, value)|(key.to_owned(), value)).collect()
}
pub fn create_filescore_key(&self, sha256: &Sha256, services: Option<Vec<ServiceName>>) -> String {
let version = 0;
let services = match services {
Some(services) => services,
None => self.services.selected.clone(),
};
let mut data = self.get_hashing_keys();
data.push(("service_spec".to_owned(), {
let mut spec = vec![];
for (key, values) in self.service_spec.clone() {
let mut values: Vec<(String, Value)> = values.into_iter().collect();
values.sort_by(|a, b|a.0.cmp(&b.0));
spec.push((key, values));
}
spec.sort_by(|a, b|a.0.cmp(&b.0));
json!(spec)
}));
data.push(("sha256".to_owned(), json!(sha256)));
data.push(("services".to_owned(), json!(services)));
let s = data.into_iter().map(|(k, v)| format!("{k}: {v}")).collect::<Vec<String>>().join(", ");
use md5::{Md5, Digest};
let mut hasher = Md5::new();
hasher.update(s);
let hash = hasher.finalize();
let mut hex = String::new();
for byte in hash {
hex += &format!("{byte:x}");
}
format!("{hex}v{version}")
}
}
#[derive(Serialize, Deserialize, Default, Debug, Described, Clone)]
#[serde(default)]
#[metadata_type(ElasticMeta)]
#[metadata(index=false, store=false)]
pub struct ServiceSelection {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub excluded: Vec<ServiceName>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub rescan: Vec<ServiceName>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub resubmit: Vec<ServiceName>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub selected: Vec<ServiceName>,
}
#[derive(Serialize, Deserialize, Debug, Described, Clone)]
#[metadata_type(ElasticMeta)]
#[metadata(index=true, store=true)]
pub struct Times {
#[metadata(store=false)]
pub completed: Option<DateTime<Utc>>,
pub submitted: DateTime<Utc>,
}
impl Default for Times {
fn default() -> Self {
Self {
completed: None,
submitted: Utc::now()
}
}
}
#[derive(Serialize, Deserialize, Debug, Described, Clone, Default)]
#[metadata_type(ElasticMeta)]
#[metadata(index=true, store=false)]
#[serde(default)]
pub struct Verdict {
pub malicious: Vec<String>,
pub non_malicious: Vec<String>,
}
#[derive(SerializeDisplay, DeserializeFromStr, Debug, PartialEq, Eq, strum::Display, strum::EnumString, Described, Clone, Copy)]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
#[metadata_type(ElasticMeta)]
pub enum SubmissionState {
Failed,
Submitted,
Completed,
}
#[test]
fn test_state_serialization() {
assert_eq!(serde_json::to_string(&SubmissionState::Failed).unwrap(), "\"failed\"");
assert_eq!(serde_json::from_str::<SubmissionState>("\"failed\"").unwrap(), SubmissionState::Failed);
assert_eq!(serde_json::to_value(SubmissionState::Failed).unwrap(), serde_json::json!("failed"));
assert_eq!(serde_json::from_str::<SubmissionState>("\"Failed\"").unwrap(), SubmissionState::Failed);
}
#[derive(Serialize, Deserialize, Debug, Described, Clone)]
#[metadata_type(ElasticMeta)]
#[metadata(index=true, store=false)]
pub struct File {
#[metadata(copyto="__text__")]
pub name: String,
#[metadata(mapping="long")]
pub size: Option<u64>,
#[metadata(copyto="__text__")]
pub sha256: Sha256,
}
#[cfg(feature = "rand")]
impl rand::distr::Distribution<File> for rand::distr::StandardUniform {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> File {
File {
name: "readme.txt".to_string(),
size: Some(rng.random_range(10..1_000_000)),
sha256: rng.random()
}
}
}