pub mod elf;
pub mod macho;
pub mod pe;
use crate::VirusTotalError;
use chrono::serde::{ts_seconds, ts_seconds_option};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum FileReportRequestResponse {
#[serde(rename = "data")]
Data(FileReportData),
#[serde(rename = "error")]
Error(VirusTotalError),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileReportData {
pub attributes: ScanResultAttributes,
#[serde(rename = "type")]
pub record_type: String,
pub id: String,
pub links: HashMap<String, String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ScanResultAttributes {
#[serde(default, with = "ts_seconds_option")]
pub creation_date: Option<DateTime<Utc>>,
pub capabilities_tags: Option<Vec<String>>,
pub malware_config: Option<HashMap<String, String>>,
pub type_description: String,
pub tlsh: Option<String>,
pub vhash: Option<String>,
pub telfhash: Option<String>,
pub type_tags: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
pub names: Vec<String>,
#[serde(with = "ts_seconds")]
pub last_modification_date: DateTime<Utc>,
#[serde(default, with = "ts_seconds_option")]
pub first_seen_itw_date: Option<DateTime<Utc>>,
pub type_tag: String,
pub times_submitted: u32,
pub total_votes: Votes,
pub size: u64,
pub popular_threat_classification: Option<PopularThreatClassification>,
#[serde(with = "ts_seconds")]
pub last_submission_date: DateTime<Utc>,
pub last_analysis_results: HashMap<String, AnalysisResult>,
pub trid: Option<Vec<TrID>>,
pub detectiteasy: Option<DetectItEasy>,
pub sha256: String,
pub type_extension: Option<String>,
#[serde(with = "ts_seconds")]
pub last_analysis_date: DateTime<Utc>,
pub unique_sources: u32,
#[serde(with = "ts_seconds")]
pub first_submission_date: DateTime<Utc>,
pub md5: String,
pub ssdeep: String,
pub sha1: String,
pub magic: String,
pub last_analysis_stats: LastAnalysisStats,
#[serde(default)]
pub sigma_analysis_summary: HashMap<String, serde_json::Value>,
#[serde(default)]
pub sigma_analysis_stats: Option<SigmaAnalysisStats>,
#[serde(default)]
pub packers: HashMap<String, String>,
pub meaningful_name: String,
pub reputation: u32,
pub macho_info: Option<Vec<macho::MachoInfo>>,
pub pe_info: Option<pe::PEInfo>,
#[serde(default)]
pub dot_net_assembly: Option<pe::dotnet::DotNetAssembly>,
#[serde(default)]
pub authentihash: Option<String>,
#[serde(default)]
pub elf_info: Option<elf::ElfInfo>,
#[serde(default)]
pub signature_info: HashMap<String, serde_json::Value>,
#[serde(default)]
pub sandbox_verdicts: HashMap<String, SandboxVerdict>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Votes {
pub harmless: u32,
pub malicious: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PopularThreatClassification {
pub suggested_threat_label: String,
#[serde(default)]
pub popular_threat_category: Vec<PopularThreatClassificationInner>,
#[serde(default)]
pub popular_threat_name: Vec<PopularThreatClassificationInner>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PopularThreatClassificationInner {
pub count: u32,
pub value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AnalysisResult {
pub category: String,
pub engine_name: String,
pub engine_version: Option<String>,
pub result: Option<String>,
pub method: String,
pub engine_update: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TrID {
pub file_type: String,
pub probability: f32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DetectItEasy {
pub filetype: String,
#[serde(default)]
pub values: Vec<DetectItEasyValues>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DetectItEasyValues {
pub info: Option<String>,
#[serde(rename = "type")]
pub detection_type: String,
pub name: String,
pub version: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LastAnalysisStats {
pub harmless: u32,
#[serde(rename = "type-unsupported")]
pub type_unsupported: u32,
pub suspicious: u32,
#[serde(rename = "confirmed-timeout")]
pub confirmed_timeout: u32,
pub timeout: u32,
pub failure: u32,
pub malicious: u32,
pub undetected: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SandboxVerdict {
pub category: String,
pub confidence: u8,
pub sandbox_name: String,
#[serde(default)]
pub malware_classification: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SigmaAnalysisStats {
pub low: u64,
pub medium: u64,
pub high: u64,
pub critical: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(include_str!("../../testdata/fff40032c3dc062147c530e3a0a5c7e6acda4d1f1369fbc994cddd3c19a2de88.json"), "Rich Text Format")]
#[case(include_str!("../../testdata/0001a1252300b4732e4a010a5dd13a291dcb8b0ebee6febedb5152dfb0bcd488.json"), "DOS COM")]
#[case(include_str!("../../testdata/001015aafcae8a6942366cbb0e7d39c0738752a7800c41ea1c655d47b0a4d04c.json"), "MS Word Document")]
#[case(include_str!("../../testdata/417c06700c3e899f0554654102fa064385bf1d3ecec32471ac488096d81bf38c.json"), "Win32 EXE")] #[case(include_str!("../../testdata/b8e7a581d85807ea6659ea2f681bd16d5baa7017ff144aa3030aefba9cbcdfd3.json"), "Mach-O")]
#[case(include_str!("../../testdata/ddecc35aa198f401948c73a0d53fd93c4ecb770198ad7db308de026745c56b71.json"), "Win32 EXE")]
#[case(include_str!("../../testdata/de10ba5e5402b46ea975b5cb8a45eb7df9e81dc81012fd4efd145ed2dce3a740.json"), "ELF")]
fn deserialize_valid_report(#[case] report: &str, #[case] file_type: &str) {
let report: FileReportRequestResponse =
serde_json::from_str(report).expect("failed to deserialize VT report");
if let FileReportRequestResponse::Data(data) = report {
if file_type == "Mach-O" {
assert!(data.attributes.macho_info.is_some());
} else if file_type == "Win32 EXE" {
assert!(data.attributes.pe_info.is_some());
} else if file_type == "ELF" {
assert!(data.attributes.elf_info.is_some());
}
println!("{data:?}");
assert_eq!(data.attributes.type_description, file_type);
assert_eq!(data.record_type, "file");
} else {
panic!("File wasn't a report!");
}
}
#[rstest]
#[case(include_str!("../../testdata/not_found.json"))]
#[case(include_str!("../../testdata/wrong_key.json"))]
fn deserialize_errors(#[case] contents: &str) {
let report: FileReportRequestResponse =
serde_json::from_str(contents).expect("failed to deserialize VT error response");
match report {
FileReportRequestResponse::Data(_) => panic!("Should have been an error type!"),
FileReportRequestResponse::Error(_) => {}
}
}
}