Skip to content

Commit 8134738

Browse files
committed
fix: compilation errors, deprecation warnings, and add unit tests
- Fix WeakPoint struct initializations missing `file` and `line` fields across src/kanren/core.rs, src/a2ml/mod.rs, and all test files - Migrate report/gui.rs from deprecated egui API (TopBottomPanel, SidePanel, CentralPanel::show, ScrollArea::id_source) to current Panel::top/left/show_inside and id_salt - Fix version mismatch: main.rs command version now matches Cargo.toml (2.1.0) - Fix stale repository URL in Cargo.toml (panic-attacker → panic-attack) - Add `log` crate and replace raw println!/eprintln! with structured logging in groove.rs, kanren/rules.rs, assail/analyzer.rs, notify.rs, ambush/mod.rs - Remove useless usize >= 0 comparisons in property_tests.rs - Add 11 unit tests for assail/analyzer.rs covering language detection, unsafe code detection, panic path thresholds, command injection, eval detection, empty directory handling, and excluded directory skipping - Add 13 unit tests for panll/mod.rs covering axis/category labels, export format, image/temporal diff serialization, and write_export I/O All 381 tests pass with zero warnings. https://claude.ai/code/session_01AMMKMRH8GhvCnNjrV6iWLn
1 parent 40b40da commit 8134738

11 files changed

Lines changed: 983 additions & 32 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ rust-version = "1.85.0"
77
authors = ["Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"]
88
license-file = "LICENSE"
99
description = "Universal static analysis, stress testing, and logic-based bug signature detection for 47 languages"
10-
repository = "https://github.com/hyperpolymath/panic-attacker"
10+
repository = "https://github.com/hyperpolymath/panic-attack"
1111

1212
[dependencies]
1313
clap = { version = "4.6", features = ["derive"] }
1414
clap_complete = "4.6"
1515
clap_complete_nushell = "4.6"
1616
colored = "3.1"
1717
anyhow = "1.0"
18+
log = "0.4"
1819
serde = { version = "1.0", features = ["derive"] }
1920
serde_json = "1.0"
2021
serde_yaml = "0.9"

