Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,3 @@ private/
bitprobe/.git/
bitprobe/data/cve_db.sqlite

# Spartan AI config (contains API keys)
.spartan/ai.env

#Other
SKILL.md
AGENTS.md

# Spartan local planning/memory state -- not for the public repo
.planning/
.memory/

# Local OpenCode config
.opencode/

# Personal AI workflow tooling -- not project convention, not for the public repo
.claude/

# Isolated feature worktrees
.worktrees/

# Local-only files
CLAUDE.md
skills-lock.json
35 changes: 34 additions & 1 deletion bitprobe/bitprobe.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from scanner.engine import ScanEngine
from scanner.config import ScanConfig, SCAN_PROFILES
from scanner.asn_db_updater import update_asn_db
from scanner.cve_db_manager import update_cve_database, get_stats
from scanner.cve_db_manager import update_cve_database, update_kev_epss, get_stats
from scanner.cve_db_bootstrap import update_with_snapshot_policy


Expand Down Expand Up @@ -221,6 +221,16 @@ def main() -> int:
action="store_true",
help="Use direct NVD synchronization without downloading a snapshot",
)
cve_parser.add_argument(
"--kev-epss-only",
action="store_true",
help="Only refresh CISA KEV / FIRST EPSS enrichment, skip the NVD sync",
)
cve_parser.add_argument(
"--skip-kev-epss",
action="store_true",
help="Skip CISA KEV / FIRST EPSS enrichment during this update",
)

cve_stats_parser = subparsers.add_parser(
"cve-stats",
Expand All @@ -247,9 +257,24 @@ def main() -> int:
years = getattr(args, "years", None)
days = getattr(args, "days", None)
snapshot_only = getattr(args, "snapshot_only", False)
kev_epss_only = getattr(args, "kev_epss_only", False)
skip_kev_epss = getattr(args, "skip_kev_epss", False)
direct = full_sync or years is not None or days is not None or getattr(args, "no_snapshot", False)
if snapshot_only and direct:
raise ValueError("--snapshot-only cannot be combined with direct-NVD options")
if kev_epss_only and skip_kev_epss:
raise ValueError("--kev-epss-only cannot be combined with --skip-kev-epss")
if kev_epss_only and (direct or snapshot_only):
raise ValueError("--kev-epss-only cannot be combined with NVD sync options")

if kev_epss_only:
counts = update_kev_epss(verbose=verbose)
print(
f"[+] KEV/EPSS enrichment updated: "
f"{counts['kev_updated']} KEV, {counts['epss_updated']} EPSS"
)
return 0

if direct:
count = update_cve_database(
days=days if days is not None else 30,
Expand All @@ -267,6 +292,12 @@ def main() -> int:
if snapshot_only:
print("[+] CVE database snapshot installed")
return 0
if not skip_kev_epss:
counts = update_kev_epss(verbose=verbose)
print(
f"[+] KEV/EPSS enrichment updated: "
f"{counts['kev_updated']} KEV, {counts['epss_updated']} EPSS"
)
print(f"[+] CVE database updated with {count} entries")
return 0
except Exception as e:
Expand All @@ -283,6 +314,8 @@ def main() -> int:
print(f"Coverage: {stats.get('coverage_mode', 'unknown')}")
print(f"NVD Cursor: {stats.get('nvd_cursor', 'Never')}")
print(f"Last Updated: {stats.get('last_updated', 'Never')}")
print(f"CISA KEV flagged: {stats.get('kev_count', 0)}")
print(f"EPSS scored: {stats.get('epss_count', 0)}")
print("\nBy Severity:")
for sev, count in stats.get('severity_counts', {}).items():
print(f" {sev.upper()}: {count}")
Expand Down
49 changes: 45 additions & 4 deletions bitprobe/plugins/cve_correlation.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,40 @@ def _append_cve_finding(
) -> None:
guidance = self._generate_contextual_guidance(tech_name, cve_id)
refs = references or []
confidence = match.get("confidence", "low")
kev = bool(match.get("kev"))
kev_date_added = match.get("kev_date_added")
epss_score = match.get("epss_score")
epss_percentile = match.get("epss_percentile")

# CISA KEV means this CVE is confirmed exploited in the wild right
# now — that outweighs a CVSS-bucket severity computed in the
# abstract, so it overrides rather than just adding to it.
if kev:
severity = "critical"

remediation = (
f"Upgrade {tech_name} to a patched version. "
"See CVE details for specific fixed versions."
)
if kev:
remediation = (
"CISA's Known Exploited Vulnerabilities catalog lists this CVE as "
"actively exploited in the wild"
+ (f" (added {kev_date_added})" if kev_date_added else "")
+ f". Treat as critical and patch {tech_name} immediately, "
"regardless of match confidence below. "
) + remediation
if confidence != "confirmed":
remediation += (
" Confidence: low — this was matched on product name without a "
"version-range check (either no version was fingerprinted, or the "
"CVE record itself carries no version bound), so verify manually "
"before treating it as a confirmed finding. Note that vendor-patched "
"or distro-packaged builds can carry a backported fix while still "
"reporting an older upstream version string."
)

findings.append(
Finding(
plugin_name=self.get_name(),
Expand All @@ -212,12 +246,14 @@ def _append_cve_finding(
"cve_id": cve_id,
"cvss_score": cvss,
"affected_versions": match.get("affected_versions", ""),
"confidence": confidence,
"kev": kev,
"kev_date_added": kev_date_added,
"epss_score": epss_score,
"epss_percentile": epss_percentile,
"references": refs[:3],
},
remediation=(
f"Upgrade {tech_name} to a patched version. "
"See CVE details for specific fixed versions."
),
remediation=remediation,
attack_scenario=guidance["attack"],
defense_strategy=guidance["defense"],
mitigation_plan=guidance["mitigation"],
Expand Down Expand Up @@ -292,6 +328,11 @@ def scan(self, url_info: Dict, request_handler) -> List[Finding]:
"matched_product": tech_name,
"detected_version": tech_version,
"affected_versions": "see NVD advisory",
"confidence": cve_row.get("confidence", "low"),
"kev": cve_row.get("kev", False),
"kev_date_added": cve_row.get("kev_date_added"),
"epss_score": cve_row.get("epss_score"),
"epss_percentile": cve_row.get("epss_percentile"),
}
self._append_cve_finding(
findings,
Expand Down
Loading
Loading