verifyos_cli/rules/
binary_stripping.rs1use crate::rules::core::{
2 AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4use goblin::mach::Mach;
5use std::path::Path;
6
7pub struct BinaryStrippingRule;
8
9impl AppStoreRule for BinaryStrippingRule {
10 fn id(&self) -> &'static str {
11 "RULE_BINARY_STRIPPING"
12 }
13
14 fn name(&self) -> &'static str {
15 "Binary Stripping & Instrumentation Check"
16 }
17
18 fn category(&self) -> RuleCategory {
19 RuleCategory::Bundling
20 }
21
22 fn severity(&self) -> Severity {
23 Severity::Error
24 }
25
26 fn recommendation(&self) -> &'static str {
27 "Ensure your binary is stripped of debug symbols and LLVM profiling instrumentation in production builds."
28 }
29
30 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
31 let Some(executable_path) = artifact.executable_path_for_bundle(artifact.app_bundle_path)
32 else {
33 return Ok(RuleReport {
34 status: RuleStatus::Skip,
35 message: Some("Main executable not found".to_string()),
36 evidence: None,
37 });
38 };
39
40 let mut issues = Vec::new();
41
42 if let Ok(hits) = artifact.instrumentation_scan() {
44 if !hits.is_empty() {
45 issues.push(format!(
46 "Leftover LLVM instrumentation detected: {}",
47 hits.join(", ")
48 ));
49 }
50 }
51
52 match check_is_stripped(&executable_path) {
54 Ok(false) => {
55 issues.push("Binary contains a symbol table (not fully stripped)".to_string());
56 }
57 Ok(true) => {}
58 Err(e) => {
59 return Ok(RuleReport {
60 status: RuleStatus::Error,
61 message: Some(format!("Failed to analyze binary symbols: {e}")),
62 evidence: None,
63 });
64 }
65 }
66
67 if issues.is_empty() {
68 return Ok(RuleReport {
69 status: RuleStatus::Pass,
70 message: Some("Binary is stripped and free of instrumentation".to_string()),
71 evidence: None,
72 });
73 }
74
75 Ok(RuleReport {
76 status: RuleStatus::Fail,
77 message: Some("Binary hygiene issues detected".to_string()),
78 evidence: Some(issues.join(" | ")),
79 })
80 }
81}
82
83fn check_is_stripped(path: &Path) -> Result<bool, Box<dyn std::error::Error>> {
84 let buffer = std::fs::read(path)?;
85 match Mach::parse(&buffer)? {
86 Mach::Binary(macho) => {
87 for lc in &macho.load_commands {
89 if let goblin::mach::load_command::CommandVariant::Symtab(symtab) = lc.command {
90 if symtab.nsyms > 0 {
91 return Ok(false);
92 }
93 }
94 }
95 Ok(true)
96 }
97 Mach::Fat(fat) => {
98 for arch in fat.iter_arches() {
100 let arch = arch?;
101 let macho = goblin::mach::MachO::parse(
102 &buffer[arch.offset as usize..(arch.offset + arch.size) as usize],
103 0,
104 )?;
105 for lc in &macho.load_commands {
106 if let goblin::mach::load_command::CommandVariant::Symtab(symtab) = lc.command {
107 if symtab.nsyms > 0 {
108 return Ok(false);
109 }
110 }
111 }
112 }
113 Ok(true)
114 }
115 }
116}