1use crate::core::ForgeGuardError;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct DependencyVulnerability {
14 pub name: String,
15 pub package: String,
16 pub version: String,
17 pub severity: String,
18 pub description: String,
19 pub recommended_fix: String,
20}
21
22pub struct DependencyScanner {
24 #[allow(dead_code)]
25 depth: u32,
26 known_vulnerabilities: Vec<KnownVulnerability>,
27 cache_path: PathBuf,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33struct KnownVulnerability {
34 package_pattern: String,
36 version_constraint: String,
38 severity: String,
40 name: String,
42 description: String,
44 fix: String,
46 #[serde(default)]
48 cve: String,
49 #[serde(default)]
51 category: String,
52}
53
54#[allow(clippy::too_many_arguments)]
56fn vuln(
57 package: &str,
58 constraint: &str,
59 severity: &str,
60 name: &str,
61 desc: &str,
62 fix: &str,
63 cve: &str,
64 category: &str,
65) -> KnownVulnerability {
66 KnownVulnerability {
67 package_pattern: package.to_string(),
68 version_constraint: constraint.to_string(),
69 severity: severity.to_string(),
70 name: name.to_string(),
71 description: desc.to_string(),
72 fix: fix.to_string(),
73 cve: cve.to_string(),
74 category: category.to_string(),
75 }
76}
77
78fn builtin_db() -> Vec<KnownVulnerability> {
80 vec![
81 vuln("openzeppelin-contracts", ">=2.0.0 <4.9.3", "high",
83 "GovernorCompatibilityBravo proposal cancellation",
84 "GovernorCompatibilityBravo allows anyone to cancel a proposal when the voting period has not started",
85 "Upgrade to OpenZeppelin Contracts 4.9.3+", "CVE-2023-34459", "Access Control"),
86 vuln("openzeppelin-contracts", ">=4.0.0 <4.8.3", "high",
87 "ERC165Checker gas griefing",
88 "ERC165Checker.supportsInterface can cause gas griefing attacks with large contract addresses",
89 "Upgrade to OpenZeppelin Contracts 4.8.3+", "CVE-2023-30541", "Gas"),
90 vuln("openzeppelin-contracts", ">=4.0.0 <4.7.2", "high",
91 "ERC2771Context and Multicall interaction",
92 "ERC2771Context and Multicall interaction can lead to lost funds with trusted forwarder pattern",
93 "Upgrade to OpenZeppelin Contracts 4.7.2+", "CVE-2022-39384", "Logic"),
94 vuln("openzeppelin-contracts", ">=4.0.0 <4.7.0", "critical",
95 "ERC20/ERC721 signature replay via delegatecall",
96 "Signature-based permit functions vulnerable to replay attacks when used with delegatecall in proxies",
97 "Upgrade to OpenZeppelin Contracts 4.7.0+", "CVE-2022-35961", "Cryptography"),
98 vuln("openzeppelin-contracts", ">=4.0.0 <4.6.0", "high",
99 "ERC1155 supply minting vulnerability",
100 "ERC1155._mint does not check zero address for batch minting, enabling supply inflation",
101 "Upgrade to OpenZeppelin Contracts 4.6.0+", "CVE-2022-35915", "Access Control"),
102 vuln("openzeppelin-contracts", ">=4.0.0 <4.4.1", "critical",
103 "UUPSUpgradeable delegatecall to untrusted implementation",
104 "UUPSUpgradeable does not prevent upgradeTo on the implementation, allowing selfdestruct",
105 "Upgrade to OpenZeppelin Contracts 4.4.1+", "CVE-2021-46339", "Upgradeability"),
106 vuln("openzeppelin-contracts", ">=4.0.0 <4.3.2", "high",
107 "SignatureChecker EIP-2098 compact signature issue",
108 "SignatureChecker does not handle EIP-2098 compact signatures, allowing malleability",
109 "Upgrade to OpenZeppelin Contracts 4.3.2+", "CVE-2021-41273", "Cryptography"),
110 vuln("openzeppelin-contracts", ">=3.4.0 <4.3.1", "high",
111 "TimelockController executor rights escalation",
112 "TimelockController allows proposers to call executeBatch with arbitrary targets",
113 "Upgrade to OpenZeppelin Contracts 4.3.1+", "CVE-2021-41264", "Access Control"),
114 vuln("openzeppelin-contracts", ">=3.0.0 <4.3.0", "high",
115 "ERC20 approve frontrunning race condition",
116 "Classic approve frontrunning attack when changing from non-zero to non-zero allowance",
117 "Use increaseAllowance/decreaseAllowance or upgrade to 4.3.0+", "CVE-2021-41180", "DeFi"),
118 vuln("openzeppelin-contracts", ">=3.0.0 <4.2.0", "high",
119 "ERC721 unsafe minting",
120 "ERC721._mint does not check recipient capability, potentially locking tokens",
121 "Use _safeMint or upgrade to OpenZeppelin Contracts 4.2.0+", "CVE-2021-39143", "Logic"),
122 vuln("openzeppelin-contracts", ">=4.0.0 <4.1.0", "medium",
123 "Governor proposal threshold bypass",
124 "Governor contract does not properly validate proposal threshold changes",
125 "Upgrade to OpenZeppelin Contracts 4.1.0+", "", "Access Control"),
126 vuln("openzeppelin-contracts", ">=2.0.0 <3.4.2", "critical",
127 "ProxyAdmin unauthorized ownership transfer",
128 "ProxyAdmin does not check msg.sender in renounceOwnership, allowing anyone to lock the proxy",
129 "Upgrade to OpenZeppelin Contracts 3.4.2+", "CVE-2021-34449", "Access Control"),
130
131 vuln("openzeppelin-contracts-upgradeable", ">=4.0.0 <4.7.2", "high",
133 "Upgradeable ERC2771Context with Multicall",
134 "Same as ERC2771Context vulnerability — affects upgradeable variants",
135 "Upgrade to OpenZeppelin Contracts Upgradeable 4.7.2+", "CVE-2022-39384", "Logic"),
136 vuln("openzeppelin-contracts-upgradeable", ">=4.0.0 <4.4.1", "critical",
137 "Upgradeable UUPS selfdestruct via delegatecall",
138 "Same as UUPS vulnerability — affects upgradeable implementations",
139 "Upgrade to OpenZeppelin Contracts Upgradeable 4.4.1+", "CVE-2021-46339", "Upgradeability"),
140
141 vuln("solmate", "<6.6.0", "medium",
143 "ERC4626 inflation attack",
144 "Solmate ERC4626 vaults vulnerable to inflation attacks via donation front-running",
145 "Upgrade to Solmate 6.6+ or use OpenZeppelin's ERC4626", "", "DeFi"),
146 vuln("solmate", "<6.4.0", "high",
147 "Solmate ERC721 unsafe minting",
148 "Solmate ERC721 does not check recipient capability before minting",
149 "Upgrade to Solmate 6.4+", "", "Logic"),
150 vuln("solmate", "<6.2.0", "medium",
151 "Solmate ERC20 permit signature malleability",
152 "Solmate ERC20 permit does not use standard EIP-2612 nonce management",
153 "Upgrade to Solmate 6.2+", "", "Cryptography"),
154
155 vuln("solady", "<0.0.124", "high",
157 "Solady ERC721 unchecked mint",
158 "Solady ERC721 mint does not check if recipient is a contract, potentially locking tokens",
159 "Upgrade to Solady 0.0.124+", "", "Logic"),
160 vuln("solady", "<0.0.96", "medium",
161 "Solady EIP-712 domain separator issue",
162 "Solady's optimized EIP-712 may produce incorrect domain separators on certain chains",
163 "Upgrade to Solady 0.0.96+", "", "Cryptography"),
164
165 vuln("@uniswap", "<3.0.0", "high",
167 "Uniswap V2 library unsafe cast",
168 "Uniswap V2 lib uses unsafe uint112 casting that can overflow",
169 "Upgrade to @uniswap/lib 3.0.0+ or use safe casting", "", "DeFi"),
170 vuln("uniswap", "<0.5.0", "high",
171 "Uniswap V2 oracle manipulation",
172 "Uniswap V2 TWAP oracle is vulnerable to manipulation with short observation windows",
173 "Use Uniswap V2 0.5.0+ with proper observation period", "", "DeFi"),
174
175 vuln("chainlink", "<1.10.0", "medium",
177 "Chainlink price feed stale data",
178 "Older Chainlink AggregatorV3Interface may return stale price data without freshness checks",
179 "Use Chainlink 1.10.0+ with proper staleness checks", "", "DeFi"),
180
181 vuln("wormhole", "<2.3.0", "critical",
183 "Wormhole signature verification bypass",
184 "Wormhole bridge signature verification can be bypassed with empty guardian set",
185 "Upgrade to Wormhole 2.3.0+", "CVE-2022-36119", "Cross-Chain"),
186
187 vuln("layerzero", "<1.0.0", "high",
189 "LayerZero message verification bypass",
190 "Older LayerZero endpoint allows message verification bypass via oracle spoofing",
191 "Upgrade to LayerZero 1.0.0+", "", "Cross-Chain"),
192
193 vuln("openzeppelin-contracts", "<3.0.0", "critical",
195 "Initializable uninitialized implementation",
196 "Initializable before v3.0.0 does not protect implementation contracts from direct calls",
197 "Upgrade to OpenZeppelin Contracts 3.0.0+ and call _disableInitializers()", "", "Upgradeability"),
198 vuln("openzeppelin-contracts", "<2.5.0", "high",
199 "MerkleProof signature replay",
200 "MerkleProof in early versions lacks domain separator, allowing cross-contract replay",
201 "Upgrade to OpenZeppelin Contracts 2.5.0+", "", "Cryptography"),
202 vuln("openzeppelin-contracts", "<2.3.0", "critical",
203 "SafeMath overflow in ERC20",
204 "SafeMath was not used in ERC20 transfers, making them vulnerable to overflow",
205 "Upgrade to OpenZeppelin Contracts 2.3.0+", "", "Security"),
206
207 vuln("openzeppelin-foundry-upgrades", "<0.1.0", "medium",
209 "Foundry Upgrades unsafe validation",
210 "OpenZeppelin Foundry Upgrades may not validate storage layouts correctly",
211 "Use OpenZeppelin Foundry Upgrades 0.1.0+", "", "Upgradeability"),
212
213 vuln("forge-std", "<1.6.0", "low",
215 "Forge Std Vm unsafe cheat codes",
216 "Older forge-std includes cheat codes exploitable if not guarded with isTest()",
217 "Upgrade to forge-std 1.6.0+", "", "Best Practices"),
218 vuln("forge-std", "<1.5.0", "medium",
219 "Forge Std console.log gas leakage",
220 "forge-std console.log calls remain in deployed bytecode, wasting gas",
221 "Remove console.log imports or upgrade to forge-std 1.5.0+", "", "Gas"),
222
223 vuln("prb-math", "<4.0.0", "medium",
225 "PRBMath unsigned integer overflow",
226 "PRBMath v3 and earlier may underflow on large-number division operations",
227 "Upgrade to PRBMath 4.0.0+ or add overflow guards", "", "Security"),
228
229 vuln("@solc", ">=0.8.0 <0.8.22", "high",
231 "Solc optimizer storage collision",
232 "Solc 0.8.0-0.8.21 optimizer can incorrectly optimize storage reads/writes",
233 "Use solc 0.8.22+ or disable optimizer for affected contracts", "", "Architecture"),
234 vuln("@solc", ">=0.8.0 <0.8.14", "high",
235 "Solc ABI-encoding v2 memory corruption",
236 "Solc 0.8.0-0.8.13 has memory corruption in ABI encoder v2 with dynamic arrays",
237 "Use solc 0.8.14+", "CVE-2022-37721", "Architecture"),
238 ]
239}
240
241impl DependencyScanner {
242 pub fn new(depth: u32) -> Self {
244 Self {
245 depth,
246 known_vulnerabilities: builtin_db(),
247 cache_path: PathBuf::from(".forge-guard-cache/vulnerability-db.json"),
248 }
249 }
250
251 pub fn update_database(&mut self) -> Result<(), ForgeGuardError> {
257 let urls = [
258 "https://raw.githubusercontent.com/codetibo/forge-guard/main/vulnerability-db.json",
259 "https://raw.githubusercontent.com/codetibo/forge-guard/main/feeds/vulnerabilities.json",
260 ];
261
262 eprintln!(" Updating vulnerability database from remote sources...");
263
264 let mut fetched = false;
265 for url in &urls {
266 match Self::fetch_url(url) {
267 Ok(body) => match serde_json::from_str::<Vec<KnownVulnerability>>(&body) {
268 Ok(remote_entries) => {
269 let count = remote_entries.len();
270 let merged =
271 Self::merge_databases(&self.known_vulnerabilities, &remote_entries);
272 self.known_vulnerabilities = merged;
273 if let Err(e) = self.cache_database(&remote_entries) {
274 eprintln!(" ⚠️ Failed to cache DB: {}", e);
275 }
276 eprintln!(
277 " ✅ Downloaded {} vulnerability entries from remote",
278 count
279 );
280 fetched = true;
281 break;
282 }
283 Err(e) => {
284 eprintln!(" ⚠️ Failed to parse remote DB from {}: {}", url, e);
285 }
286 },
287 Err(e) => {
288 eprintln!(" ⚠️ Could not fetch {}: {}", url, e);
289 }
290 }
291 }
292
293 if !fetched {
294 if self.load_cached_db() {
295 eprintln!(
296 " ✅ Loaded {} entries from local cache",
297 self.known_vulnerabilities.len()
298 );
299 } else {
300 eprintln!(
301 " ℹ️ Using built-in database ({} entries)",
302 self.known_vulnerabilities.len()
303 );
304 eprintln!(" ℹ️ Install `curl` to enable online updates");
305 }
306 }
307
308 Ok(())
309 }
310
311 fn fetch_url(url: &str) -> Result<String, ForgeGuardError> {
313 let output = std::process::Command::new("curl")
314 .args(["-sSL", "--max-time", "10", url])
315 .stdout(std::process::Stdio::piped())
316 .stderr(std::process::Stdio::null())
317 .output()
318 .map_err(|e| {
319 ForgeGuardError::Command(format!("Failed to run curl: {}. Is curl installed?", e))
320 })?;
321
322 if !output.status.success() {
323 return Err(ForgeGuardError::Command(format!(
324 "curl exited with code {}",
325 output.status.code().unwrap_or(-1)
326 )));
327 }
328
329 let body = String::from_utf8_lossy(&output.stdout).to_string();
330 if body.is_empty() {
331 return Err(ForgeGuardError::Command(
332 "Empty response from remote source".into(),
333 ));
334 }
335 Ok(body)
336 }
337
338 fn merge_databases(
340 builtin: &[KnownVulnerability],
341 remote: &[KnownVulnerability],
342 ) -> Vec<KnownVulnerability> {
343 let mut merged: Vec<KnownVulnerability> = builtin.to_vec();
344
345 'remote: for remote_entry in remote {
346 for existing in merged.iter_mut() {
347 if existing.name == remote_entry.name
348 && existing.package_pattern == remote_entry.package_pattern
349 {
350 *existing = remote_entry.clone();
351 continue 'remote;
352 }
353 }
354 merged.push(remote_entry.clone());
355 }
356 merged
357 }
358
359 fn cache_database(&self, entries: &[KnownVulnerability]) -> Result<(), ForgeGuardError> {
361 if let Some(parent) = self.cache_path.parent() {
362 std::fs::create_dir_all(parent)?;
363 }
364 let json = serde_json::to_string_pretty(entries)?;
365 std::fs::write(&self.cache_path, json)?;
366 Ok(())
367 }
368
369 fn load_cached_db(&mut self) -> bool {
371 if !self.cache_path.exists() {
372 return false;
373 }
374 match std::fs::read_to_string(&self.cache_path) {
375 Ok(content) => match serde_json::from_str::<Vec<KnownVulnerability>>(&content) {
376 Ok(entries) => {
377 self.known_vulnerabilities = Self::merge_databases(&builtin_db(), &entries);
378 true
379 }
380 Err(_) => false,
381 },
382 Err(_) => false,
383 }
384 }
385
386 pub fn scan(&self) -> Result<Vec<DependencyVulnerability>, ForgeGuardError> {
388 let mut vulnerabilities = Vec::new();
389 let mut dep_lines = Vec::new();
390
391 if let Ok(content) = std::fs::read_to_string("remappings.txt") {
393 for line in content.lines() {
394 let line = line.trim();
395 if !line.is_empty() && !line.starts_with('#') && !line.starts_with("//") {
396 dep_lines.push(line.to_string());
397 }
398 }
399 }
400
401 if let Ok(content) = std::fs::read_to_string("foundry.toml") {
403 for line in content.lines() {
404 let line = line.trim();
405 if line.starts_with("remappings") || (line.contains('=') && line.contains('@')) {
406 dep_lines.push(line.to_string());
407 }
408 }
409 }
410
411 if let Ok(entries) = std::fs::read_dir("lib") {
413 for entry in entries.flatten() {
414 if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
415 let name = entry.file_name().to_string_lossy().to_string();
416 dep_lines.push(format!("{}=lib/{}", name, name));
417 }
418 }
419 }
420
421 for line in &dep_lines {
423 vulnerabilities.extend(self.check_line(line));
424 }
425
426 vulnerabilities.extend(self.check_implicit_patterns()?);
428
429 vulnerabilities.sort_by(|a, b| a.name.cmp(&b.name).then(a.package.cmp(&b.package)));
431 vulnerabilities.dedup_by(|a, b| a.name == b.name && a.package == b.package);
432
433 Ok(vulnerabilities)
434 }
435
436 fn check_line(&self, line: &str) -> Vec<DependencyVulnerability> {
438 let mut results = Vec::new();
439 for vuln in &self.known_vulnerabilities {
440 if line.contains(&vuln.package_pattern) {
441 results.push(DependencyVulnerability {
442 name: vuln.name.clone(),
443 package: vuln.package_pattern.clone(),
444 version: vuln.version_constraint.clone(),
445 severity: vuln.severity.clone(),
446 description: vuln.description.clone(),
447 recommended_fix: vuln.fix.clone(),
448 });
449 }
450 }
451 results
452 }
453
454 fn check_implicit_patterns(&self) -> Result<Vec<DependencyVulnerability>, ForgeGuardError> {
456 let mut results = Vec::new();
457 if let Ok(content) = std::fs::read_to_string("foundry.toml") {
458 for line in content.lines() {
459 let line = line.trim();
460 if let Some(version_str) = line
461 .strip_prefix("solc")
462 .or_else(|| line.strip_prefix("solc_version"))
463 .or_else(|| line.strip_prefix("solc-version"))
464 {
465 let version_str = version_str
466 .trim()
467 .trim_start_matches('=')
468 .trim()
469 .trim_matches('"')
470 .trim_matches('\'');
471 results.extend(self.check_solc_version(version_str));
472 } else if let Some(remapping_target) = line.split('=').nth(1) {
473 let target = remapping_target.trim();
474 for vuln in &self.known_vulnerabilities {
475 if target.contains(&vuln.package_pattern) {
476 results.push(DependencyVulnerability {
477 name: vuln.name.clone(),
478 package: vuln.package_pattern.clone(),
479 version: vuln.version_constraint.clone(),
480 severity: vuln.severity.clone(),
481 description: vuln.description.clone(),
482 recommended_fix: vuln.fix.clone(),
483 });
484 }
485 }
486 }
487 }
488 }
489 results.sort_by(|a, b| a.name.cmp(&b.name));
490 results.dedup_by(|a, b| a.name == b.name);
491 Ok(results)
492 }
493
494 fn check_solc_version(&self, version: &str) -> Vec<DependencyVulnerability> {
496 let mut results = Vec::new();
497 for vuln in &self.known_vulnerabilities {
498 if vuln.package_pattern == "@solc"
499 && self.matches_constraint(version, &vuln.version_constraint)
500 {
501 results.push(DependencyVulnerability {
502 name: vuln.name.clone(),
503 package: vuln.package_pattern.clone(),
504 version: vuln.version_constraint.clone(),
505 severity: vuln.severity.clone(),
506 description: vuln.description.clone(),
507 recommended_fix: vuln.fix.clone(),
508 });
509 }
510 }
511 results
512 }
513
514 fn matches_constraint(&self, version: &str, constraint: &str) -> bool {
516 let version_parts = parse_version(version);
517 if version_parts.is_empty() {
518 return false;
519 }
520 let parts: Vec<&str> = constraint.split_whitespace().collect();
521
522 if parts.len() == 1 {
523 let op = parts[0];
524 if let Some(target) = op.strip_prefix('<') {
525 let target_parts = parse_version(target);
526 if target_parts.is_empty() {
527 return false;
528 }
529 return compare_versions(&version_parts, &target_parts) == std::cmp::Ordering::Less;
530 } else if let Some(target) = op.strip_prefix("<=") {
531 let target_parts = parse_version(target);
532 if target_parts.is_empty() {
533 return false;
534 }
535 return compare_versions(&version_parts, &target_parts)
536 != std::cmp::Ordering::Greater;
537 }
538 return true;
539 }
540
541 if parts.len() >= 2 {
542 let lower = parts[0];
543 let upper = parts[1];
544
545 let lower_satisfied = if let Some(target) = lower.strip_prefix(">=") {
546 let target_parts = parse_version(target);
547 if target_parts.is_empty() {
548 true
549 } else {
550 compare_versions(&version_parts, &target_parts) != std::cmp::Ordering::Less
551 }
552 } else if let Some(target) = lower.strip_prefix('>') {
553 let target_parts = parse_version(target);
554 if target_parts.is_empty() {
555 true
556 } else {
557 compare_versions(&version_parts, &target_parts) == std::cmp::Ordering::Greater
558 }
559 } else {
560 true
561 };
562
563 let upper_satisfied = if let Some(target) = upper.strip_prefix('<') {
564 let target_parts = parse_version(target);
565 if target_parts.is_empty() {
566 true
567 } else {
568 compare_versions(&version_parts, &target_parts) == std::cmp::Ordering::Less
569 }
570 } else if let Some(target) = upper.strip_prefix("<=") {
571 let target_parts = parse_version(target);
572 if target_parts.is_empty() {
573 true
574 } else {
575 compare_versions(&version_parts, &target_parts) != std::cmp::Ordering::Greater
576 }
577 } else {
578 true
579 };
580
581 return lower_satisfied && upper_satisfied;
582 }
583 true
584 }
585
586 pub fn db_size(&self) -> usize {
588 self.known_vulnerabilities.len()
589 }
590}
591
592fn parse_version(v: &str) -> Vec<u32> {
594 v.trim()
595 .split('.')
596 .filter_map(|s| s.parse::<u32>().ok())
597 .collect()
598}
599
600fn compare_versions(a: &[u32], b: &[u32]) -> std::cmp::Ordering {
602 let max_len = a.len().max(b.len());
603 for i in 0..max_len {
604 let a_val = a.get(i).copied().unwrap_or(0);
605 let b_val = b.get(i).copied().unwrap_or(0);
606 match a_val.cmp(&b_val) {
607 std::cmp::Ordering::Equal => continue,
608 other => return other,
609 }
610 }
611 std::cmp::Ordering::Equal
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
619 fn test_parse_version() {
620 assert_eq!(parse_version("4.9.3"), vec![4, 9, 3]);
621 assert_eq!(parse_version("6.6"), vec![6, 6]);
622 assert_eq!(parse_version("0.8.22"), vec![0, 8, 22]);
623 assert_eq!(parse_version(""), vec![] as Vec<u32>);
624 assert_eq!(parse_version("invalid"), vec![] as Vec<u32>);
625 }
626
627 #[test]
628 fn test_compare_versions() {
629 assert_eq!(
630 compare_versions(&[4, 9, 3], &[4, 9, 3]),
631 std::cmp::Ordering::Equal
632 );
633 assert_eq!(
634 compare_versions(&[4, 9, 2], &[4, 9, 3]),
635 std::cmp::Ordering::Less
636 );
637 assert_eq!(
638 compare_versions(&[4, 10, 0], &[4, 9, 3]),
639 std::cmp::Ordering::Greater
640 );
641 assert_eq!(
642 compare_versions(&[6, 6], &[6, 5, 0]),
643 std::cmp::Ordering::Greater
644 );
645 }
646
647 #[test]
648 fn test_matches_constraint_less_than() {
649 let scanner = DependencyScanner::new(1);
650 assert!(scanner.matches_constraint("4.9.2", "<4.9.3"));
651 assert!(!scanner.matches_constraint("4.9.3", "<4.9.3"));
652 assert!(!scanner.matches_constraint("4.10.0", "<4.9.3"));
653 }
654
655 #[test]
656 fn test_matches_constraint_range() {
657 let scanner = DependencyScanner::new(1);
658 assert!(scanner.matches_constraint("4.5.0", ">=4.0.0 <5.0.0"));
659 assert!(scanner.matches_constraint("4.0.0", ">=4.0.0 <5.0.0"));
660 assert!(!scanner.matches_constraint("3.9.9", ">=4.0.0 <5.0.0"));
661 assert!(!scanner.matches_constraint("5.0.0", ">=4.0.0 <5.0.0"));
662 }
663
664 #[test]
665 fn test_scanner_creation() {
666 let scanner = DependencyScanner::new(0);
667 assert!(scanner.db_size() >= 30);
668 assert_eq!(scanner.depth, 0);
669 }
670
671 #[test]
672 fn test_builtin_db_not_empty() {
673 let db = builtin_db();
674 assert!(!db.is_empty());
675 assert!(db.len() >= 30);
676 }
677
678 #[test]
679 fn test_check_solc_version_vulnerable() {
680 let scanner = DependencyScanner::new(1);
681 let results = scanner.check_solc_version("0.8.13");
682 assert!(results.iter().any(|v| v.name.contains("ABI-encoding")));
683 }
684
685 #[test]
686 fn test_check_solc_version_safe() {
687 let scanner = DependencyScanner::new(1);
688 let results = scanner.check_solc_version("0.8.22");
689 assert!(!results.iter().any(|v| v.name.contains("ABI-encoding")));
690 }
691
692 #[test]
693 fn test_check_line_matches_openzeppelin() {
694 let scanner = DependencyScanner::new(1);
695 let line = "@openzeppelin/contracts=lib/openzeppelin-contracts";
696 let results = scanner.check_line(line);
697 assert!(!results.is_empty());
698 assert!(results.iter().any(|v| v.package.contains("openzeppelin")));
699 }
700
701 #[test]
702 fn test_check_line_solmate() {
703 let scanner = DependencyScanner::new(1);
704 let line = "solmate=lib/solmate";
705 let results = scanner.check_line(line);
706 assert!(!results.is_empty());
707 assert!(results.iter().any(|v| v.package.contains("solmate")));
708 }
709
710 #[test]
711 fn test_check_line_no_match() {
712 let scanner = DependencyScanner::new(1);
713 let line = "my-custom-lib=lib/my-custom-lib";
714 let results = scanner.check_line(line);
715 assert!(results.is_empty());
716 }
717
718 #[test]
719 fn test_merge_databases_with_override() {
720 let builtin = vec![KnownVulnerability {
721 package_pattern: "test-pkg".to_string(),
722 version_constraint: "<1.0.0".to_string(),
723 severity: "high".to_string(),
724 name: "Test Vuln".to_string(),
725 description: "old desc".to_string(),
726 fix: "upgrade".to_string(),
727 cve: String::new(),
728 category: String::new(),
729 }];
730 let remote = vec![KnownVulnerability {
731 package_pattern: "test-pkg".to_string(),
732 version_constraint: "<2.0.0".to_string(),
733 severity: "critical".to_string(),
734 name: "Test Vuln".to_string(),
735 description: "new desc".to_string(),
736 fix: "upgrade".to_string(),
737 cve: String::new(),
738 category: String::new(),
739 }];
740 let merged = DependencyScanner::merge_databases(&builtin, &remote);
741 assert_eq!(merged.len(), 1);
742 assert_eq!(merged[0].severity, "critical");
743 }
744
745 #[test]
746 fn test_cache_roundtrip() {
747 let scanner = DependencyScanner::new(1);
748 let entries = vec![KnownVulnerability {
749 package_pattern: "cache-test".to_string(),
750 version_constraint: "<1.0.0".to_string(),
751 severity: "low".to_string(),
752 name: "Cache Test".to_string(),
753 description: "Testing cache".to_string(),
754 fix: "nothing".to_string(),
755 cve: String::new(),
756 category: String::new(),
757 }];
758 assert!(scanner.cache_database(&entries).is_ok());
759
760 let mut scanner2 = DependencyScanner::new(1);
761 assert!(scanner2.load_cached_db());
762 assert!(scanner2
763 .known_vulnerabilities
764 .iter()
765 .any(|v| v.name == "Cache Test"));
766
767 let _ = std::fs::remove_file(&scanner.cache_path);
768 }
769
770 #[test]
771 fn test_solc_version_non_matching() {
772 let scanner = DependencyScanner::new(1);
773 let results = scanner.check_solc_version("1.0.0");
774 assert!(results.is_empty());
775 }
776}