Skip to content

Commit e6b5846

Browse files
authored
Merge pull request #11 from rywils/feature/cve-database-distribution
Add CVE snapshot distribution
2 parents 295cbf7 + 13ff840 commit e6b5846

29 files changed

Lines changed: 2591 additions & 147 deletions
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Update CVE Database
2+
3+
on:
4+
schedule:
5+
- cron: "17 6 * * *"
6+
workflow_dispatch:
7+
inputs:
8+
full_rebuild:
9+
description: Ignore the existing snapshot and rebuild from NVD
10+
required: false
11+
type: boolean
12+
default: false
13+
14+
permissions:
15+
contents: write
16+
17+
concurrency:
18+
group: cve-db-producer
19+
cancel-in-progress: false
20+
21+
jobs:
22+
sync-and-publish:
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 360
25+
env:
26+
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
27+
GH_TOKEN: ${{ github.token }}
28+
steps:
29+
- uses: actions/checkout@v4
30+
with:
31+
persist-credentials: false
32+
33+
- name: Configure data directory
34+
run: echo "BITSENTRY_DATA_DIR=$RUNNER_TEMP/bitsentry-data" >> "$GITHUB_ENV"
35+
36+
- uses: actions/setup-python@v5
37+
with:
38+
python-version: "3.13"
39+
40+
- name: Install dependencies
41+
run: pip install -r requirements.txt
42+
43+
- name: Require NVD API key
44+
run: test -n "$NVD_API_KEY"
45+
46+
- name: Restore previous snapshot
47+
id: restore
48+
if: ${{ !inputs.full_rebuild }}
49+
run: |
50+
if gh release view cve-db-latest >/dev/null 2>&1; then
51+
mkdir -p previous
52+
gh release download cve-db-latest --dir previous --pattern manifest.json --pattern cve_db.sqlite.gz
53+
PYTHONPATH=bitprobe python -m scanner.cve_db_bootstrap --manifest previous/manifest.json --artifact previous/cve_db.sqlite.gz
54+
echo "restored=true" >> "$GITHUB_OUTPUT"
55+
else
56+
echo "restored=false" >> "$GITHUB_OUTPUT"
57+
fi
58+
59+
- name: Update canonical database
60+
env:
61+
FULL_REBUILD: ${{ inputs.full_rebuild }}
62+
SNAPSHOT_RESTORED: ${{ steps.restore.outputs.restored }}
63+
run: |
64+
if [[ "$FULL_REBUILD" == "true" || "$SNAPSHOT_RESTORED" != "true" ]]; then
65+
PYTHONPATH=bitprobe python bitprobe/bitprobe.py update-cve-db --full --no-snapshot
66+
else
67+
PYTHONPATH=bitprobe python bitprobe/bitprobe.py update-cve-db --no-snapshot
68+
fi
69+
70+
- name: Build snapshot
71+
run: python scripts/build_cve_snapshot.py
72+
73+
- name: Publish releases
74+
run: bash scripts/update_cve_snapshot_release.sh dist

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,6 @@ AGENTS.md
8181

8282
# Personal AI workflow tooling -- not project convention, not for the public repo
8383
.claude/
84+
85+
# Isolated feature worktrees
86+
.worktrees/

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ Network scanning has a fallback chain in `bitprobe/scanner/engines/network/__ini
3030

3131
## Data
3232

33-
- CVE data: `bitprobe/scanner/data/cve_db.sqlite` (primary) with `cve_db.json` as fallback/legacy (`cve_db.py`, `cve_db_manager.py`, `cve_updater.py`). Refresh via `bitprobe update-cve-db` (NVD API key recommended, see README).
33+
- CVE data: `~/.bitsentry/data/cve_db.sqlite` (override with `BITSENTRY_DATA_DIR`), with the source-tree database used only as a one-time legacy migration source. Refresh via `bitsentry update-cve-db`; the default path installs a verified release snapshot before incremental NVD catch-up.
3434
- ASN/IP intel DB: `asn_db_updater.py`, refreshed via `bitsentry update-db`.
35-
- Neither DB is checked into git (`bitprobe/data/cve_db.sqlite` is gitignored — a prior commit removed a large SQLite file from the repo for this reason).
35+
- Neither generated database is checked into git. Published CVE snapshots live in reserved `cve-db-*` GitHub Releases.
3636

3737
## Commands
3838

README.md

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ BitSentry is a CLI-first security assessment suite. The public build focuses on
1010
- web-focused vulnerability scanning
1111

1212
It is built to run cleanly in local shells, CI pipelines, and Docker.
13+
Supported hosts are Linux and macOS.
1314
> Use only on systems you own or are explicitly authorized to test.
1415
1516
## Current Product Status
@@ -54,14 +55,8 @@ Refresh local intelligence databases once so scans are useful. This is separate
5455
# 1) ASN database (fast; needed for ASN/IP intel plugins)
5556
bitsentry update-db
5657

57-
# 2) CVE database (choose one bootstrap — required for technology/CVE correlation)
58-
export NVD_API_KEY="your-nvd-api-key" # optional but strongly recommended
59-
60-
# Recommended: full local mirror (slow once; best coverage)
61-
bitsentry update-cve-db --full
62-
63-
# Alternative: smaller first-time bootstrap (~15 years of publications)
64-
# bitsentry update-cve-db --years 15
58+
# 2) CVE database: verified snapshot, then incremental NVD catch-up
59+
bitsentry update-cve-db
6560

6661
# Check what was loaded
6762
bitsentry cve-stats
@@ -70,14 +65,14 @@ bitsentry cve-stats
7065
bitsentry scan example.com
7166
```
7267

