feat: add NVD CVE database lookup for banner scanning#3
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds optional CLI ChangesNVD CVE Database Lookup
Sequence DiagramsequenceDiagram
participant User
participant CLI
participant Main
participant Portscan
participant NvdDatabase
participant SQLite
User->>CLI: run CLI with/without --nvd-db
CLI->>Main: parsed nvd_db value
Main->>NvdDatabase: init(db_path)
Portscan->>NvdDatabase: try_search(banner, service)
NvdDatabase->>SQLite: query cve WHERE description LIKE '%term%'
SQLite-->>NvdDatabase: rows with cve id + data JSON
NvdDatabase-->>Portscan: findings (CVE id, severity, confidence, evidence)
Portscan->>Main: append findings
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Integrates a local NVD SQLite cache for CVE matching beyond the built-in rule set. The scanner queries extracted banner keywords against CVE descriptions when a synced NVD database is available. Also wires `cve_ids` and `confidence` into portscan findings — CVE ID is now a structured field and confidence is derived from CVSS score (built-in rules) or severity (NVD lookup). Adds `--nvd-db` CLI arg for configuring the database path. NVD findings carry the `nvd` tag so they can be filtered independently.
4504b6f to
5f589f2
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/portscan/src/cve.rs`:
- Around line 349-350: Clamp the computed confidence to the [0.0, 1.0] range
before passing it to .confidence: compute a bounded value from rule.cvss (e.g.
let confidence = (rule.cvss / 10.0).clamp(0.0, 1.0) and then call
.confidence(confidence as f64)) so out-of-range CVSS in user rules cannot
produce invalid confidence values; update the code around .cve(&rule.cve) and
.confidence((rule.cvss / 10.0) as f64) to use this clamped value.
In `@crates/portscan/src/cve/nvd.rs`:
- Around line 172-183: The search_description function issues unbounded "LIKE"
scans (search_description) which can explode on large NVD DBs; change the
function to bound matches and/or use full-text search: update the signature to
accept a max_results parameter (or switch to using the FTS5 virtual table and
MATCH) and modify the SQL to either use an FTS MATCH query or append "LIMIT ?2"
to the prepared statement, passing the max_results parameter when calling
stmt.query_map; ensure callers are updated to pass a sensible limit so repeated
keyword queries cannot trigger unbounded scans.
In `@crates/portscan/src/lib.rs`:
- Around line 680-686: The spawn_blocking join result for the NVD lookup is
currently ignoring Err(JoinError) silently; update the #[cfg(feature = "nvd")]
block that calls tokio::task::spawn_blocking(move || cve::nvd::try_search(&b,
&s)).await to explicitly handle the Err branch: match the await result (or use
if let Err(e) = ...) and emit a clear diagnostic including the JoinError and
context (e.g., which CVE/scan input b/s and that the NVD enrichment failed)
instead of dropping it, then decide whether to continue without enrichment or
propagate the error; reference the cve::nvd::try_search call, the spawn_blocking
future, and the nvd_findings/findings variables when adding the
logging/handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3c78a381-06c9-4718-956a-f99957ff9ece
📒 Files selected for processing (6)
crates/cli/src/args.rscrates/cli/src/main.rscrates/portscan/Cargo.tomlcrates/portscan/src/cve.rscrates/portscan/src/cve/nvd.rscrates/portscan/src/lib.rs
| .cve(&rule.cve) | ||
| .confidence((rule.cvss / 10.0) as f64) |
There was a problem hiding this comment.
Clamp confidence to [0.0, 1.0] before writing it.
Line 350 assumes CVSS is always bounded. Since community rules are user-provided, out-of-range CVSS can produce invalid confidence values.
Suggested fix
- .confidence((rule.cvss / 10.0) as f64)
+ .confidence(((rule.cvss / 10.0) as f64).clamp(0.0, 1.0))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .cve(&rule.cve) | |
| .confidence((rule.cvss / 10.0) as f64) | |
| .cve(&rule.cve) | |
| .confidence(((rule.cvss / 10.0) as f64).clamp(0.0, 1.0)) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/portscan/src/cve.rs` around lines 349 - 350, Clamp the computed
confidence to the [0.0, 1.0] range before passing it to .confidence: compute a
bounded value from rule.cvss (e.g. let confidence = (rule.cvss /
10.0).clamp(0.0, 1.0) and then call .confidence(confidence as f64)) so
out-of-range CVSS in user rules cannot produce invalid confidence values; update
the code around .cve(&rule.cve) and .confidence((rule.cvss / 10.0) as f64) to
use this clamped value.
| fn search_description( | ||
| conn: &rusqlite::Connection, | ||
| text: &str, | ||
| ) -> Result<Vec<String>, rusqlite::Error> { | ||
| let mut stmt = conn.prepare( | ||
| "SELECT id FROM cve WHERE description LIKE '%' || ?1 || '%'", | ||
| )?; | ||
| let ids = stmt | ||
| .query_map(rusqlite::params![text], |row| row.get(0))? | ||
| .filter_map(|r| r.ok()) | ||
| .collect(); | ||
| Ok(ids) |
There was a problem hiding this comment.
Bound NVD match cardinality to avoid unbounded scan cost.
Line 177 performs an unbounded LIKE scan, and the caller runs it for many keywords. On a large NVD DB this can explode results and stall scans.
Suggested fix
fn search_description(
conn: &rusqlite::Connection,
text: &str,
) -> Result<Vec<String>, rusqlite::Error> {
let mut stmt = conn.prepare(
- "SELECT id FROM cve WHERE description LIKE '%' || ?1 || '%'",
+ "SELECT id FROM cve WHERE description LIKE '%' || ?1 || '%' LIMIT 200",
)?;
let ids = stmt
.query_map(rusqlite::params![text], |row| row.get(0))?
.filter_map(|r| r.ok())
.collect();
Ok(ids)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn search_description( | |
| conn: &rusqlite::Connection, | |
| text: &str, | |
| ) -> Result<Vec<String>, rusqlite::Error> { | |
| let mut stmt = conn.prepare( | |
| "SELECT id FROM cve WHERE description LIKE '%' || ?1 || '%'", | |
| )?; | |
| let ids = stmt | |
| .query_map(rusqlite::params![text], |row| row.get(0))? | |
| .filter_map(|r| r.ok()) | |
| .collect(); | |
| Ok(ids) | |
| fn search_description( | |
| conn: &rusqlite::Connection, | |
| text: &str, | |
| ) -> Result<Vec<String>, rusqlite::Error> { | |
| let mut stmt = conn.prepare( | |
| "SELECT id FROM cve WHERE description LIKE '%' || ?1 || '%' LIMIT 200", | |
| )?; | |
| let ids = stmt | |
| .query_map(rusqlite::params![text], |row| row.get(0))? | |
| .filter_map(|r| r.ok()) | |
| .collect(); | |
| Ok(ids) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/portscan/src/cve/nvd.rs` around lines 172 - 183, The
search_description function issues unbounded "LIKE" scans (search_description)
which can explode on large NVD DBs; change the function to bound matches and/or
use full-text search: update the signature to accept a max_results parameter (or
switch to using the FTS5 virtual table and MATCH) and modify the SQL to either
use an FTS MATCH query or append "LIMIT ?2" to the prepared statement, passing
the max_results parameter when calling stmt.query_map; ensure callers are
updated to pass a sensible limit so repeated keyword queries cannot trigger
unbounded scans.
| if let Ok(nvd_findings) = tokio::task::spawn_blocking(move || { | ||
| cve::nvd::try_search(&b, &s) | ||
| }) | ||
| .await | ||
| { | ||
| findings.extend(nvd_findings); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP --type=rust 'spawn_blocking\s*\(' -C3
rg -nP --type=rust 'if let Ok\([^)]*\)\s*=\s*tokio::task::spawn_blocking' -C3
rg -nP --type=rust 'JoinError|tracing::(warn|error|debug).*(spawn_blocking|nvd)' -C3Repository: santhsecurity/gossan
Length of output: 2851
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== portscan/src/lib.rs context around lines 660-710 ==="
sed -n '650,710p' crates/portscan/src/lib.rs | nl -ba | sed -n '1,120p'
echo
echo "=== NVD-related references in crates/portscan/src/lib.rs ==="
rg -n --hidden --no-ignore --glob='*.rs' "nvd|try_search|cve::nvd" crates/portscan/src/lib.rs
echo
echo "=== How cve::nvd::try_search is called elsewhere in repo ==="
rg -n --hidden --no-ignore --glob='*.rs' "cve::nvd::try_search" crates | head -n 50
echo
echo "=== Locate definition of try_search in cve/nvd module ==="
rg -n --hidden --no-ignore --glob='*.rs' "mod nvd|try_search" crates | head -n 50
echo
echo "=== Search for spawn_blocking join-error handling patterns elsewhere ==="
rg -n --hidden --no-ignore --glob='*.rs' \
"match\s+tokio::task::spawn_blocking|tokio::task::spawn_blocking.*await\s*\{|if let Ok\([^)]*\)\s*=\s*tokio::task::spawn_blocking|JoinError" \
crates | head -n 200Repository: santhsecurity/gossan
Length of output: 182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== portscan/src/lib.rs context around lines 650-710 ==="
awk 'NR>=650 && NR<=710 {printf "%d:%s\n", NR, $0}' crates/portscan/src/lib.rs
echo
echo "=== NVD-related references in crates/portscan/src/lib.rs ==="
rg -n --hidden --no-ignore --glob='*.rs' "nvd|try_search|cve::nvd" crates/portscan/src/lib.rs
echo
echo "=== How cve::nvd::try_search is called elsewhere in repo (first 50 matches) ==="
rg -n --hidden --no-ignore --glob='*.rs' "cve::nvd::try_search" crates | head -n 50
echo
echo "=== Search for definition of try_search in cve/nvd module (first 50 matches) ==="
rg -n --hidden --no-ignore --glob='*.rs' "fn\s+try_search|try_search\s*\(" crates | head -n 50
echo
echo "=== Search for spawn_blocking join-error handling patterns elsewhere (first 200 matches) ==="
rg -n --hidden --no-ignore --glob='*.rs' \
"match\s+tokio::task::spawn_blocking|if let Ok\([^)]*\)\s*=\s*tokio::task::spawn_blocking|JoinError" \
crates | head -n 200Repository: santhsecurity/gossan
Length of output: 3210
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Does crates/portscan/src/lib.rs use tracing? ==="
rg -n --glob='*.rs' "tracing::(warn|error|debug|info)!" crates/portscan/src/lib.rs crates/portscan/src | head -n 50
echo
echo "=== Is tracing imported in crates/portscan/src/lib.rs? ==="
rg -n --glob='*.rs' "^\\s*(use\\s+)?tracing::" crates/portscan/src/lib.rs
echo
echo "=== Any existing JoinError handling in portscan crate? ==="
rg -n --glob='*.rs' "JoinError" crates/portscan/src | head -n 50
echo
echo "=== Any other spawn_blocking call sites using if let Ok(...) pattern? ==="
rg -n --glob='*.rs' "if let Ok\\([^)]*\\)\\s*=\\s*tokio::task::spawn_blocking" crates/portscan/src | head -n 50Repository: santhsecurity/gossan
Length of output: 4961
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== crates/portscan/src/cve/nvd.rs around try_search (lines ~120-190) ==="
awk 'NR>=110 && NR<=210 {printf "%d:%s\n", NR, $0}' crates/portscan/src/cve/nvd.rs
echo
echo "=== crates/scm/src/git_scanner.rs spawn_blocking await handling (lines ~1-120) ==="
awk 'NR>=1 && NR<=140 {printf "%d:%s\n", NR, $0}' crates/scm/src/git_scanner.rs
echo
echo "=== crates/intel/src/lib.rs spawn_blocking join handling (lines ~140-250) ==="
awk 'NR>=140 && NR<=260 {printf "%d:%s\n", NR, $0}' crates/intel/src/lib.rsRepository: santhsecurity/gossan
Length of output: 11161
Handle spawn_blocking join failures explicitly for NVD lookup.
In crates/portscan/src/lib.rs (the #[cfg(feature = "nvd")] block), the if let Ok(...) = ... .await path drops Err(JoinError) silently, so a failed/panicking NVD lookup leads to missing enrichment without any diagnostics. Log the Err branch.
Suggested patch
- if let Ok(nvd_findings) = tokio::task::spawn_blocking(move || {
- cve::nvd::try_search(&b, &s)
- })
- .await
- {
- findings.extend(nvd_findings);
- }
+ match tokio::task::spawn_blocking(move || cve::nvd::try_search(&b, &s)).await {
+ Ok(nvd_findings) => findings.extend(nvd_findings),
+ Err(e) => tracing::warn!(error = %e, "NVD lookup task failed"),
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if let Ok(nvd_findings) = tokio::task::spawn_blocking(move || { | |
| cve::nvd::try_search(&b, &s) | |
| }) | |
| .await | |
| { | |
| findings.extend(nvd_findings); | |
| } | |
| match tokio::task::spawn_blocking(move || cve::nvd::try_search(&b, &s)).await { | |
| Ok(nvd_findings) => findings.extend(nvd_findings), | |
| Err(e) => tracing::warn!(error = %e, "NVD lookup task failed"), | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/portscan/src/lib.rs` around lines 680 - 686, The spawn_blocking join
result for the NVD lookup is currently ignoring Err(JoinError) silently; update
the #[cfg(feature = "nvd")] block that calls tokio::task::spawn_blocking(move ||
cve::nvd::try_search(&b, &s)).await to explicitly handle the Err branch: match
the await result (or use if let Err(e) = ...) and emit a clear diagnostic
including the JoinError and context (e.g., which CVE/scan input b/s and that the
NVD enrichment failed) instead of dropping it, then decide whether to continue
without enrichment or propagate the error; reference the cve::nvd::try_search
call, the spawn_blocking future, and the nvd_findings/findings variables when
adding the logging/handling.
|
Pushed |
Integrates a local NVD SQLite cache for CVE matching beyond the built-in rule set. The scanner queries extracted banner keywords against CVE descriptions when a synced NVD database is available.
Also wires
cve_idsandconfidenceinto portscan findings — CVE ID is now a structured field and confidence is derived from CVSS score (built-in rules) or severity (NVD lookup).Adds
--nvd-dbCLI arg for configuring the database path. NVD findings carry thenvdtag so they can be filtered independently.Verification
Before the fix, portscan CVE findings had
"cve_ids": []and"confidence": null. After:{ "confidence": 0.53, "cve_ids": ["CVE-2018-15473"] }Usage
The NVD database must be synced separately with
nvd_cve(install viacargo install nvd_cve). Once synced to the default path (~/.cache/nvd/nvd.sqlite3), gossan automatically queries it during port scans.Summary by CodeRabbit