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 exiftool: Option<ExifTool>,
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 sigma_analysis_results: Vec<SigmaAnalysisResults>,
#[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)]
#[serde(rename_all = "PascalCase")]
pub struct ExifTool {
pub character_set: Option<String>,
pub code_size: Option<String>,
pub company_name: Option<String>,
pub create_date: Option<String>,
pub creator: Option<String>,
pub creator_tool: Option<String>,
#[serde(rename = "DocumentID")]
pub document_id: Option<String>,
pub entry_point: Option<String>,
pub file_description: Option<String>,
pub file_flags_mask: Option<String>,
#[serde(rename = "FileOS")]
pub file_os: Option<String>,
pub file_size: Option<String>,
pub file_subtype: Option<String>,
pub file_type: Option<String>,
pub file_type_extension: Option<String>,
pub file_version: Option<String>,
pub file_version_number: Option<String>,
pub format: Option<String>,
#[serde(rename = "HasXFA")]
pub hasxfa: Option<String>,
pub image_file_characteristics: Option<String>,
pub image_version: Option<String>,
pub initialized_data_size: Option<String>,
#[serde(rename = "InstanceID")]
pub instance_id: Option<String>,
pub internal_name: Option<String>,
pub language_code: Option<String>,
pub legal_copyright: Option<String>,
pub linearized: Option<String>,
pub linker_version: Option<String>,
#[serde(rename = "MIMEType")]
pub mimetype: Option<String>,
pub machine_type: Option<String>,
pub metadata_date: Option<String>,
pub modify_date: Option<String>,
#[serde(rename = "OSVersion")]
pub os_version: Option<String>,
pub object_file_type: Option<String>,
pub original_file_name: Option<String>,
pub page_count: Option<String>,
#[serde(rename = "PDFVersion")]
pub pdf_version: Option<String>,
#[serde(rename = "PEType")]
pub petype: Option<String>,
pub producer: Option<String>,
pub product_name: Option<String>,
pub product_version: Option<String>,
pub product_version_number: Option<String>,
pub subsystem: Option<String>,
pub subsystem_version: Option<String>,
pub time_stamp: Option<String>,
pub uninitialized_data_size: Option<String>,
#[serde(rename = "XMPToolkit")]
pub xmp_toolkit: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[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,
}
impl LastAnalysisStats {
pub fn av_count(&self) -> u32 {
self.harmless + self.suspicious + self.malicious + self.undetected
}
pub fn safe_count(&self) -> u32 {
self.harmless + self.undetected
}
pub fn error_count(&self) -> u32 {
self.type_unsupported + self.confirmed_timeout + self.timeout + self.failure
}
pub fn is_benign(&self) -> bool {
self.malicious == 0 && self.suspicious == 0
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SandboxVerdict {
pub category: SandboxVerdictCategory,
pub confidence: u8,
pub sandbox_name: String,
#[serde(default)]
pub malware_classification: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SandboxVerdictCategory {
#[serde(alias = "suspicious", alias = "Suspicious")]
Suspicious,
#[serde(alias = "malicious", alias = "Malicious")]
Malicious,
#[serde(alias = "harmless", alias = "Harmless")]
Harmless,
#[serde(alias = "undetected", alias = "Undetected")]
Undetected,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SigmaAnalysisStats {
pub low: u64,
pub medium: u64,
pub high: u64,
pub critical: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SigmaAnalysisResults {
pub rule_title: String,
pub rule_source: String,
pub match_context: Vec<HashMap<String, serde_json::Value>>,
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case::rtf(include_str!("../../testdata/fff40032c3dc062147c530e3a0a5c7e6acda4d1f1369fbc994cddd3c19a2de88.json"), "Rich Text Format")]
#[case::com(include_str!("../../testdata/0001a1252300b4732e4a010a5dd13a291dcb8b0ebee6febedb5152dfb0bcd488.json"), "DOS COM")]
#[case::word(include_str!("../../testdata/001015aafcae8a6942366cbb0e7d39c0738752a7800c41ea1c655d47b0a4d04c.json"), "MS Word Document")]
#[case::exedotnet(include_str!("../../testdata/417c06700c3e899f0554654102fa064385bf1d3ecec32471ac488096d81bf38c.json"), "Win32 EXE")] #[case::macho(include_str!("../../testdata/b8e7a581d85807ea6659ea2f681bd16d5baa7017ff144aa3030aefba9cbcdfd3.json"), "Mach-O")]
#[case::exe(include_str!("../../testdata/ddecc35aa198f401948c73a0d53fd93c4ecb770198ad7db308de026745c56b71.json"), "Win32 EXE")]
#[case::elf(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");
for (key, value) in &data.attributes.extra {
println!("KEY: {key}");
println!("VALUE: {value}\n\n");
}
assert!(data.attributes.extra.is_empty());
} 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(_) => {}
}
}
#[test]
fn pe_exif() {
const PE_JSON: &str = r#"{
"CodeSize": "86528",
"EntryPoint": "0x5d45",
"FileFlagsMask": "0x003f",
"FileOS": "Windows NT 32-bit",
"FileSubtype": "0",
"FileType": "Win32 EXE",
"FileTypeExtension": "exe",
"FileVersionNumber": "1.0.0.1",
"ImageFileCharacteristics": "Executable, 32-bit",
"ImageVersion": "0.0",
"InitializedDataSize": "15447552",
"LinkerVersion": "10.0",
"MIMEType": "application/octet-stream",
"MachineType": "Intel 386 or later, and compatibles",
"OSVersion": "5.1",
"ObjectFileType": "Executable application",
"PEType": "PE32",
"ProductVersionNumber": "1.0.0.1",
"Subsystem": "Windows GUI",
"SubsystemVersion": "5.1",
"TimeStamp": "2018:06:10 05:04:21+02:00",
"UninitializedDataSize": "0"
}"#;
let exiftool: ExifTool = serde_json::from_str(PE_JSON).unwrap();
assert_eq!(exiftool.file_type.unwrap(), "Win32 EXE");
assert_eq!(exiftool.mimetype.unwrap(), "application/octet-stream");
assert_eq!(exiftool.os_version.unwrap(), "5.1");
assert!(exiftool.extra.is_empty());
}
#[test]
fn pdf_exif() {
const PDF_JSON: &str = r#"{
"CreateDate": "2020:02:27 18:03:45+03:00",
"DocumentID": "uuid:5ac8d66b-6716-466c-b665-965766c06571",
"FileType": "PDF",
"FileTypeExtension": "pdf",
"Format": "application/pdf",
"HasXFA": "No",
"InstanceID": "uuid:696b3606-6627-606f-b636-769b656676f0",
"Linearized": "No",
"MIMEType": "application/pdf",
"MetadataDate": "2020:02:27 18:03:45+03:00",
"ModifyDate": "2020:02:27 18:03:45+03:00",
"PDFVersion": "1.6",
"PageCount": "2",
"XMPToolkit": "Adobe XMP Core 5.4-c005 78.147326, 2012/08/23-13:03:03"
}"#;
let exiftool: ExifTool = serde_json::from_str(PDF_JSON).unwrap();
assert_eq!(exiftool.pdf_version.unwrap(), "1.6");
assert_eq!(
exiftool.xmp_toolkit.unwrap(),
"Adobe XMP Core 5.4-c005 78.147326, 2012/08/23-13:03:03"
);
assert!(exiftool.extra.is_empty());
}
}