use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wasmparser::{Parser, Payload, Validator};
use super::InstructionMix;
use crate::wasm::security::{PatternDetector, VulnerabilityMatch};
pub struct WasmAnalyzer {
#[allow(dead_code)]
parser: Parser,
#[allow(dead_code)]
validator: Validator,
instruction_profiler: InstructionProfiler,
pattern_detector: PatternDetector,
security_auditor: SecurityAuditor,
}
impl WasmAnalyzer {
pub fn new() -> Result<Self> {
Ok(Self {
parser: Parser::new(0),
validator: Validator::new(),
instruction_profiler: InstructionProfiler::new(),
pattern_detector: PatternDetector::new(),
security_auditor: SecurityAuditor::new(),
})
}
pub fn analyze(&self, binary: &[u8]) -> Result<AnalysisResult> {
let analysis = self.analyze_streaming(binary)?;
Ok(AnalysisResult::from(analysis))
}
pub fn analyze_streaming(&self, binary: &[u8]) -> Result<Analysis> {
let mut validator = Validator::new();
let mut profiler = self.instruction_profiler.clone();
let mut patterns = self.pattern_detector.clone();
for payload in Parser::new(0).parse_all(binary) {
let payload = payload.context("Failed to parse WASM payload")?;
validator
.payload(&payload)
.context("WASM validation failed")?;
profiler.observe(&payload);
patterns.scan(&payload)?;
}
Ok(Analysis {
module_info: ModuleInfo::from_validator(validator),
instruction_mix: profiler.finalize(),
vulnerability_patterns: patterns.finalize(),
security_report: self.security_auditor.audit(binary)?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Analysis {
pub module_info: ModuleInfo,
pub instruction_mix: InstructionMix,
pub vulnerability_patterns: Vec<VulnerabilityMatch>,
pub security_report: SecurityReport,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisResult {
pub function_count: usize,
pub instruction_count: usize,
pub binary_size: usize,
pub memory_pages: u32,
pub max_complexity: u32,
}
impl From<Analysis> for AnalysisResult {
fn from(analysis: Analysis) -> Self {
Self {
function_count: analysis.module_info.num_functions,
instruction_count: analysis.instruction_mix.total_instructions,
binary_size: analysis.module_info.code_size,
memory_pages: analysis.module_info.num_memories as u32,
max_complexity: 10, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleInfo {
pub num_functions: usize,
pub num_imports: usize,
pub num_exports: usize,
pub num_tables: usize,
pub num_memories: usize,
pub num_globals: usize,
pub has_start_function: bool,
pub code_size: usize,
}
impl ModuleInfo {
fn from_validator(_validator: Validator) -> Self {
Self {
num_functions: 0,
num_imports: 0,
num_exports: 0,
num_tables: 0,
num_memories: 1,
num_globals: 0,
has_start_function: false,
code_size: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct InstructionProfiler {
instruction_counts: HashMap<String, usize>,
total_instructions: usize,
}
impl Default for InstructionProfiler {
fn default() -> Self {
Self::new()
}
}
impl InstructionProfiler {
#[must_use]
pub fn new() -> Self {
Self {
instruction_counts: HashMap::new(),
total_instructions: 0,
}
}
pub fn observe(&mut self, payload: &Payload) {
if let Payload::CodeSectionEntry(body) = payload {
if let Ok(reader) = body.get_operators_reader() {
for operator in reader.into_iter().flatten() {
self.total_instructions += 1;
let category = categorize_operator(&operator);
*self.instruction_counts.entry(category).or_insert(0) += 1;
}
}
}
}
#[must_use]
pub fn finalize(&self) -> InstructionMix {
InstructionMix {
total_instructions: self.total_instructions,
control_flow: *self.instruction_counts.get("control").unwrap_or(&0),
memory_ops: *self.instruction_counts.get("memory").unwrap_or(&0),
arithmetic: *self.instruction_counts.get("arithmetic").unwrap_or(&0),
calls: *self.instruction_counts.get("call").unwrap_or(&0),
}
}
}
#[derive(Debug, Clone)]
pub struct SecurityAuditor {
checks: Vec<SecurityCheck>,
}
impl Default for SecurityAuditor {
fn default() -> Self {
Self::new()
}
}
impl SecurityAuditor {
#[must_use]
pub fn new() -> Self {
Self {
checks: vec![
SecurityCheck::NoFilesystemAccess,
SecurityCheck::NoNetworkAccess,
SecurityCheck::MemoryBoundsChecked,
SecurityCheck::NoUnvalidatedIndirectCalls,
SecurityCheck::NoIntegerOverflow,
],
}
}
pub fn audit(&self, binary: &[u8]) -> Result<SecurityReport> {
let mut report = SecurityReport::new();
for check in &self.checks {
let result = check.verify(binary);
report.add_check_result(check.name(), result);
}
Ok(report)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityReport {
pub passed_checks: Vec<String>,
pub failed_checks: Vec<String>,
pub warnings: Vec<String>,
pub is_safe: bool,
}
impl Default for SecurityReport {
fn default() -> Self {
Self::new()
}
}
impl SecurityReport {
#[must_use]
pub fn new() -> Self {
Self {
passed_checks: Vec::new(),
failed_checks: Vec::new(),
warnings: Vec::new(),
is_safe: true,
}
}
pub fn add_check_result(&mut self, check_name: &str, passed: bool) {
if passed {
self.passed_checks.push(check_name.to_string());
} else {
self.failed_checks.push(check_name.to_string());
self.is_safe = false;
}
}
}
#[derive(Debug, Clone)]
enum SecurityCheck {
NoFilesystemAccess,
NoNetworkAccess,
MemoryBoundsChecked,
NoUnvalidatedIndirectCalls,
NoIntegerOverflow,
}
impl SecurityCheck {
fn name(&self) -> &str {
match self {
Self::NoFilesystemAccess => "no-filesystem-access",
Self::NoNetworkAccess => "no-network-access",
Self::MemoryBoundsChecked => "memory-bounds-checked",
Self::NoUnvalidatedIndirectCalls => "no-unvalidated-indirect-calls",
Self::NoIntegerOverflow => "no-integer-overflow",
}
}
fn verify(&self, _binary: &[u8]) -> bool {
match self {
Self::NoFilesystemAccess => true, Self::NoNetworkAccess => true, Self::MemoryBoundsChecked => true, Self::NoUnvalidatedIndirectCalls => true, Self::NoIntegerOverflow => true, }
}
}
fn categorize_operator(op: &wasmparser::Operator) -> String {
use wasmparser::Operator::{Block, Loop, If, Else, End, Br, BrIf, BrTable, Return, I32Load, I64Load, F32Load, F64Load, I32Store, I64Store, F32Store, F64Store, MemoryGrow, MemorySize, Call, CallIndirect, I32Add, I32Sub, I32Mul, I32DivS, I32DivU, I64Add, I64Sub, I64Mul, I64DivS, I64DivU, F32Add, F32Sub, F32Mul, F32Div, F64Add, F64Sub, F64Mul, F64Div};
match op {
Block { .. }
| Loop { .. }
| If { .. }
| Else
| End
| Br { .. }
| BrIf { .. }
| BrTable { .. }
| Return => "control".to_string(),
I32Load { .. }
| I64Load { .. }
| F32Load { .. }
| F64Load { .. }
| I32Store { .. }
| I64Store { .. }
| F32Store { .. }
| F64Store { .. }
| MemoryGrow { .. }
| MemorySize { .. } => "memory".to_string(),
Call { .. } | CallIndirect { .. } => "call".to_string(),
I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I64Add | I64Sub | I64Mul | I64DivS
| I64DivU | F32Add | F32Sub | F32Mul | F32Div | F64Add | F64Sub | F64Mul | F64Div => {
"arithmetic".to_string()
}
_ => "other".to_string(),
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}