mirror of
https://github.com/imjasonh/rustvulncheck
synced 2026-07-18 06:48:57 +00:00
Add syn-based type tracking and golden codebase integration tests
Type tracking (type_tracker.rs): - Uses syn AST parsing to resolve variable types from function parameters, let bindings with annotations, constructor calls (Type::new()), struct literals, builder patterns, and closure parameters - Integrated into scanner to promote method-call matches from Medium to High confidence when the receiver's type is known Golden integration tests: - vulnerable_project: 4 vulnerable deps, 3 with reachable calls across multiple files (qualified calls, typed method calls, constructor-inferred types). Verifies regex dep is listed but Compiler::compile is NOT reachable. - safe_project: patched deps (hyper, smallvec) excluded, vulnerable deps (tokio, regex) listed but no symbols reachable. Reports POSSIBLY SAFE. - clean_project: no vulnerable deps at all. Reports CLEAN. 37 tests total (33 unit + 4 integration), all passing. https://claude.ai/code/session_011sdj18ZvQYbDVwEKqrWDj1
This commit is contained in:
parent
b148c8d4b8
commit
a2db0d4d15
14 changed files with 1013 additions and 83 deletions
|
|
@ -1,15 +1,36 @@
|
|||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn fixtures() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
|
||||
}
|
||||
|
||||
fn run_analyze(project: &str, db: &str) -> std::process::Output {
|
||||
let f = fixtures();
|
||||
Command::new(env!("CARGO_BIN_EXE_cargo-deep-audit"))
|
||||
.args([
|
||||
"analyze",
|
||||
"--project",
|
||||
f.join(project).to_str().unwrap(),
|
||||
"--db",
|
||||
f.join(db).to_str().unwrap(),
|
||||
])
|
||||
.output()
|
||||
.expect("failed to run binary")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 1: Enrichment pipeline tests
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_parse_fixture_advisories() {
|
||||
let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
|
||||
|
||||
let f = fixtures();
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cargo-deep-audit"))
|
||||
.args([
|
||||
"enrich",
|
||||
"--advisory-db",
|
||||
fixtures.to_str().unwrap(),
|
||||
f.to_str().unwrap(),
|
||||
"--limit",
|
||||
"10",
|
||||
"--include-all",
|
||||
|
|
@ -21,92 +42,142 @@ fn test_parse_fixture_advisories() {
|
|||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("STDOUT:\n{}", stdout);
|
||||
println!("STDERR:\n{}", stderr);
|
||||
println!("STDOUT:\n{stdout}");
|
||||
println!("STDERR:\n{stderr}");
|
||||
|
||||
// Should find all 3 fixture advisories
|
||||
assert!(
|
||||
stdout.contains("Found 3 total advisories"),
|
||||
"Expected 3 advisories, got: {}",
|
||||
stdout
|
||||
"Expected 3 advisories, got: {stdout}"
|
||||
);
|
||||
|
||||
// Should identify hyper and tokio as having GitHub refs
|
||||
assert!(stdout.contains("RUSTSEC-2024-0001"));
|
||||
assert!(stdout.contains("RUSTSEC-2024-0002"));
|
||||
assert!(stdout.contains("RUSTSEC-2024-0003"));
|
||||
|
||||
// Should write output file
|
||||
assert!(stdout.contains("Wrote enriched database to /tmp/test_vuln_db.json"));
|
||||
|
||||
// Verify JSON output is valid
|
||||
let json_content = std::fs::read_to_string("/tmp/test_vuln_db.json").unwrap();
|
||||
let db: serde_json::Value = serde_json::from_str(&json_content).unwrap();
|
||||
assert!(db["entries"].as_array().unwrap().len() > 0);
|
||||
assert!(!db["entries"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 2: Golden codebase integration tests
|
||||
// =============================================================================
|
||||
|
||||
/// Golden test: vulnerable project with multiple call patterns.
|
||||
///
|
||||
/// The vulnerable_project fixture has:
|
||||
/// - hyper 0.14.27 (vulnerable, patched >= 1.0.0) with calls to Client::encode and Decoder::decode
|
||||
/// - smallvec 1.10.0 (vulnerable, patched >= 1.11.0) with calls to SmallVec::insert_many
|
||||
/// - tokio 1.37.0 (vulnerable, patched >= 1.38.0) with calls to JoinHandle::abort
|
||||
/// - regex 1.9.6 (vulnerable, patched >= 1.10.0) — present but Compiler::compile is NOT called
|
||||
#[test]
|
||||
fn test_analyze_detects_vulnerable_call() {
|
||||
let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
|
||||
let test_project = fixtures.join("test_project");
|
||||
let vuln_db = fixtures.join("test_vuln_db.json");
|
||||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cargo-deep-audit"))
|
||||
.args([
|
||||
"analyze",
|
||||
"--project",
|
||||
test_project.to_str().unwrap(),
|
||||
"--db",
|
||||
vuln_db.to_str().unwrap(),
|
||||
])
|
||||
.output()
|
||||
.expect("failed to run binary");
|
||||
|
||||
fn golden_vulnerable_project() {
|
||||
let output = run_analyze("vulnerable_project", "golden_vuln_db.json");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("STDOUT:\n{}", stdout);
|
||||
println!("STDERR:\n{}", stderr);
|
||||
println!("STDOUT:\n{stdout}");
|
||||
|
||||
// Should find hyper and tokio as vulnerable deps (both below patched versions)
|
||||
assert!(
|
||||
stdout.contains("RUSTSEC-2024-0001"),
|
||||
"Should detect hyper advisory"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("RUSTSEC-2024-0002"),
|
||||
"Should detect tokio advisory"
|
||||
);
|
||||
|
||||
// serde 1.0.190 is above patched version >= 1.0.180, should NOT appear
|
||||
assert!(
|
||||
!stdout.contains("RUSTSEC-2024-0099"),
|
||||
"serde should not be flagged as vulnerable (version is patched)"
|
||||
);
|
||||
|
||||
// Should detect the Request::parse call in the test project
|
||||
assert!(
|
||||
stdout.contains("Request::parse"),
|
||||
"Should find the vulnerable call site: {}",
|
||||
stdout
|
||||
);
|
||||
|
||||
// Should report as VULNERABLE since a reachable symbol was found
|
||||
assert!(
|
||||
stdout.contains("VULNERABLE"),
|
||||
"Should report vulnerable status: {}",
|
||||
stdout
|
||||
);
|
||||
|
||||
// Exit code should be 1 (vulnerable)
|
||||
// --- Should exit non-zero (vulnerable) ---
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"Should exit with non-zero when vulnerabilities found"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("VULNERABLE"),
|
||||
"Should report VULNERABLE status:\n{stdout}"
|
||||
);
|
||||
|
||||
// --- Should detect all four vulnerable dependencies ---
|
||||
assert!(stdout.contains("RUSTSEC-2024-0001"), "hyper advisory missing:\n{stdout}");
|
||||
assert!(stdout.contains("RUSTSEC-2024-0010"), "smallvec advisory missing:\n{stdout}");
|
||||
assert!(stdout.contains("RUSTSEC-2024-0020"), "tokio advisory missing:\n{stdout}");
|
||||
assert!(stdout.contains("RUSTSEC-2024-0030"), "regex advisory missing:\n{stdout}");
|
||||
|
||||
// --- Should find reachable calls for hyper, smallvec, and tokio ---
|
||||
assert!(
|
||||
stdout.contains("Client::encode"),
|
||||
"Should find Client::encode call:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("insert_many"),
|
||||
"Should find SmallVec::insert_many call:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("abort"),
|
||||
"Should find JoinHandle::abort call:\n{stdout}"
|
||||
);
|
||||
|
||||
// --- regex Compiler::compile is NOT called in user code ---
|
||||
// It's an internal symbol; user code only calls Regex::new.
|
||||
// It should appear in "Vulnerable Dependencies" but NOT in "Reachable" findings.
|
||||
let reachable_section = stdout
|
||||
.split("Reachable Vulnerable Symbols")
|
||||
.nth(1)
|
||||
.unwrap_or("");
|
||||
assert!(
|
||||
!reachable_section.contains("Compiler::compile"),
|
||||
"Compiler::compile should NOT appear as reachable:\n{stdout}"
|
||||
);
|
||||
|
||||
// --- Should scan multiple source files ---
|
||||
assert!(
|
||||
stdout.contains("3 source files"),
|
||||
"Should scan all 3 .rs files:\n{stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Golden test: safe project where vulnerabilities exist in deps but aren't reachable.
|
||||
///
|
||||
/// The safe_project fixture has:
|
||||
/// - hyper 1.2.0 (PATCHED — version >= 1.0.0) → should not appear at all
|
||||
/// - smallvec 1.11.0 (PATCHED — version >= 1.11.0) → should not appear at all
|
||||
/// - tokio 1.37.0 (vulnerable) but abort() is NEVER called → dep listed, no findings
|
||||
/// - regex 1.9.6 (vulnerable) but Compiler::compile is NEVER called → dep listed, no findings
|
||||
#[test]
|
||||
fn test_analyze_clean_project() {
|
||||
// Create a temporary project with no vulnerable calls
|
||||
fn golden_safe_project() {
|
||||
let output = run_analyze("safe_project", "golden_vuln_db.json");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
println!("STDOUT:\n{stdout}");
|
||||
|
||||
// --- Should exit zero (safe / not reachable) ---
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Should exit successfully when no reachable vulns:\n{stdout}"
|
||||
);
|
||||
|
||||
// --- Patched deps should NOT appear ---
|
||||
assert!(
|
||||
!stdout.contains("RUSTSEC-2024-0001"),
|
||||
"hyper is patched (1.2.0), should not appear:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("RUSTSEC-2024-0010"),
|
||||
"smallvec is patched (1.11.0), should not appear:\n{stdout}"
|
||||
);
|
||||
|
||||
// --- Vulnerable but unreachable deps SHOULD appear in dep list ---
|
||||
assert!(
|
||||
stdout.contains("RUSTSEC-2024-0020"),
|
||||
"tokio advisory should be listed (vulnerable version):\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("RUSTSEC-2024-0030"),
|
||||
"regex advisory should be listed (vulnerable version):\n{stdout}"
|
||||
);
|
||||
|
||||
// --- But no reachable findings ---
|
||||
assert!(
|
||||
stdout.contains("POSSIBLY SAFE"),
|
||||
"Should report POSSIBLY SAFE (deps present but not reachable):\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("Reachable Vulnerable Symbols"),
|
||||
"Should NOT have reachable symbols section:\n{stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Minimal test: project with zero vulnerable dependencies.
|
||||
#[test]
|
||||
fn golden_clean_project() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let src_dir = tmp.path().join("src");
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
|
|
@ -116,7 +187,7 @@ fn test_analyze_clean_project() {
|
|||
r#"version = 3
|
||||
|
||||
[[package]]
|
||||
name = "safe-app"
|
||||
name = "clean-app"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
|
|
@ -133,30 +204,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
|
||||
let vuln_db = fixtures.join("test_vuln_db.json");
|
||||
|
||||
let f = fixtures();
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cargo-deep-audit"))
|
||||
.args([
|
||||
"analyze",
|
||||
"--project",
|
||||
tmp.path().to_str().unwrap(),
|
||||
"--db",
|
||||
vuln_db.to_str().unwrap(),
|
||||
f.join("golden_vuln_db.json").to_str().unwrap(),
|
||||
])
|
||||
.output()
|
||||
.expect("failed to run binary");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
println!("STDOUT:\n{}", stdout);
|
||||
println!("STDOUT:\n{stdout}");
|
||||
|
||||
// Should report clean
|
||||
assert!(
|
||||
stdout.contains("CLEAN"),
|
||||
"Should report clean status: {}",
|
||||
stdout
|
||||
"Should report CLEAN:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Should exit successfully when clean"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
assert!(output.status.success(), "Should exit successfully when clean");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue