1#![allow(clippy::match_bool)]
4
5use crate::check::{Analyze, GenericMap};
6
7use goblin::mach::Mach;
8use goblin::Object;
9
10use byte_unit::Byte;
11use chrono::{DateTime, Utc};
12use serde_json::{json, Value};
13
14use std::fs::{self, Metadata};
15use std::path::{Path, PathBuf};
16
17pub mod check;
18pub mod sarif;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
22pub enum Format {
23 Table,
25 Json,
27 Sarif,
29 Markdown,
31}
32
33#[derive(thiserror::Error, Debug)]
35pub enum BinError {
36 #[error("IOError: `{0}`")]
37 Io(#[from] std::io::Error),
38 #[error("libgoblin: `{0}`")]
39 Goblin(#[from] goblin::error::Error),
40 #[error("serde: `{0}`")]
41 Serde(#[from] serde_json::Error),
42 #[error("internal: `{0}`")]
43 Internal(String),
44 #[error("unknown data store error")]
45 Unknown,
46}
47
48pub type BinResult<R> = Result<R, BinError>;
49
50#[derive(serde::Serialize)]
52pub struct Detector {
53 basic: GenericMap,
54 compilation: GenericMap,
55 mitigations: GenericMap,
56 instrumentation: GenericMap,
57}
58
59impl Detector {
60 pub fn run(binpath: PathBuf) -> BinResult<Self> {
61 let basic_map: GenericMap = Self::base_metadata(&binpath)?;
63
64 let data: Vec<u8> = std::fs::read(&binpath)?;
66
67 match Object::parse(&data)? {
69 Object::Elf(elf) => Ok(Self {
70 basic: Self::elf_basic(basic_map, &elf),
71 compilation: elf.compilation(&data)?,
72 mitigations: elf.mitigations(),
73 instrumentation: elf.instrumentation(),
74 }),
75 Object::PE(pe) => Ok(Self {
76 basic: Self::pe_basic(basic_map, &pe),
77 compilation: pe.compilation(&data)?,
78 mitigations: pe.mitigations(),
79 instrumentation: pe.instrumentation(),
80 }),
81 Object::Mach(Mach::Binary(mach)) => Ok(Self {
82 basic: Self::mach_basic(basic_map, &mach),
83 compilation: mach.compilation(&data)?,
84 mitigations: mach.mitigations(),
85 instrumentation: mach.instrumentation(),
86 }),
87 bin => Err(BinError::Internal(format!(
88 "unsupported filetype for analysis: {:?}",
89 bin
90 ))),
91 }
92 }
93
94 fn base_metadata(binpath: &Path) -> BinResult<GenericMap> {
97 let mut basic_map: GenericMap = GenericMap::new();
98
99 let abspath_buf: PathBuf = fs::canonicalize(binpath)?;
101 let abspath: String = abspath_buf
102 .to_str()
103 .ok_or_else(|| BinError::Internal("path is not valid UTF-8".to_string()))?
104 .to_string();
105 basic_map.insert("Absolute Path".to_string(), json!(abspath));
106
107 let metadata: Metadata = fs::metadata(binpath)?;
109
110 let size: u128 = metadata.len() as u128;
112 let byte = Byte::from_bytes(size);
113 let filesize: String = byte.get_appropriate_unit(false).to_string();
114 basic_map.insert("File Size".to_string(), json!(filesize));
115
116 if let Ok(time) = metadata.accessed() {
118 let datetime: DateTime<Utc> = time.into();
119 let stamp: String = datetime.format("%Y-%m-%d %H:%M:%S").to_string();
120 basic_map.insert("Last Modified".to_string(), json!(stamp));
121 }
122 Ok(basic_map)
123 }
124
125 fn elf_basic(mut basic_map: GenericMap, elf: &goblin::elf::Elf<'_>) -> GenericMap {
127 use goblin::elf::header;
128
129 basic_map.insert("Binary Format".to_string(), json!("ELF"));
130 let arch: String = header::machine_to_str(elf.header.e_machine).to_string();
131 basic_map.insert("Architecture".to_string(), json!(arch));
132 let entry_point: String = format!("0x{:x}", elf.header.e_entry);
133 basic_map.insert("Entry Point Address".to_string(), json!(entry_point));
134 basic_map
135 }
136
137 fn pe_basic(mut basic_map: GenericMap, pe: &goblin::pe::PE<'_>) -> GenericMap {
139 basic_map.insert("Binary Format".to_string(), json!("PE/EXE"));
140 let arch: &str = if pe.is_64 { "PE32+" } else { "PE32" };
141 basic_map.insert("Architecture".to_string(), json!(arch));
142 let entry_point: String = format!("0x{:x}", pe.entry);
143 basic_map.insert("Entry Point Address".to_string(), json!(entry_point));
144 basic_map
145 }
146
147 fn mach_basic(mut basic_map: GenericMap, mach: &goblin::mach::MachO<'_>) -> GenericMap {
149 use goblin::mach::constants::cputype;
150 use goblin::mach::load_command::CommandVariant;
151
152 basic_map.insert("Binary Format".to_string(), json!("Mach-O"));
153 let cputype: &str = match mach.header.cputype() {
154 cputype::CPU_TYPE_I386 => "i386",
155 cputype::CPU_TYPE_X86_64 => "x86_64",
156 cputype::CPU_TYPE_ARM => "arm",
157 cputype::CPU_TYPE_ARM64 => "arm64",
158 _ => "<unknown>",
159 };
160 basic_map.insert("Architecture".to_string(), json!(cputype));
161
162 for cmd in &mach.load_commands {
163 if let CommandVariant::Main(entry) = cmd.command {
164 let entry_point: String = format!("0x{:x}", entry.entryoff);
165 basic_map.insert("Entry Point".to_string(), json!(entry_point));
166 }
167 }
168 basic_map
169 }
170
171 pub fn output(&self, json: Option<String>, format: Format) -> BinResult<()> {
178 if let Some(path) = json {
179 let output: String = serde_json::to_string_pretty(self)?;
180 if path == "-" {
181 println!("{output}");
182 } else {
183 fs::write(path, output)?;
184 }
185 return Ok(());
186 }
187
188 match format {
189 Format::Json => {
190 println!("{}", serde_json::to_string_pretty(self)?);
191 },
192 Format::Sarif => {
193 let report: String = self.to_sarif(env!("CARGO_PKG_VERSION"))?;
194 sarif::validate_sarif(&report)?;
195 println!("{report}");
196 }
197 Format::Markdown => {
198 let report: String = self.to_sarif(env!("CARGO_PKG_VERSION"))?;
199 let markdown: String = sarif::to_markdown(&report)?;
200 println!("{markdown}");
201 },
202 Format::Table => {
203 Detector::table("BASIC", &self.basic);
205 Detector::table("COMPILATION", &self.compilation);
206 Detector::table("EXPLOIT MITIGATIONS", &self.mitigations);
207
208 if !self.instrumentation.is_empty() {
210 Detector::table("INSTRUMENTATION", &self.instrumentation);
211 }
212 },
213 }
214 Ok(())
215 }
216
217 pub fn to_sarif(&self, tool_version: &str) -> BinResult<String> {
224 let binary_uri: &str = self
225 .basic
226 .get("Absolute Path")
227 .and_then(Value::as_str)
228 .unwrap_or_default();
229 let sections: [(&str, &GenericMap); 4] = [
230 ("basic", &self.basic),
231 ("compilation", &self.compilation),
232 ("mitigations", &self.mitigations),
233 ("instrumentation", &self.instrumentation),
234 ];
235 sarif::build(binary_uri, §ions, tool_version)
236 }
237
238 pub fn write_reports(&self, output_dir: &Path) -> BinResult<()> {
247 if !output_dir.is_dir() {
248 return Err(BinError::Internal(format!(
249 "output directory does not exist: {}",
250 output_dir.display()
251 )));
252 }
253 let sarif_json: String = self.to_sarif(env!("CARGO_PKG_VERSION"))?;
254 sarif::validate_sarif(&sarif_json)?;
255 fs::write(output_dir.join("report.sarif"), &sarif_json)?;
256
257 let markdown: String = sarif::to_markdown(&sarif_json)?;
258 fs::write(output_dir.join("report.md"), &markdown)?;
259 Ok(())
260 }
261
262 #[inline]
263 pub fn table(name: &str, mapping: &GenericMap) {
264 println!("-----------------------------------------------");
265 println!("{}", name);
266 println!("-----------------------------------------------\n");
267 for (name, feature) in mapping {
268 let value: String = match feature {
269 Value::Bool(true) => String::from("\x1b[0;32m✔️\x1b[0m"),
270 Value::Bool(false) => String::from("\x1b[0;31m✖️\x1b[0m"),
271 Value::String(val) => val.clone(),
272 other => other.to_string(),
273 };
274 println!("{0: <45} {1}", name, value);
275 }
276 println!();
277 }
278}