Skip to main content

verifyos_cli/rules/
binary_stripping.rs

1use 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        // 1. Check for LLVM instrumentation (from macho_scanner)
43        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        // 2. Check for symbol table using goblin
53        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            // Check for symbol table in load commands
88            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            // Check all architectures
99            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}