src/ambush/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub fn execute(config: AttackConfig) -> Result<Vec<AttackResult>> {
4242

4343
for program in &config.target_programs {
4444
for axis in &config.axes {
45-
println!(
45+
log::info!(
4646
"Ambushing {:?} on axis {:?} (intensity: {:?}, duration: {:?})",
4747
program, axis, config.intensity, config.duration
4848
);

src/assail/analyzer.rs

Lines changed: 338 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ impl Analyzer {
253253
Ok(b) => b,
254254
Err(e) => {
255255
if self.verbose {
256-
eprintln!("Skipping unreadable file: {} ({})", file.display(), e);
256+
log::debug!("Skipping unreadable file: {} ({})", file.display(), e);
257257
}
258258
continue;
259259
}
@@ -267,7 +267,7 @@ impl Analyzer {
267267
let (cow, _, had_errors) = encoding_rs::WINDOWS_1252.decode(&raw_bytes);
268268
if had_errors {
269269
if self.verbose {
270-
eprintln!(
270+
log::debug!(
271271
"Skipping non-text file: {} (neither UTF-8 nor Latin-1)",
272272
file.display()
273273
);
@@ -3232,3 +3232,339 @@ impl Analyzer {
32323232
}
32333233
}
32343234
}
3235+
3236+
#[cfg(test)]
3237+
mod tests {
3238+
use super::*;
3239+
use std::fs;
3240+
use tempfile::TempDir;
3241+
3242+
// ---------------------------------------------------------------
3243+
// 1. Language::detect for different file extensions
3244+
// ---------------------------------------------------------------
3245+
3246+
#[test]
3247+
fn language_detect_common_extensions() {
3248+
assert_eq!(Language::detect("main.rs"), Language::Rust);
3249+
assert_eq!(Language::detect("lib.c"), Language::C);
3250+
assert_eq!(Language::detect("app.cpp"), Language::Cpp);
3251+
assert_eq!(Language::detect("server.go"), Language::Go);
3252+
assert_eq!(Language::detect("script.py"), Language::Python);
3253+
assert_eq!(Language::detect("index.js"), Language::JavaScript);
3254+
assert_eq!(Language::detect("app.rb"), Language::Ruby);
3255+
assert_eq!(Language::detect("mix.ex"), Language::Elixir);
3256+
assert_eq!(Language::detect("gen_server.erl"), Language::Erlang);
3257+
assert_eq!(Language::detect("router.gleam"), Language::Gleam);
3258+
assert_eq!(Language::detect("README.md"), Language::Unknown);
3259+
assert_eq!(Language::detect("Makefile"), Language::Unknown);
3260+
}
3261+
3262+
#[test]
3263+
fn language_detect_typescript_maps_to_javascript() {
3264+
assert_eq!(Language::detect("component.ts"), Language::JavaScript);
3265+
assert_eq!(Language::detect("component.tsx"), Language::JavaScript);
3266+
assert_eq!(Language::detect("page.jsx"), Language::JavaScript);
3267+
}
3268+
3269+
// ---------------------------------------------------------------
3270+
// 2. Analyzer::new() with a valid temp directory
3271+
// ---------------------------------------------------------------
3272+
3273+
#[test]
3274+
fn analyzer_new_valid_directory() {
3275+
let tmp = TempDir::new().unwrap();
3276+
// Create a Rust file so language detection succeeds
3277+
fs::write(tmp.path().join("main.rs"), "fn main() {}").unwrap();
3278+
let analyzer = Analyzer::new(tmp.path());
3279+
assert!(analyzer.is_ok(), "Analyzer::new should succeed on a valid directory with source files");
3280+
}
3281+
3282+
// ---------------------------------------------------------------
3283+
// 3. Analyzer::new() with a non-existent path
3284+
// ---------------------------------------------------------------
3285+
3286+
#[test]
3287+
fn analyzer_new_nonexistent_path() {
3288+
let result = Analyzer::new(Path::new("/tmp/this_path_definitely_does_not_exist_29387"));
3289+
assert!(result.is_err(), "Analyzer::new should error on non-existent path");
3290+
let err_msg = result.err().expect("expected error").to_string();
3291+
assert!(
3292+
err_msg.contains("does not exist"),
3293+
"Error message should mention 'does not exist', got: {err_msg}"
3294+
);
3295+
}
3296+
3297+
// ---------------------------------------------------------------
3298+
// 4. analyze() on a Rust file containing `unsafe {}` — UnsafeCode
3299+
// ---------------------------------------------------------------
3300+
3301+
#[test]
3302+
fn analyze_rust_detects_unsafe_code() {
3303+
let tmp = TempDir::new().unwrap();
3304+
let rust_file = tmp.path().join("danger.rs");
3305+
fs::write(
3306+
&rust_file,
3307+
r#"
3308+
fn safe_wrapper() {
3309+
unsafe {
3310+
let ptr = std::ptr::null::<u8>();
3311+
*ptr;
3312+
}
3313+
}
3314+
"#,
3315+
)
3316+
.unwrap();
3317+
3318+
let analyzer = Analyzer::new(&rust_file).unwrap();
3319+
let report = analyzer.analyze().unwrap();
3320+
3321+
let unsafe_points: Vec<_> = report
3322+
.weak_points
3323+
.iter()
3324+
.filter(|wp| wp.category == WeakPointCategory::UnsafeCode)
3325+
.collect();
3326+
3327+
assert!(
3328+
!unsafe_points.is_empty(),
3329+
"Should detect UnsafeCode weak point for file containing `unsafe {{}}`"
3330+
);
3331+
}
3332+
3333+
// ---------------------------------------------------------------
3334+
// 5. analyze() on a Rust file containing `.unwrap()` — PanicPath
3335+
// ---------------------------------------------------------------
3336+
3337+
#[test]
3338+
fn analyze_rust_detects_panic_path_from_unwrap() {
3339+
let tmp = TempDir::new().unwrap();
3340+
let rust_file = tmp.path().join("unwrappy.rs");
3341+
// The analyzer triggers PanicPath when unwrap_calls > 5,
3342+
// so we need at least 6 unwrap calls.
3343+
fs::write(
3344+
&rust_file,
3345+
r#"
3346+
fn lots_of_unwraps() {
3347+
let a = Some(1).unwrap();
3348+
let b = Some(2).unwrap();
3349+
let c = Some(3).unwrap();
3350+
let d = Some(4).unwrap();
3351+
let e = Some(5).unwrap();
3352+
let f = Some(6).unwrap();
3353+
let g = Some(7).unwrap();
3354+
}
3355+
"#,
3356+
)
3357+
.unwrap();
3358+
3359+
let analyzer = Analyzer::new(&rust_file).unwrap();
3360+
let report = analyzer.analyze().unwrap();
3361+
3362+
let panic_points: Vec<_> = report
3363+
.weak_points
3364+
.iter()
3365+
.filter(|wp| wp.category == WeakPointCategory::PanicPath)
3366+
.collect();
3367+
3368+
assert!(
3369+
!panic_points.is_empty(),
3370+
"Should detect PanicPath weak point when >5 unwrap() calls are present"
3371+
);
3372+
}
3373+
3374+
// ---------------------------------------------------------------
3375+
// 6. analyze() on a Python file with `eval(` — DynamicCodeExecution
3376+
// ---------------------------------------------------------------
3377+
3378+
#[test]
3379+
fn analyze_python_detects_eval() {
3380+
let tmp = TempDir::new().unwrap();
3381+
let py_file = tmp.path().join("danger.py");
3382+
fs::write(
3383+
&py_file,
3384+
r#"
3385+
user_input = input("Enter expression: ")
3386+
result = eval(user_input)
3387+
print(result)
3388+
"#,
3389+
)
3390+
.unwrap();
3391+
3392+
let analyzer = Analyzer::new(&py_file).unwrap();
3393+
let report = analyzer.analyze().unwrap();
3394+
3395+
let dyn_exec_points: Vec<_> = report
3396+
.weak_points
3397+
.iter()
3398+
.filter(|wp| wp.category == WeakPointCategory::DynamicCodeExecution)
3399+
.collect();
3400+
3401+
assert!(
3402+
!dyn_exec_points.is_empty(),
3403+
"Should detect DynamicCodeExecution for Python eval() usage"
3404+
);
3405+
}
3406+
3407+
// ---------------------------------------------------------------
3408+
// 7. analyze() on a Go file with `exec.Command(` — CommandInjection
3409+
// ---------------------------------------------------------------
3410+
3411+
#[test]
3412+
fn analyze_go_detects_exec_command() {
3413+
let tmp = TempDir::new().unwrap();
3414+
let go_file = tmp.path().join("runner.go");
3415+
fs::write(
3416+
&go_file,
3417+
r#"
3418+
package main
3419+
3420+
import (
3421+
"os/exec"
3422+
"fmt"
3423+
)
3424+
3425+
func main() {
3426+
cmd := exec.Command("ls", "-la")
3427+
out, _ := cmd.Output()
3428+
fmt.Println(string(out))
3429+
}
3430+
"#,
3431+
)
3432+
.unwrap();
3433+
3434+
let analyzer = Analyzer::new(&go_file).unwrap();
3435+
let report = analyzer.analyze().unwrap();
3436+
3437+
let cmd_injection_points: Vec<_> = report
3438+
.weak_points
3439+
.iter()
3440+
.filter(|wp| wp.category == WeakPointCategory::CommandInjection)
3441+
.collect();
3442+
3443+
assert!(
3444+
!cmd_injection_points.is_empty(),
3445+
"Should detect CommandInjection for Go exec.Command usage"
3446+
);
3447+
}
3448+
3449+
// ---------------------------------------------------------------
3450+
// 8. analyze() on an empty directory — empty results
3451+
// ---------------------------------------------------------------
3452+
3453+
#[test]
3454+
fn analyze_empty_directory_produces_empty_results() {
3455+
let tmp = TempDir::new().unwrap();
3456+
// Create a single file so language detection doesn't fail,
3457+
// but put nothing dangerous in it.
3458+
fs::write(tmp.path().join("empty.rs"), "").unwrap();
3459+
3460+
let analyzer = Analyzer::new(tmp.path()).unwrap();
3461+
let report = analyzer.analyze().unwrap();
3462+
3463+
assert!(
3464+
report.weak_points.is_empty(),
3465+
"Empty source files should produce no weak points, got: {:?}",
3466+
report.weak_points,
3467+
);
3468+
assert_eq!(report.statistics.total_lines, 0);
3469+
}
3470+
3471+
// ---------------------------------------------------------------
3472+
// 9. analyze() should skip files in excluded directories
3473+
// (walk_directory skips node_modules, target, .git, etc.)
3474+
// ---------------------------------------------------------------
3475+
3476+
#[test]
3477+
fn analyze_skips_excluded_directories() {
3478+
let tmp = TempDir::new().unwrap();
3479+
3480+
// Create a benign top-level file so language detection succeeds
3481+
fs::write(tmp.path().join("lib.rs"), "fn safe() {}").unwrap();
3482+
3483+
// Create a node_modules directory with a dangerous file inside.
3484+
// walk_directory should skip node_modules entirely.
3485+
let excluded_dir = tmp.path().join("node_modules");
3486+
fs::create_dir_all(&excluded_dir).unwrap();
3487+
fs::write(
3488+
excluded_dir.join("bad.rs"),
3489+
"fn bad() { unsafe { std::ptr::null::<u8>().read(); } }\n",
3490+
)
3491+
.unwrap();
3492+
3493+
let analyzer = Analyzer::new(tmp.path()).unwrap();
3494+
let report = analyzer.analyze().unwrap();
3495+
3496+
// The dangerous file in node_modules should NOT produce weak points
3497+
let unsafe_in_excluded: Vec<_> = report
3498+
.weak_points
3499+
.iter()
3500+
.filter(|wp| {
3501+
wp.category == WeakPointCategory::UnsafeCode
3502+
&& wp
3503+
.location
3504+
.as_deref()
3505+
.map_or(false, |loc| loc.contains("node_modules"))
3506+
})
3507+
.collect();
3508+
3509+
assert!(
3510+
unsafe_in_excluded.is_empty(),
3511+
"Files inside node_modules/ should be skipped during analysis"
3512+
);
3513+
}
3514+
3515+
// ---------------------------------------------------------------
3516+
// 10. analyze() on a single file produces correct language field
3517+
// ---------------------------------------------------------------
3518+
3519+
#[test]
3520+
fn analyze_single_file_reports_correct_language() {
3521+
let tmp = TempDir::new().unwrap();
3522+
let go_file = tmp.path().join("main.go");
3523+
fs::write(&go_file, "package main\nfunc main() {}\n").unwrap();
3524+
3525+
let analyzer = Analyzer::new(&go_file).unwrap();
3526+
let report = analyzer.analyze().unwrap();
3527+
3528+
assert_eq!(
3529+
report.language,
3530+
Language::Go,
3531+
"Report should identify Go as the language for a .go file"
3532+
);
3533+
}
3534+
3535+
// ---------------------------------------------------------------
3536+
// 11. Rust file with few unwraps should NOT trigger PanicPath
3537+
// ---------------------------------------------------------------
3538+
3539+
#[test]
3540+
fn analyze_rust_few_unwraps_no_panic_path() {
3541+
let tmp = TempDir::new().unwrap();
3542+
let rust_file = tmp.path().join("safe.rs");
3543+
// Only 3 unwrap calls — threshold is >5
3544+
fs::write(
3545+
&rust_file,
3546+
r#"
3547+
fn few_unwraps() {
3548+
let a = Some(1).unwrap();
3549+
let b = Some(2).unwrap();
3550+
let c = Some(3).unwrap();
3551+
}
3552+
"#,
3553+
)
3554+
.unwrap();
3555+
3556+
let analyzer = Analyzer::new(&rust_file).unwrap();
3557+
let report = analyzer.analyze().unwrap();
3558+
3559+
let panic_points: Vec<_> = report
3560+
.weak_points
3561+
.iter()
3562+
.filter(|wp| wp.category == WeakPointCategory::PanicPath)
3563+
.collect();
3564+
3565+
assert!(
3566+
panic_points.is_empty(),
3567+
"Should NOT trigger PanicPath when unwrap count is <= 5"
3568+
);
3569+
}
3570+
}

0 commit comments

Comments
 (0)