diff --git a/abstract-interpreter.test.ts b/abstract-interpreter.test.ts new file mode 100644 index 0000000..01e6cc0 --- /dev/null +++ b/abstract-interpreter.test.ts @@ -0,0 +1,190 @@ +/** + * Tests for abstract-interpreter.ts — interval domain + expression evaluation. + */ + +import { + AbstractInterpreter, + Interval, + type VariableState, +} from './abstract-interpreter'; + +describe('Interval', () => { + test('fromValue creates a singleton interval', () => { + const i = Interval.fromValue(5n); + expect(i.min).toBe(5n); + expect(i.max).toBe(5n); + }); + + test('add widens bounds correctly', () => { + const a = new Interval(1n, 3n); + const b = new Interval(2n, 4n); + const r = a.add(b); + expect(r.min).toBe(3n); + expect(r.max).toBe(7n); + }); + + test('sub uses worst-case endpoints', () => { + const a = new Interval(10n, 20n); + const b = new Interval(1n, 5n); + const r = a.sub(b); + // min - maxOther .. max - minOther + expect(r.min).toBe(5n); + expect(r.max).toBe(19n); + }); + + test('mul considers all four endpoint products', () => { + const a = new Interval(-2n, 3n); + const b = new Interval(-4n, 5n); + const r = a.mul(b); + // products: 8, -10, -12, 15 → min -12, max 15 + expect(r.min).toBe(-12n); + expect(r.max).toBe(15n); + }); + + test('mul of positive intervals stays positive', () => { + const a = new Interval(2n, 3n); + const b = new Interval(4n, 5n); + const r = a.mul(b); + expect(r.min).toBe(8n); + expect(r.max).toBe(15n); + }); + + test('isWithinBounds returns true when fully inside', () => { + const i = new Interval(0n, 100n); + expect(i.isWithinBounds(0n, AbstractInterpreter.U32_MAX)).toBe(true); + }); + + test('isWithinBounds returns false when max exceeds bound', () => { + const i = new Interval(0n, AbstractInterpreter.U32_MAX + 1n); + expect(i.isWithinBounds(0n, AbstractInterpreter.U32_MAX)).toBe(false); + }); + + test('isWithinBounds returns false when min is below bound', () => { + const i = new Interval(-1n, 10n); + expect(i.isWithinBounds(0n, AbstractInterpreter.U32_MAX)).toBe(false); + }); + + test('toString formats as [min, max]', () => { + expect(new Interval(1n, 2n).toString()).toBe('[1, 2]'); + }); +}); + +describe('AbstractInterpreter type bounds', () => { + test('U32_MAX is 2^32 - 1', () => { + expect(AbstractInterpreter.U32_MAX).toBe(4294967295n); + }); + + test('U64_MAX is 2^64 - 1', () => { + expect(AbstractInterpreter.U64_MAX).toBe(18446744073709551615n); + }); + + test('U128_MAX is 2^128 - 1', () => { + expect(AbstractInterpreter.U128_MAX).toBe( + 340282366920938463463374607431768211455n, + ); + }); +}); + +describe('AbstractInterpreter.evaluateExpression', () => { + let interpreter: AbstractInterpreter; + let state: VariableState; + + beforeEach(() => { + interpreter = new AbstractInterpreter(); + state = new Map(); + }); + + test('evaluates numeric literal', () => { + const r = interpreter.evaluateExpression('42', state); + expect(r).not.toBeNull(); + expect(r!.min).toBe(42n); + expect(r!.max).toBe(42n); + }); + + test('trims whitespace around literal', () => { + const r = interpreter.evaluateExpression(' 7 ', state); + expect(r!.min).toBe(7n); + expect(r!.max).toBe(7n); + }); + + test('resolves variable from state', () => { + state.set('x', new Interval(10n, 20n)); + const r = interpreter.evaluateExpression('x', state); + expect(r!.min).toBe(10n); + expect(r!.max).toBe(20n); + }); + + test('returns null for unknown variable', () => { + expect(interpreter.evaluateExpression('unknown', state)).toBeNull(); + }); + + test('returns null for empty input', () => { + expect(interpreter.evaluateExpression('', state)).toBeNull(); + expect(interpreter.evaluateExpression(' ', state)).toBeNull(); + }); + + test('returns null for malformed expression', () => { + expect(interpreter.evaluateExpression('1 +', state)).toBeNull(); + expect(interpreter.evaluateExpression('+ 1', state)).toBeNull(); + expect(interpreter.evaluateExpression('foo bar', state)).toBeNull(); + }); + + test('adds two literals', () => { + const r = interpreter.evaluateExpression('3 + 5', state); + expect(r!.min).toBe(8n); + expect(r!.max).toBe(8n); + }); + + test('subtracts two literals', () => { + const r = interpreter.evaluateExpression('10 - 3', state); + expect(r!.min).toBe(7n); + expect(r!.max).toBe(7n); + }); + + test('multiplies two literals', () => { + const r = interpreter.evaluateExpression('4 * 5', state); + expect(r!.min).toBe(20n); + expect(r!.max).toBe(20n); + }); + + test('adds variable and literal', () => { + state.set('a', new Interval(1n, 2n)); + const r = interpreter.evaluateExpression('a + 3', state); + expect(r!.min).toBe(4n); + expect(r!.max).toBe(5n); + }); + + test('multiplies two variables (interval product)', () => { + state.set('x', new Interval(2n, 3n)); + state.set('y', new Interval(4n, 5n)); + const r = interpreter.evaluateExpression('x * y', state); + expect(r!.min).toBe(8n); + expect(r!.max).toBe(15n); + }); + + test('detects potential u32 overflow via isWithinBounds', () => { + // 4000000000 + 4000000000 exceeds U32_MAX + const r = interpreter.evaluateExpression('4000000000 + 4000000000', state); + expect(r).not.toBeNull(); + expect(r!.isWithinBounds(0n, AbstractInterpreter.U32_MAX)).toBe(false); + }); + + test('safe u32 addition stays within bounds', () => { + const r = interpreter.evaluateExpression('100 + 200', state); + expect(r!.isWithinBounds(0n, AbstractInterpreter.U32_MAX)).toBe(true); + }); + + test('large multiplication may exceed u64', () => { + const r = interpreter.evaluateExpression( + '18446744073709551615 * 2', + state, + ); + expect(r).not.toBeNull(); + expect(r!.isWithinBounds(0n, AbstractInterpreter.U64_MAX)).toBe(false); + }); + + test('returns null when one side of binary op is unknown', () => { + expect(interpreter.evaluateExpression('x + 1', state)).toBeNull(); + expect(interpreter.evaluateExpression('1 + x', state)).toBeNull(); + }); +}); diff --git a/scanner-engine/src/main.rs b/scanner-engine/src/main.rs index ffbe6b8..5eb85a3 100644 --- a/scanner-engine/src/main.rs +++ b/scanner-engine/src/main.rs @@ -8,7 +8,6 @@ use std::{ path::{Path, PathBuf}, time::{SystemTime, UNIX_EPOCH}, }; -use thiserror::Error; use walkdir::WalkDir; mod multisig_scanner; @@ -32,30 +31,6 @@ struct ScanReport { report_hash: String, } -/// Structured error type for all scanner-engine failure modes. -/// Each variant maps to a distinct exit code and stderr format string. -#[derive(Debug, Error)] -pub enum ScannerError { - #[error("io error: {path}: {source}")] - IoError { - path: String, - #[source] - source: std::io::Error, - }, - #[error("serialization failed: {0}")] - SerializationError(#[source] serde_json::Error), - #[error("report write failed: {path}: {source}")] - ReportWriteError { - path: String, - #[source] - source: std::io::Error, - }, - #[error("invalid target: {path}: {reason}")] - InvalidTarget { path: String, reason: String }, - #[error("report integrity check failed")] - IntegrityCheckFailed, -} - /// Static analysis rules applied to Soroban/Rust contract source files. const RULES: &[(&str, &str, &str)] = &[ (r"unwrap\(\)", "UNSAFE_UNWRAP", "HIGH"), @@ -221,16 +196,7 @@ fn main() { let (file_count, all_findings) = scan_target(&target, &rules); let governance_findings = scan_governance_target(&target); - // Serialize findings — exit 2 on failure, nothing written to stdout - let report_json = match serde_json::to_string_pretty(&all_findings) { - Ok(json) => json, - Err(e) => { - eprintln!("[scanner] ERROR: serialization failed: {}", e); - std::process::exit(2); - } - }; - let hash = sha256_of(&report_json); - + // Build report shell; hash filled after successful serialization. let combined_report = ScanReport { target: target.clone(), total_files: file_count, @@ -239,7 +205,14 @@ fn main() { report_hash: String::new(), }; - let report_json = serde_json::to_string_pretty(&combined_report).unwrap(); + // Serialize findings — exit 2 on failure, nothing written to stdout + let report_json = match serde_json::to_string_pretty(&combined_report) { + Ok(json) => json, + Err(e) => { + eprintln!("[scanner] ERROR: serialization failed: {}", e); + std::process::exit(2); + } + }; let hash = sha256_of(&report_json); let report = ScanReport { @@ -263,7 +236,13 @@ fn main() { } eprintln!("[scanner] ZK state validation hook passed — scan state transition verified."); - let out = serde_json::to_string_pretty(&report).unwrap(); + let out = match serde_json::to_string_pretty(&report) { + Ok(json) => json, + Err(e) => { + eprintln!("[scanner] ERROR: serialization failed: {}", e); + std::process::exit(2); + } + }; println!("{}", out); // Write report to /reports directory — exit 3 on failure @@ -297,7 +276,10 @@ fn main() { } eprintln!("[scanner] Report written to {}", report_path.display()); eprintln!("[scanner] Report SHA-256: {}", report.report_hash); - eprintln!("[scanner] Governance findings: {}", report.governance_findings.len()); + eprintln!( + "[scanner] Governance findings: {}", + report.governance_findings.len() + ); // CRITICAL findings check — exit 1 if report.findings.iter().any(|f| f.severity == "CRITICAL") { @@ -354,7 +336,9 @@ mod tests { let post = sha256_of("[]"); let mut hook = ZkStateValidationHook::new(); - assert!(verify_scan_state_transition(&mut hook, &pre, &post, &scan_nullifier(&post)).is_ok()); + assert!( + verify_scan_state_transition(&mut hook, &pre, &post, &scan_nullifier(&post)).is_ok() + ); } #[test] @@ -400,4 +384,4 @@ mod tests { Err(ZkStateError::NoOpTransition) ); } -} + }