pub mod entropy;
pub mod hardening;
use anyhow::Result;
use serde::Serialize;
use crate::analyzer::entropy::SectionEntropy;
use crate::analyzer::hardening::{CheckResult, HardeningInfo};
#[derive(Debug, Clone, Serialize)]
pub struct AnalysisReport {
pub hardening: HardeningInfo,
pub sections: Vec<SectionEntropy>,
pub security_score: u8,
}
impl AnalysisReport {
pub fn analyze(data: &[u8]) -> Result<Self> {
let hardening = HardeningInfo::analyze(data)?;
let sections = SectionEntropy::analyze(data)?;
let security_score = compute_score(&hardening, §ions);
Ok(Self {
hardening,
sections,
security_score,
})
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("JSON serialization cannot fail")
}
pub fn to_json_pretty(&self) -> String {
serde_json::to_string_pretty(self).expect("JSON serialization cannot fail")
}
}
fn compute_score(info: &HardeningInfo, _sections: &[SectionEntropy]) -> u8 {
let mut score: i32 = 0;
score += match &info.nx {
CheckResult::Enabled => 20,
CheckResult::Partial(_) => 10,
_ => 0,
};
score += match &info.pie {
CheckResult::Enabled => 20,
CheckResult::Partial(_) => 10,
_ => 0,
};
score += match &info.relro {
CheckResult::Enabled => 15,
CheckResult::Partial(_) => 7,
CheckResult::NotApplicable => 15, CheckResult::Disabled => 0,
};
score += match &info.stack_canary {
CheckResult::Enabled => 15,
CheckResult::Partial(_) => 7,
_ => 0,
};
score += match &info.fortify_source {
CheckResult::Enabled => 10,
CheckResult::Partial(_) => 5,
_ => 0,
};
score += match &info.rpath {
CheckResult::Enabled => 10, CheckResult::NotApplicable => 10, CheckResult::Partial(_) => 5,
CheckResult::Disabled => 0,
};
score += match &info.stripped {
CheckResult::Enabled => 5,
_ => 0,
};
let sym_deduction = (info.dangerous_symbols.len() as i32).min(5);
score += 5 - sym_deduction;
score.clamp(0, 100) as u8
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::hardening::{DangerousSymbol, SymbolCategory};
fn full_hardening() -> HardeningInfo {
HardeningInfo {
format: "ELF".to_owned(),
architecture: "X86_64".to_owned(),
nx: CheckResult::Enabled,
pie: CheckResult::Enabled,
relro: CheckResult::Enabled,
stack_canary: CheckResult::Enabled,
fortify_source: CheckResult::Enabled,
rpath: CheckResult::Enabled,
stripped: CheckResult::Enabled,
dangerous_symbols: vec![],
}
}
fn bare_hardening() -> HardeningInfo {
HardeningInfo {
format: "ELF".to_owned(),
architecture: "X86".to_owned(),
nx: CheckResult::Disabled,
pie: CheckResult::Disabled,
relro: CheckResult::Disabled,
stack_canary: CheckResult::Disabled,
fortify_source: CheckResult::Disabled,
rpath: CheckResult::Disabled,
stripped: CheckResult::Disabled,
dangerous_symbols: vec![],
}
}
fn dangerous(name: &str, cat: SymbolCategory) -> DangerousSymbol {
DangerousSymbol {
name: name.to_owned(),
category: cat,
}
}
#[test]
fn score_100_for_fully_hardened() {
assert_eq!(compute_score(&full_hardening(), &[]), 100);
}
#[test]
fn score_5_for_all_disabled_no_dangerous_symbols() {
assert_eq!(compute_score(&bare_hardening(), &[]), 5);
}
#[test]
fn score_0_for_all_disabled_with_dangerous_symbols() {
let mut info = bare_hardening();
info.dangerous_symbols = (0..5)
.map(|i| dangerous(&format!("sym{i}"), SymbolCategory::Exec))
.collect();
assert_eq!(compute_score(&info, &[]), 0);
}
#[test]
fn score_na_relro_does_not_penalise() {
let mut info = full_hardening();
info.relro = CheckResult::NotApplicable;
assert_eq!(compute_score(&info, &[]), 100);
}
#[test]
fn score_na_rpath_does_not_penalise() {
let mut info = full_hardening();
info.rpath = CheckResult::NotApplicable;
assert_eq!(compute_score(&info, &[]), 100);
}
#[test]
fn score_partial_relro_deducts_correctly() {
let mut info = full_hardening();
info.relro = CheckResult::Partial("Partial RELRO".to_owned());
assert_eq!(compute_score(&info, &[]), 100 - (15 - 7));
}
#[test]
fn score_one_dangerous_symbol_deducts_one() {
let mut info = full_hardening();
info.dangerous_symbols = vec![dangerous("system", SymbolCategory::Exec)];
assert_eq!(compute_score(&info, &[]), 99);
}
#[test]
fn score_dangerous_symbols_capped_at_five() {
let mut info = full_hardening();
info.dangerous_symbols = (0..10)
.map(|i| dangerous(&format!("sym{i}"), SymbolCategory::Net))
.collect();
assert_eq!(compute_score(&info, &[]), 95);
}
#[test]
fn score_always_clamped_to_zero() {
let mut info = bare_hardening();
info.dangerous_symbols = (0..20)
.map(|i| dangerous(&format!("sym{i}"), SymbolCategory::Mem))
.collect();
assert_eq!(compute_score(&info, &[]), 0);
}
#[test]
fn score_is_additive_per_check() {
let full = compute_score(&full_hardening(), &[]);
let mut no_nx = full_hardening();
no_nx.nx = CheckResult::Disabled;
assert_eq!(compute_score(&no_nx, &[]), full - 20, "NX is worth 20 pts");
let mut no_pie = full_hardening();
no_pie.pie = CheckResult::Disabled;
assert_eq!(
compute_score(&no_pie, &[]),
full - 20,
"PIE is worth 20 pts"
);
let mut no_relro = full_hardening();
no_relro.relro = CheckResult::Disabled;
assert_eq!(
compute_score(&no_relro, &[]),
full - 15,
"RELRO is worth 15 pts"
);
let mut no_canary = full_hardening();
no_canary.stack_canary = CheckResult::Disabled;
assert_eq!(
compute_score(&no_canary, &[]),
full - 15,
"Stack canary is worth 15 pts"
);
let mut no_fortify = full_hardening();
no_fortify.fortify_source = CheckResult::Disabled;
assert_eq!(
compute_score(&no_fortify, &[]),
full - 10,
"FORTIFY_SOURCE is worth 10 pts"
);
let mut has_rpath = full_hardening();
has_rpath.rpath = CheckResult::Disabled;
assert_eq!(
compute_score(&has_rpath, &[]),
full - 10,
"No-RPATH is worth 10 pts"
);
let mut not_stripped = full_hardening();
not_stripped.stripped = CheckResult::Disabled;
assert_eq!(
compute_score(¬_stripped, &[]),
full - 5,
"Stripped is worth 5 pts"
);
}
#[test]
#[cfg(target_os = "linux")]
fn analyze_self_succeeds() {
let path = std::env::current_exe().expect("current exe");
let data = std::fs::read(&path).expect("read self");
let report = AnalysisReport::analyze(&data).expect("analyze");
assert!(!report.sections.is_empty(), "expected at least one section");
assert!(
report.security_score <= 100,
"score {} is out of [0, 100]",
report.security_score
);
}
#[test]
#[cfg(target_os = "linux")]
fn to_json_is_valid_and_has_expected_keys() {
let path = std::env::current_exe().expect("current exe");
let data = std::fs::read(&path).expect("read self");
let report = AnalysisReport::analyze(&data).expect("analyze");
let v: serde_json::Value =
serde_json::from_str(&report.to_json()).expect("to_json must produce valid JSON");
assert!(v.get("hardening").is_some(), "missing 'hardening'");
assert!(v.get("sections").is_some(), "missing 'sections'");
assert!(
v.get("security_score").is_some(),
"missing 'security_score'"
);
}
#[test]
#[cfg(target_os = "linux")]
fn to_json_pretty_is_valid_json() {
let path = std::env::current_exe().expect("current exe");
let data = std::fs::read(&path).expect("read self");
let report = AnalysisReport::analyze(&data).expect("analyze");
serde_json::from_str::<serde_json::Value>(&report.to_json_pretty())
.expect("to_json_pretty must produce valid JSON");
}
#[test]
#[cfg(target_os = "linux")]
fn to_json_score_matches_struct_field() {
let path = std::env::current_exe().expect("current exe");
let data = std::fs::read(&path).expect("read self");
let report = AnalysisReport::analyze(&data).expect("analyze");
let v: serde_json::Value = serde_json::from_str(&report.to_json()).unwrap();
let json_score = v["security_score"].as_u64().unwrap() as u8;
assert_eq!(json_score, report.security_score);
}
#[test]
fn analyze_rejects_empty_data() {
assert!(AnalysisReport::analyze(&[]).is_err());
}
#[test]
fn analyze_rejects_non_binary() {
assert!(AnalysisReport::analyze(b"hello, not a binary").is_err());
}
}