mirror of
https://github.com/imjasonh/rustvulncheck
synced 2026-07-15 09:28:06 +00:00
Add analyze command to detect reachable vulnerable symbols in Rust projects
Implements the next phase of cargo-deep-audit: given a Rust project and the enriched vulnerability database, determine which vulnerable symbols are actually called from user code. New modules: - lockfile.rs: Parse Cargo.lock to extract dependency names and versions - scanner.rs: Scan .rs source files for references to vulnerable symbols using import tracking (use statements) and call pattern matching, with High/Medium confidence levels - analyzer.rs: Orchestrate the full pipeline — load vuln_db, match against lockfile versions (semver-aware), scan source, produce structured report Changes: - main.rs refactored into subcommands: `enrich` (Phase 1) and `analyze` (Phase 2) - db.rs/diff_analyzer.rs: Added Deserialize support to load vuln_db.json - CI workflow updated for subcommand syntax - 24 tests total (21 unit + 3 integration), all passing https://claude.ai/code/session_011sdj18ZvQYbDVwEKqrWDj1
This commit is contained in:
parent
5b9897a671
commit
b148c8d4b8
13 changed files with 1221 additions and 12 deletions
22
tests/fixtures/test_project/Cargo.lock
generated
vendored
Normal file
22
tests/fixtures/test_project/Cargo.lock
generated
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "test-app"
|
||||
version = "0.1.0"
|
||||
6
tests/fixtures/test_project/src/main.rs
vendored
Normal file
6
tests/fixtures/test_project/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use hyper::http::Request;
|
||||
|
||||
fn main() {
|
||||
let req = Request::parse(b"GET / HTTP/1.1\r\n");
|
||||
println!("{:?}", req);
|
||||
}
|
||||
50
tests/fixtures/test_vuln_db.json
vendored
Normal file
50
tests/fixtures/test_vuln_db.json
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"generated_at": "unix:1700000000",
|
||||
"entries": [
|
||||
{
|
||||
"advisory_id": "RUSTSEC-2024-0001",
|
||||
"package": "hyper",
|
||||
"title": "Lenient HTTP header parsing allows request smuggling",
|
||||
"date": "2024-01-15",
|
||||
"patched_versions": [">= 1.0.0"],
|
||||
"commit_sha": "abc123",
|
||||
"vulnerable_symbols": [
|
||||
{
|
||||
"file": "src/http/request.rs",
|
||||
"function": "hyper::http::Request::parse",
|
||||
"change_type": "Modified"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"advisory_id": "RUSTSEC-2024-0002",
|
||||
"package": "tokio",
|
||||
"title": "Race condition in task cancellation",
|
||||
"date": "2024-02-01",
|
||||
"patched_versions": [">= 1.33.0"],
|
||||
"commit_sha": "def456",
|
||||
"vulnerable_symbols": [
|
||||
{
|
||||
"file": "src/runtime/task.rs",
|
||||
"function": "tokio::runtime::task::Task::cancel",
|
||||
"change_type": "Modified"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"advisory_id": "RUSTSEC-2024-0099",
|
||||
"package": "serde",
|
||||
"title": "Fake advisory for testing",
|
||||
"date": "2024-03-01",
|
||||
"patched_versions": [">= 1.0.180"],
|
||||
"commit_sha": "ghi789",
|
||||
"vulnerable_symbols": [
|
||||
{
|
||||
"file": "src/de.rs",
|
||||
"function": "serde::de::Deserializer::deserialize",
|
||||
"change_type": "Modified"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ fn test_parse_fixture_advisories() {
|
|||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_cargo-deep-audit"))
|
||||
.args([
|
||||
"enrich",
|
||||
"--advisory-db",
|
||||
fixtures.to_str().unwrap(),
|
||||
"--limit",
|
||||
|
|
@ -43,3 +44,119 @@ fn test_parse_fixture_advisories() {
|
|||
let db: serde_json::Value = serde_json::from_str(&json_content).unwrap();
|
||||
assert!(db["entries"].as_array().unwrap().len() > 0);
|
||||
}
|
||||
|
||||
#[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");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("STDOUT:\n{}", stdout);
|
||||
println!("STDERR:\n{}", stderr);
|
||||
|
||||
// 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)
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"Should exit with non-zero when vulnerabilities found"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_clean_project() {
|
||||
// Create a temporary project with no vulnerable calls
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let src_dir = tmp.path().join("src");
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
|
||||
std::fs::write(
|
||||
tmp.path().join("Cargo.lock"),
|
||||
r#"version = 3
|
||||
|
||||
[[package]]
|
||||
name = "safe-app"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.200"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(
|
||||
src_dir.join("main.rs"),
|
||||
"fn main() { println!(\"Hello\"); }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
|
||||
let vuln_db = fixtures.join("test_vuln_db.json");
|
||||
|
||||
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(),
|
||||
])
|
||||
.output()
|
||||
.expect("failed to run binary");
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
println!("STDOUT:\n{}", stdout);
|
||||
|
||||
// Should report clean
|
||||
assert!(
|
||||
stdout.contains("CLEAN"),
|
||||
"Should report clean status: {}",
|
||||
stdout
|
||||
);
|
||||
|
||||
// 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