use crate::core::ForgeGuardError;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyVulnerability {
pub name: String,
pub package: String,
pub version: String,
pub severity: String,
pub description: String,
pub recommended_fix: String,
}
pub struct DependencyScanner {
#[allow(dead_code)]
depth: u32,
known_vulnerabilities: Vec<KnownVulnerability>,
cache_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct KnownVulnerability {
package_pattern: String,
version_constraint: String,
severity: String,
name: String,
description: String,
fix: String,
#[serde(default)]
cve: String,
#[serde(default)]
category: String,
}
#[allow(clippy::too_many_arguments)]
fn vuln(
package: &str,
constraint: &str,
severity: &str,
name: &str,
desc: &str,
fix: &str,
cve: &str,
category: &str,
) -> KnownVulnerability {
KnownVulnerability {
package_pattern: package.to_string(),
version_constraint: constraint.to_string(),
severity: severity.to_string(),
name: name.to_string(),
description: desc.to_string(),
fix: fix.to_string(),
cve: cve.to_string(),
category: category.to_string(),
}
}
fn builtin_db() -> Vec<KnownVulnerability> {
vec![
vuln("openzeppelin-contracts", ">=2.0.0 <4.9.3", "high",
"GovernorCompatibilityBravo proposal cancellation",
"GovernorCompatibilityBravo allows anyone to cancel a proposal when the voting period has not started",
"Upgrade to OpenZeppelin Contracts 4.9.3+", "CVE-2023-34459", "Access Control"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.8.3", "high",
"ERC165Checker gas griefing",
"ERC165Checker.supportsInterface can cause gas griefing attacks with large contract addresses",
"Upgrade to OpenZeppelin Contracts 4.8.3+", "CVE-2023-30541", "Gas"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.7.2", "high",
"ERC2771Context and Multicall interaction",
"ERC2771Context and Multicall interaction can lead to lost funds with trusted forwarder pattern",
"Upgrade to OpenZeppelin Contracts 4.7.2+", "CVE-2022-39384", "Logic"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.7.0", "critical",
"ERC20/ERC721 signature replay via delegatecall",
"Signature-based permit functions vulnerable to replay attacks when used with delegatecall in proxies",
"Upgrade to OpenZeppelin Contracts 4.7.0+", "CVE-2022-35961", "Cryptography"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.6.0", "high",
"ERC1155 supply minting vulnerability",
"ERC1155._mint does not check zero address for batch minting, enabling supply inflation",
"Upgrade to OpenZeppelin Contracts 4.6.0+", "CVE-2022-35915", "Access Control"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.4.1", "critical",
"UUPSUpgradeable delegatecall to untrusted implementation",
"UUPSUpgradeable does not prevent upgradeTo on the implementation, allowing selfdestruct",
"Upgrade to OpenZeppelin Contracts 4.4.1+", "CVE-2021-46339", "Upgradeability"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.3.2", "high",
"SignatureChecker EIP-2098 compact signature issue",
"SignatureChecker does not handle EIP-2098 compact signatures, allowing malleability",
"Upgrade to OpenZeppelin Contracts 4.3.2+", "CVE-2021-41273", "Cryptography"),
vuln("openzeppelin-contracts", ">=3.4.0 <4.3.1", "high",
"TimelockController executor rights escalation",
"TimelockController allows proposers to call executeBatch with arbitrary targets",
"Upgrade to OpenZeppelin Contracts 4.3.1+", "CVE-2021-41264", "Access Control"),
vuln("openzeppelin-contracts", ">=3.0.0 <4.3.0", "high",
"ERC20 approve frontrunning race condition",
"Classic approve frontrunning attack when changing from non-zero to non-zero allowance",
"Use increaseAllowance/decreaseAllowance or upgrade to 4.3.0+", "CVE-2021-41180", "DeFi"),
vuln("openzeppelin-contracts", ">=3.0.0 <4.2.0", "high",
"ERC721 unsafe minting",
"ERC721._mint does not check recipient capability, potentially locking tokens",
"Use _safeMint or upgrade to OpenZeppelin Contracts 4.2.0+", "CVE-2021-39143", "Logic"),
vuln("openzeppelin-contracts", ">=4.0.0 <4.1.0", "medium",
"Governor proposal threshold bypass",
"Governor contract does not properly validate proposal threshold changes",
"Upgrade to OpenZeppelin Contracts 4.1.0+", "", "Access Control"),
vuln("openzeppelin-contracts", ">=2.0.0 <3.4.2", "critical",
"ProxyAdmin unauthorized ownership transfer",
"ProxyAdmin does not check msg.sender in renounceOwnership, allowing anyone to lock the proxy",
"Upgrade to OpenZeppelin Contracts 3.4.2+", "CVE-2021-34449", "Access Control"),
vuln("openzeppelin-contracts-upgradeable", ">=4.0.0 <4.7.2", "high",
"Upgradeable ERC2771Context with Multicall",
"Same as ERC2771Context vulnerability — affects upgradeable variants",
"Upgrade to OpenZeppelin Contracts Upgradeable 4.7.2+", "CVE-2022-39384", "Logic"),
vuln("openzeppelin-contracts-upgradeable", ">=4.0.0 <4.4.1", "critical",
"Upgradeable UUPS selfdestruct via delegatecall",
"Same as UUPS vulnerability — affects upgradeable implementations",
"Upgrade to OpenZeppelin Contracts Upgradeable 4.4.1+", "CVE-2021-46339", "Upgradeability"),
vuln("solmate", "<6.6.0", "medium",
"ERC4626 inflation attack",
"Solmate ERC4626 vaults vulnerable to inflation attacks via donation front-running",
"Upgrade to Solmate 6.6+ or use OpenZeppelin's ERC4626", "", "DeFi"),
vuln("solmate", "<6.4.0", "high",
"Solmate ERC721 unsafe minting",
"Solmate ERC721 does not check recipient capability before minting",
"Upgrade to Solmate 6.4+", "", "Logic"),
vuln("solmate", "<6.2.0", "medium",
"Solmate ERC20 permit signature malleability",
"Solmate ERC20 permit does not use standard EIP-2612 nonce management",
"Upgrade to Solmate 6.2+", "", "Cryptography"),
vuln("solady", "<0.0.124", "high",
"Solady ERC721 unchecked mint",
"Solady ERC721 mint does not check if recipient is a contract, potentially locking tokens",
"Upgrade to Solady 0.0.124+", "", "Logic"),
vuln("solady", "<0.0.96", "medium",
"Solady EIP-712 domain separator issue",
"Solady's optimized EIP-712 may produce incorrect domain separators on certain chains",
"Upgrade to Solady 0.0.96+", "", "Cryptography"),
vuln("@uniswap", "<3.0.0", "high",
"Uniswap V2 library unsafe cast",
"Uniswap V2 lib uses unsafe uint112 casting that can overflow",
"Upgrade to @uniswap/lib 3.0.0+ or use safe casting", "", "DeFi"),
vuln("uniswap", "<0.5.0", "high",
"Uniswap V2 oracle manipulation",
"Uniswap V2 TWAP oracle is vulnerable to manipulation with short observation windows",
"Use Uniswap V2 0.5.0+ with proper observation period", "", "DeFi"),
vuln("chainlink", "<1.10.0", "medium",
"Chainlink price feed stale data",
"Older Chainlink AggregatorV3Interface may return stale price data without freshness checks",
"Use Chainlink 1.10.0+ with proper staleness checks", "", "DeFi"),
vuln("wormhole", "<2.3.0", "critical",
"Wormhole signature verification bypass",
"Wormhole bridge signature verification can be bypassed with empty guardian set",
"Upgrade to Wormhole 2.3.0+", "CVE-2022-36119", "Cross-Chain"),
vuln("layerzero", "<1.0.0", "high",
"LayerZero message verification bypass",
"Older LayerZero endpoint allows message verification bypass via oracle spoofing",
"Upgrade to LayerZero 1.0.0+", "", "Cross-Chain"),
vuln("openzeppelin-contracts", "<3.0.0", "critical",
"Initializable uninitialized implementation",
"Initializable before v3.0.0 does not protect implementation contracts from direct calls",
"Upgrade to OpenZeppelin Contracts 3.0.0+ and call _disableInitializers()", "", "Upgradeability"),
vuln("openzeppelin-contracts", "<2.5.0", "high",
"MerkleProof signature replay",
"MerkleProof in early versions lacks domain separator, allowing cross-contract replay",
"Upgrade to OpenZeppelin Contracts 2.5.0+", "", "Cryptography"),
vuln("openzeppelin-contracts", "<2.3.0", "critical",
"SafeMath overflow in ERC20",
"SafeMath was not used in ERC20 transfers, making them vulnerable to overflow",
"Upgrade to OpenZeppelin Contracts 2.3.0+", "", "Security"),
vuln("openzeppelin-foundry-upgrades", "<0.1.0", "medium",
"Foundry Upgrades unsafe validation",
"OpenZeppelin Foundry Upgrades may not validate storage layouts correctly",
"Use OpenZeppelin Foundry Upgrades 0.1.0+", "", "Upgradeability"),
vuln("forge-std", "<1.6.0", "low",
"Forge Std Vm unsafe cheat codes",
"Older forge-std includes cheat codes exploitable if not guarded with isTest()",
"Upgrade to forge-std 1.6.0+", "", "Best Practices"),
vuln("forge-std", "<1.5.0", "medium",
"Forge Std console.log gas leakage",
"forge-std console.log calls remain in deployed bytecode, wasting gas",
"Remove console.log imports or upgrade to forge-std 1.5.0+", "", "Gas"),
vuln("prb-math", "<4.0.0", "medium",
"PRBMath unsigned integer overflow",
"PRBMath v3 and earlier may underflow on large-number division operations",
"Upgrade to PRBMath 4.0.0+ or add overflow guards", "", "Security"),
vuln("@solc", ">=0.8.0 <0.8.22", "high",
"Solc optimizer storage collision",
"Solc 0.8.0-0.8.21 optimizer can incorrectly optimize storage reads/writes",
"Use solc 0.8.22+ or disable optimizer for affected contracts", "", "Architecture"),
vuln("@solc", ">=0.8.0 <0.8.14", "high",
"Solc ABI-encoding v2 memory corruption",
"Solc 0.8.0-0.8.13 has memory corruption in ABI encoder v2 with dynamic arrays",
"Use solc 0.8.14+", "CVE-2022-37721", "Architecture"),
]
}
impl DependencyScanner {
pub fn new(depth: u32) -> Self {
Self {
depth,
known_vulnerabilities: builtin_db(),
cache_path: PathBuf::from(".forge-guard-cache/vulnerability-db.json"),
}
}
pub fn update_database(&mut self) -> Result<(), ForgeGuardError> {
let urls = [
"https://raw.githubusercontent.com/codetibo/forge-guard/main/vulnerability-db.json",
"https://raw.githubusercontent.com/codetibo/forge-guard/main/feeds/vulnerabilities.json",
];
eprintln!(" Updating vulnerability database from remote sources...");
let mut fetched = false;
for url in &urls {
match Self::fetch_url(url) {
Ok(body) => match serde_json::from_str::<Vec<KnownVulnerability>>(&body) {
Ok(remote_entries) => {
let count = remote_entries.len();
let merged =
Self::merge_databases(&self.known_vulnerabilities, &remote_entries);
self.known_vulnerabilities = merged;
if let Err(e) = self.cache_database(&remote_entries) {
eprintln!(" ⚠️ Failed to cache DB: {}", e);
}
eprintln!(
" ✅ Downloaded {} vulnerability entries from remote",
count
);
fetched = true;
break;
}
Err(e) => {
eprintln!(" ⚠️ Failed to parse remote DB from {}: {}", url, e);
}
},
Err(e) => {
eprintln!(" ⚠️ Could not fetch {}: {}", url, e);
}
}
}
if !fetched {
if self.load_cached_db() {
eprintln!(
" ✅ Loaded {} entries from local cache",
self.known_vulnerabilities.len()
);
} else {
eprintln!(
" ℹ️ Using built-in database ({} entries)",
self.known_vulnerabilities.len()
);
eprintln!(" ℹ️ Install `curl` to enable online updates");
}
}
Ok(())
}
fn fetch_url(url: &str) -> Result<String, ForgeGuardError> {
let output = std::process::Command::new("curl")
.args(["-sSL", "--max-time", "10", url])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.map_err(|e| {
ForgeGuardError::Command(format!("Failed to run curl: {}. Is curl installed?", e))
})?;
if !output.status.success() {
return Err(ForgeGuardError::Command(format!(
"curl exited with code {}",
output.status.code().unwrap_or(-1)
)));
}
let body = String::from_utf8_lossy(&output.stdout).to_string();
if body.is_empty() {
return Err(ForgeGuardError::Command(
"Empty response from remote source".into(),
));
}
Ok(body)
}
fn merge_databases(
builtin: &[KnownVulnerability],
remote: &[KnownVulnerability],
) -> Vec<KnownVulnerability> {
let mut merged: Vec<KnownVulnerability> = builtin.to_vec();
'remote: for remote_entry in remote {
for existing in merged.iter_mut() {
if existing.name == remote_entry.name
&& existing.package_pattern == remote_entry.package_pattern
{
*existing = remote_entry.clone();
continue 'remote;
}
}
merged.push(remote_entry.clone());
}
merged
}
fn cache_database(&self, entries: &[KnownVulnerability]) -> Result<(), ForgeGuardError> {
if let Some(parent) = self.cache_path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(entries)?;
std::fs::write(&self.cache_path, json)?;
Ok(())
}
fn load_cached_db(&mut self) -> bool {
if !self.cache_path.exists() {
return false;
}
match std::fs::read_to_string(&self.cache_path) {
Ok(content) => match serde_json::from_str::<Vec<KnownVulnerability>>(&content) {
Ok(entries) => {
self.known_vulnerabilities = Self::merge_databases(&builtin_db(), &entries);
true
}
Err(_) => false,
},
Err(_) => false,
}
}
pub fn scan(&self) -> Result<Vec<DependencyVulnerability>, ForgeGuardError> {
let mut vulnerabilities = Vec::new();
let mut dep_lines = Vec::new();
if let Ok(content) = std::fs::read_to_string("remappings.txt") {
for line in content.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with('#') && !line.starts_with("//") {
dep_lines.push(line.to_string());
}
}
}
if let Ok(content) = std::fs::read_to_string("foundry.toml") {
for line in content.lines() {
let line = line.trim();
if line.starts_with("remappings") || (line.contains('=') && line.contains('@')) {
dep_lines.push(line.to_string());
}
}
}
if let Ok(entries) = std::fs::read_dir("lib") {
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let name = entry.file_name().to_string_lossy().to_string();
dep_lines.push(format!("{}=lib/{}", name, name));
}
}
}
for line in &dep_lines {
vulnerabilities.extend(self.check_line(line));
}
vulnerabilities.extend(self.check_implicit_patterns()?);
vulnerabilities.sort_by(|a, b| a.name.cmp(&b.name).then(a.package.cmp(&b.package)));
vulnerabilities.dedup_by(|a, b| a.name == b.name && a.package == b.package);
Ok(vulnerabilities)
}
fn check_line(&self, line: &str) -> Vec<DependencyVulnerability> {
let mut results = Vec::new();
for vuln in &self.known_vulnerabilities {
if line.contains(&vuln.package_pattern) {
results.push(DependencyVulnerability {
name: vuln.name.clone(),
package: vuln.package_pattern.clone(),
version: vuln.version_constraint.clone(),
severity: vuln.severity.clone(),
description: vuln.description.clone(),
recommended_fix: vuln.fix.clone(),
});
}
}
results
}
fn check_implicit_patterns(&self) -> Result<Vec<DependencyVulnerability>, ForgeGuardError> {
let mut results = Vec::new();
if let Ok(content) = std::fs::read_to_string("foundry.toml") {
for line in content.lines() {
let line = line.trim();
if let Some(version_str) = line
.strip_prefix("solc")
.or_else(|| line.strip_prefix("solc_version"))
.or_else(|| line.strip_prefix("solc-version"))
{
let version_str = version_str
.trim()
.trim_start_matches('=')
.trim()
.trim_matches('"')
.trim_matches('\'');
results.extend(self.check_solc_version(version_str));
} else if let Some(remapping_target) = line.split('=').nth(1) {
let target = remapping_target.trim();
for vuln in &self.known_vulnerabilities {
if target.contains(&vuln.package_pattern) {
results.push(DependencyVulnerability {
name: vuln.name.clone(),
package: vuln.package_pattern.clone(),
version: vuln.version_constraint.clone(),
severity: vuln.severity.clone(),
description: vuln.description.clone(),
recommended_fix: vuln.fix.clone(),
});
}
}
}
}
}
results.sort_by(|a, b| a.name.cmp(&b.name));
results.dedup_by(|a, b| a.name == b.name);
Ok(results)
}
fn check_solc_version(&self, version: &str) -> Vec<DependencyVulnerability> {
let mut results = Vec::new();
for vuln in &self.known_vulnerabilities {
if vuln.package_pattern == "@solc"
&& self.matches_constraint(version, &vuln.version_constraint)
{
results.push(DependencyVulnerability {
name: vuln.name.clone(),
package: vuln.package_pattern.clone(),
version: vuln.version_constraint.clone(),
severity: vuln.severity.clone(),
description: vuln.description.clone(),
recommended_fix: vuln.fix.clone(),
});
}
}
results
}
fn matches_constraint(&self, version: &str, constraint: &str) -> bool {
let version_parts = parse_version(version);
if version_parts.is_empty() {
return false;
}
let parts: Vec<&str> = constraint.split_whitespace().collect();
if parts.len() == 1 {
let op = parts[0];
if let Some(target) = op.strip_prefix('<') {
let target_parts = parse_version(target);
if target_parts.is_empty() {
return false;
}
return compare_versions(&version_parts, &target_parts) == std::cmp::Ordering::Less;
} else if let Some(target) = op.strip_prefix("<=") {
let target_parts = parse_version(target);
if target_parts.is_empty() {
return false;
}
return compare_versions(&version_parts, &target_parts)
!= std::cmp::Ordering::Greater;
}
return true;
}
if parts.len() >= 2 {
let lower = parts[0];
let upper = parts[1];
let lower_satisfied = if let Some(target) = lower.strip_prefix(">=") {
let target_parts = parse_version(target);
if target_parts.is_empty() {
true
} else {
compare_versions(&version_parts, &target_parts) != std::cmp::Ordering::Less
}
} else if let Some(target) = lower.strip_prefix('>') {
let target_parts = parse_version(target);
if target_parts.is_empty() {
true
} else {
compare_versions(&version_parts, &target_parts) == std::cmp::Ordering::Greater
}
} else {
true
};
let upper_satisfied = if let Some(target) = upper.strip_prefix('<') {
let target_parts = parse_version(target);
if target_parts.is_empty() {
true
} else {
compare_versions(&version_parts, &target_parts) == std::cmp::Ordering::Less
}
} else if let Some(target) = upper.strip_prefix("<=") {
let target_parts = parse_version(target);
if target_parts.is_empty() {
true
} else {
compare_versions(&version_parts, &target_parts) != std::cmp::Ordering::Greater
}
} else {
true
};
return lower_satisfied && upper_satisfied;
}
true
}
pub fn db_size(&self) -> usize {
self.known_vulnerabilities.len()
}
}
fn parse_version(v: &str) -> Vec<u32> {
v.trim()
.split('.')
.filter_map(|s| s.parse::<u32>().ok())
.collect()
}
fn compare_versions(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
let max_len = a.len().max(b.len());
for i in 0..max_len {
let a_val = a.get(i).copied().unwrap_or(0);
let b_val = b.get(i).copied().unwrap_or(0);
match a_val.cmp(&b_val) {
std::cmp::Ordering::Equal => continue,
other => return other,
}
}
std::cmp::Ordering::Equal
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version() {
assert_eq!(parse_version("4.9.3"), vec![4, 9, 3]);
assert_eq!(parse_version("6.6"), vec![6, 6]);
assert_eq!(parse_version("0.8.22"), vec![0, 8, 22]);
assert_eq!(parse_version(""), vec![] as Vec<u32>);
assert_eq!(parse_version("invalid"), vec![] as Vec<u32>);
}
#[test]
fn test_compare_versions() {
assert_eq!(
compare_versions(&[4, 9, 3], &[4, 9, 3]),
std::cmp::Ordering::Equal
);
assert_eq!(
compare_versions(&[4, 9, 2], &[4, 9, 3]),
std::cmp::Ordering::Less
);
assert_eq!(
compare_versions(&[4, 10, 0], &[4, 9, 3]),
std::cmp::Ordering::Greater
);
assert_eq!(
compare_versions(&[6, 6], &[6, 5, 0]),
std::cmp::Ordering::Greater
);
}
#[test]
fn test_matches_constraint_less_than() {
let scanner = DependencyScanner::new(1);
assert!(scanner.matches_constraint("4.9.2", "<4.9.3"));
assert!(!scanner.matches_constraint("4.9.3", "<4.9.3"));
assert!(!scanner.matches_constraint("4.10.0", "<4.9.3"));
}
#[test]
fn test_matches_constraint_range() {
let scanner = DependencyScanner::new(1);
assert!(scanner.matches_constraint("4.5.0", ">=4.0.0 <5.0.0"));
assert!(scanner.matches_constraint("4.0.0", ">=4.0.0 <5.0.0"));
assert!(!scanner.matches_constraint("3.9.9", ">=4.0.0 <5.0.0"));
assert!(!scanner.matches_constraint("5.0.0", ">=4.0.0 <5.0.0"));
}
#[test]
fn test_scanner_creation() {
let scanner = DependencyScanner::new(0);
assert!(scanner.db_size() >= 30);
assert_eq!(scanner.depth, 0);
}
#[test]
fn test_builtin_db_not_empty() {
let db = builtin_db();
assert!(!db.is_empty());
assert!(db.len() >= 30);
}
#[test]
fn test_check_solc_version_vulnerable() {
let scanner = DependencyScanner::new(1);
let results = scanner.check_solc_version("0.8.13");
assert!(results.iter().any(|v| v.name.contains("ABI-encoding")));
}
#[test]
fn test_check_solc_version_safe() {
let scanner = DependencyScanner::new(1);
let results = scanner.check_solc_version("0.8.22");
assert!(!results.iter().any(|v| v.name.contains("ABI-encoding")));
}
#[test]
fn test_check_line_matches_openzeppelin() {
let scanner = DependencyScanner::new(1);
let line = "@openzeppelin/contracts=lib/openzeppelin-contracts";
let results = scanner.check_line(line);
assert!(!results.is_empty());
assert!(results.iter().any(|v| v.package.contains("openzeppelin")));
}
#[test]
fn test_check_line_solmate() {
let scanner = DependencyScanner::new(1);
let line = "solmate=lib/solmate";
let results = scanner.check_line(line);
assert!(!results.is_empty());
assert!(results.iter().any(|v| v.package.contains("solmate")));
}
#[test]
fn test_check_line_no_match() {
let scanner = DependencyScanner::new(1);
let line = "my-custom-lib=lib/my-custom-lib";
let results = scanner.check_line(line);
assert!(results.is_empty());
}
#[test]
fn test_merge_databases_with_override() {
let builtin = vec![KnownVulnerability {
package_pattern: "test-pkg".to_string(),
version_constraint: "<1.0.0".to_string(),
severity: "high".to_string(),
name: "Test Vuln".to_string(),
description: "old desc".to_string(),
fix: "upgrade".to_string(),
cve: String::new(),
category: String::new(),
}];
let remote = vec![KnownVulnerability {
package_pattern: "test-pkg".to_string(),
version_constraint: "<2.0.0".to_string(),
severity: "critical".to_string(),
name: "Test Vuln".to_string(),
description: "new desc".to_string(),
fix: "upgrade".to_string(),
cve: String::new(),
category: String::new(),
}];
let merged = DependencyScanner::merge_databases(&builtin, &remote);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].severity, "critical");
}
#[test]
fn test_cache_roundtrip() {
let scanner = DependencyScanner::new(1);
let entries = vec![KnownVulnerability {
package_pattern: "cache-test".to_string(),
version_constraint: "<1.0.0".to_string(),
severity: "low".to_string(),
name: "Cache Test".to_string(),
description: "Testing cache".to_string(),
fix: "nothing".to_string(),
cve: String::new(),
category: String::new(),
}];
assert!(scanner.cache_database(&entries).is_ok());
let mut scanner2 = DependencyScanner::new(1);
assert!(scanner2.load_cached_db());
assert!(scanner2
.known_vulnerabilities
.iter()
.any(|v| v.name == "Cache Test"));
let _ = std::fs::remove_file(&scanner.cache_path);
}
#[test]
fn test_solc_version_non_matching() {
let scanner = DependencyScanner::new(1);
let results = scanner.check_solc_version("1.0.0");
assert!(results.is_empty());
}
}