73-
**Ongoing maintenance** (after the one-time bootstrap above):
68+
**Ongoing maintenance:**
7469

7570
```bash
7671
bitsentry update-db # refresh ASN data when stale
77-
bitsentry update-cve-db # incremental CVE sync (fast)
72+
bitsentry update-cve-db # snapshot if needed, otherwise incremental sync
7873
```
7974

80-
If you skip CVE bootstrap, the first scan may still run but will only auto-fetch a **short recent-publication window**—not enough for historical product/CVE exposure. See [CVE database](#cve-database) below for details.
75+
If you skip this step, the first scan uses the same snapshot bootstrap automatically. See [CVE database](#cve-database) for direct-NVD and offline fallback behavior.
8176

8277
### Option 2: manual setup
8378

@@ -88,8 +83,7 @@ pip install -r requirements.txt
8883

8984
# Same post-install DB steps as Option 1 (use python bitsentry.py if bitsentry is not on PATH)
9085
python bitsentry.py update-db
91-
export NVD_API_KEY="your-nvd-api-key" # optional
92-
python bitsentry.py update-cve-db --full # or: --years 15
86+
python bitsentry.py update-cve-db
9387
python bitsentry.py cve-stats
9488

9589
# Full workflow (default): BitScope discovery -> BitProbe scan
@@ -159,48 +153,49 @@ python bitsentry.py update-db # alias: update-asn-db
159153

160154
### CVE database
161155

162-
BitProbe stores CVEs in a local SQLite database (`bitprobe/data/cve_db.sqlite`) and matches them **by detected product and version** during scans—not by “CVEs published in the last N days.”
156+
BitProbe stores mutable CVE data in `~/.bitsentry/data/cve_db.sqlite` and matches CVEs by detected product and version. Set `BITSENTRY_DATA_DIR` to use a different data directory.
163157

164158
| Phase | What happens |
165159
|---|---|
166-
| **Bootstrap** | Populates the local DB (one-time or after a wipe) |
160+
| **Bootstrap** | Downloads and verifies the published full-corpus snapshot |
167161
| **Incremental sync** | Fetches only NVD records modified since the last cursor (fast) |
168162
| **Scan** | Fingerprints the target, then queries the DB for that product/CPE |
169163

170-
A short publication window (for example `--days 30`) only controls **what gets downloaded into the DB**. It does not limit scan logic. For real exposure coverage, bootstrap with a full or multi-year mirror first, then rely on incremental updates.
171-
172-
**Recommended first-time setup:**
164+
The default command installs a verified snapshot when the database is missing or incomplete, then fetches changes made after the snapshot cursor:
173165

174166
```bash
175-
# Optional but strongly recommended (higher NVD rate limits)
176-
export NVD_API_KEY="your-nvd-api-key"
177-
178-
# One-time: build a complete local mirror (slow; ~350k CVEs)
179-
python bitsentry.py update-cve-db --full
180-
181-
# Alternative: compromise bootstrap (~15 years of publications)
182-
python bitsentry.py update-cve-db --years 15
183-
184-
# Ongoing refresh (incremental when a sync cursor exists)
185167
python bitsentry.py update-cve-db
186168

187-
# Inspect local store
169+
# Install the snapshot without an incremental NVD catch-up
170+
python bitsentry.py update-cve-db --snapshot-only
171+
172+
# Inspect local coverage and counts
188173
python bitsentry.py cve-stats
189174
```
190175

191-
**Other options:**
176+
Direct-NVD modes skip the snapshot. BitSentry splits long NVD date ranges into 119-day windows:
192177

193178
```bash
194-
# Quick bootstrap only (~recent publications; not sufficient alone for deep history)
179+
# Rebuild the complete corpus directly from NVD
180+
python bitsentry.py update-cve-db --full
181+
182+
# Raw unfiltered crawl (best-effort offset resumption)
183+
python bitsentry.py update-cve-db --raw-full
184+
185+
# Build partial publication-window databases
195186
python bitsentry.py update-cve-db --days 30
187+
python bitsentry.py update-cve-db --years 15
188+
189+
# Synchronize directly without downloading a snapshot
190+
python bitsentry.py update-cve-db --no-snapshot
196191

197192
# Skip automatic CVE refresh at scan startup
198193
export BITSENTRY_SKIP_CVE_UPDATE=1
199194
```
200195

201-
On scan startup, if the DB is empty, BitProbe may run a **7-day publication bootstrap** so the tool stays usable without blocking on a full NVD download. Run `update-cve-db --full` or `--years 15` before relying on CVE findings in production assessments.
196+
Set `NVD_API_KEY` for the higher NVD request limit. Interrupted windowed updates resume from the last committed page. BitSentry checksum-verifies snapshots and installs them atomically. If the snapshot is unavailable on an empty installation, it falls back to a 30-day publication database and warns that coverage is partial.
202197

203-
Direct product commands are also available via `python bitprobe/bitprobe.py ...` (same flags: `--full`, `--years`, `--days`).
198+
Direct product commands are also available via `python bitprobe/bitprobe.py ...` with the same flags.
204199

205200
### Other maintenance
206201

bitprobe/bitprobe.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from scanner.config import ScanConfig, SCAN_PROFILES
1717
from scanner.asn_db_updater import update_asn_db
1818
from scanner.cve_db_manager import update_cve_database, get_stats
19+
from scanner.cve_db_bootstrap import update_with_snapshot_policy
1920

2021

2122
def cmd_scan(args) -> int:
@@ -191,8 +192,8 @@ def main() -> int:
191192
cve_parser.add_argument(
192193
"--days",
193194
type=int,
194-
default=30,
195-
help="Publication window for bootstrap when DB is empty (default: 30)",
195+
default=None,
196+
help="Build a publication-window mirror directly from NVD",
196197
)
197198
cve_parser.add_argument(
198199
"--years",
@@ -203,7 +204,22 @@ def main() -> int:
203204
cve_parser.add_argument(
204205
"--full",
205206
action="store_true",
206-
help="Build full local NVD mirror (~350k CVEs; first-time setup)",
207+
help="Rebuild the full local mirror directly from NVD",
208+
)
209+
cve_parser.add_argument(
210+
"--raw-full",
211+
action="store_true",
212+
help="Best-effort unfiltered NVD crawl; offset resumption is not deterministic",
213+
)
214+
cve_parser.add_argument(
215+
"--snapshot-only",
216+
action="store_true",
217+
help="Install the published snapshot without contacting NVD afterward",
218+
)
219+
cve_parser.add_argument(
220+
"--no-snapshot",
221+
action="store_true",
222+
help="Use direct NVD synchronization without downloading a snapshot",
207223
)
208224

209225
cve_stats_parser = subparsers.add_parser(
@@ -226,14 +242,31 @@ def main() -> int:
226242

227243
elif args.command == "update-cve-db":
228244
try:
229-
full_sync = getattr(args, "full", False)
230-
count = update_cve_database(
231-
days=args.days,
232-
years=getattr(args, "years", None),
233-
full_sync=full_sync,
234-
force=full_sync,
235-
verbose=verbose,
236-
)
245+
raw_full = getattr(args, "raw_full", False)
246+
full_sync = getattr(args, "full", False) or raw_full
247+
years = getattr(args, "years", None)
248+
days = getattr(args, "days", None)
249+
snapshot_only = getattr(args, "snapshot_only", False)
250+
direct = full_sync or years is not None or days is not None or getattr(args, "no_snapshot", False)
251+
if snapshot_only and direct:
252+
raise ValueError("--snapshot-only cannot be combined with direct-NVD options")
253+
if direct:
254+
count = update_cve_database(
255+
days=days if days is not None else 30,
256+
years=years,
257+
full_sync=full_sync,
258+
raw_full_sync=raw_full,
259+
force=full_sync,
260+
verbose=verbose,
261+
)
262+
else:
263+
count = update_with_snapshot_policy(
264+
snapshot_only=snapshot_only,
265+
verbose=verbose,
266+
)
267+
if snapshot_only:
268+
print("[+] CVE database snapshot installed")
269+
return 0
237270
print(f"[+] CVE database updated with {count} entries")
238271
return 0
239272
except Exception as e:
@@ -247,6 +280,8 @@ def main() -> int:
247280
print("=" * 40)
248281
print(f"Total CVEs: {stats.get('total_cves', 0)}")
249282
print(f"Total Products: {stats.get('total_products', 0)}")
283+
print(f"Coverage: {stats.get('coverage_mode', 'unknown')}")
284+
print(f"NVD Cursor: {stats.get('nvd_cursor', 'Never')}")
250285
print(f"Last Updated: {stats.get('last_updated', 'Never')}")
251286
print("\nBy Severity:")
252287
for sev, count in stats.get('severity_counts', {}).items():

bitprobe/scanner/cve_db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import os
33
from pathlib import Path
44
from typing import List, Dict, Any
5+
from scanner.paths import CVE_DB_PATH as CVE_SQLITE_PATH
56

67
_DATA_DIR = Path(__file__).resolve().parents[1] / "data"
78
CVE_DB_PATH = str(_DATA_DIR / "cve_db.json")
8-
CVE_SQLITE_PATH = str(_DATA_DIR / "cve_db.sqlite")
99

1010

1111
def sqlite_cve_db_available() -> bool:

0 commit comments

Comments
 (0)