vulnmirror 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ Copyright (C) 2026 by Nemo Xiong
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: vulnmirror
3
+ Version: 0.1.0
4
+ Summary: A local, incrementally updated SQLite mirror of CVE Program, NVD, CISA KEV and GitHub Advisory Database metadata.
5
+ Keywords: cve,nvd,kev,ghsa,osv,vulnerability,sqlite,mirror
6
+ Author: Nemo Xiong
7
+ Author-email: Nemo Xiong <nemo@anzupop.com>
8
+ License-Expression: 0BSD
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Security
22
+ Requires-Dist: certifi>=2024.2.2
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+
26
+ # vulnmirror
27
+
28
+ A local, incrementally updated SQLite mirror of public vulnerability metadata:
29
+
30
+ | Source | What it contributes | Incremental channel |
31
+ |---|---|---|
32
+ | CVE Program [`cvelistV5`](https://github.com/CVEProject/cvelistV5) | the authoritative CVE record: state, dates, CNA, description, CNA/ADP affected vendor + product, references with tags, CWE, CVSS | `cves/deltaLog.json` (about 30 days) |
33
+ | [NVD](https://nvd.nist.gov) CVE API 2.0 feeds | NVD analysis status, CPE configurations, NVD reference tags, NVD CWE and CVSS | `modified` + `recent` feeds (8 days) |
34
+ | [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | known-exploited flag, date added | full reload (small) |
35
+ | [GitHub Advisory Database](https://github.com/github/advisory-database) (OSV format) | package ecosystem and name, affected and fixed versions, CVE aliases, CWE, severity | `git diff` between synced commits |
36
+
37
+ Everything lands in one SQLite file you can query with plain SQL. Every row records which
38
+ provider supplied it (`cna`, `adp:<name>`, `nvd`), so CNA-reported and NVD-enriched facts
39
+ are never silently mixed.
40
+
41
+ ## Requirements
42
+
43
+ - Python 3.11 or newer, `curl` and `git` on `PATH`
44
+ - Disk: about 3.2 GB of raw downloads and a 2.8 GB database (measured 2026-09-24)
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ uv tool install vulnmirror # or: pipx install vulnmirror
50
+ # from a checkout:
51
+ uv tool install --editable .
52
+ ```
53
+
54
+ Or run it without installing:
55
+
56
+ ```sh
57
+ uvx vulnmirror status # from PyPI, in a throwaway environment
58
+ uv run vulnmirror status # inside a checkout
59
+ uv run --project /path/to/vulnmirror vulnmirror status # a checkout, from any directory
60
+ ```
61
+
62
+ Every form reads the same data directory (next section), so they can be mixed freely.
63
+
64
+ ## Quick start
65
+
66
+ ```sh
67
+ vulnmirror init # download everything, clone the advisory database, build, update
68
+ vulnmirror status # sync markers, their age, row counts
69
+ vulnmirror update # afterwards: incremental update, about a minute
70
+ ```
71
+
72
+ `init` retries through network outages, and every download resumes where it stopped, so
73
+ it is safe to interrupt. A first run took roughly half an hour on 2026-09-23: the NVD feeds
74
+ took about 13 minutes (NVD serves them slowly; they are fetched in parallel), the rest went
75
+ to cloning the advisory database and a build of under 3 minutes.
76
+
77
+ ## Where the data lives
78
+
79
+ The data directory ("home") is resolved in this order:
80
+
81
+ 1. `--home PATH`
82
+ 2. `$VULNMIRROR_HOME`
83
+ 3. `home = "..."` in `$XDG_CONFIG_HOME/vulnmirror/config.toml` (default `~/.config/vulnmirror/config.toml`)
84
+ 4. `$XDG_DATA_HOME/vulnmirror` (default `~/.local/share/vulnmirror`)
85
+
86
+ ```sh
87
+ vulnmirror config set-home ~/datasets/vulnmirror
88
+ vulnmirror config show
89
+ ```
90
+
91
+ Layout inside the home: `vulnmirror.sqlite`, `MANIFEST.json` (the raw snapshot), and
92
+ `raw/` (`cvelistV5/`, `nvd/`, `kev/`, `ghsa/advisory-database/`, `cases/`).
93
+
94
+ ## Keeping it current
95
+
96
+ ```sh
97
+ vulnmirror update # all sources
98
+ vulnmirror update --only nvd,ghsa # a subset
99
+ ```
100
+
101
+ Each source is applied in its own transaction. A source that fails is rolled back and keeps
102
+ its sync marker, so running `update` again retries the same window; running it twice in a
103
+ row changes nothing. Run it at least weekly:
104
+
105
+ - NVD older than 7 days: `update` reloads the NVD tables from the yearly feeds by itself.
106
+ - cvelistV5 older than the delta log (about 30 days): `update` refuses; run `vulnmirror fetch`
107
+ and `vulnmirror build`, then `update`.
108
+ - The advisory-database clone missing or re-cloned: `update` clones it again and reloads the
109
+ GHSA tables when the recorded commit is gone.
110
+
111
+ A full rebuild (`vulnmirror fetch && vulnmirror build`) writes to `vulnmirror.sqlite.building`
112
+ and renames it into place when done, so queries keep working meanwhile.
113
+
114
+ ## Individual records: `vulnmirror get`
115
+
116
+ Fetch single records by identifier or URL, one at a time or from lists:
117
+
118
+ ```sh
119
+ vulnmirror get CVE-2024-3094 # CVE record + NVD record
120
+ vulnmirror get --type nvd CVE-2024-3094 # NVD record only
121
+ vulnmirror get https://nvd.nist.gov/vuln/detail/CVE-2021-44228
122
+ vulnmirror get https://github.com/advisories/GHSA-jfh8-c2jp-5v3q
123
+ vulnmirror get -f refs.txt # one reference per line, '#' comments
124
+ cat refs.txt | vulnmirror get -f - # from stdin
125
+ vulnmirror get -f refs.txt --ingest # also upsert into the database
126
+ ```
127
+
128
+ | Reference | Record type |
129
+ |---|---|
130
+ | `CVE-YYYY-NNNN` | `cve` and `nvd` (narrow with `--type`) |
131
+ | `GHSA-xxxx-xxxx-xxxx` | `ghsa` |
132
+ | `cve.org`, `cve.mitre.org`, cvelistV5 file URLs | `cve` |
133
+ | `nvd.nist.gov` pages and NVD API URLs | `nvd` |
134
+ | GitHub advisory pages (global or repository), `osv.dev`, `api.osv.dev`, advisory-database file URLs | `ghsa` |
135
+ | any other URL that contains exactly one CVE or GHSA identifier | inferred from the identifier |
136
+
137
+ Records are stored as `raw/cases/<type>/<ID>.json`. An existing file is kept unless you pass
138
+ `--force`, so re-running a long list only fetches what is missing. Every download is logged
139
+ with its source URL in `raw/cases/index.jsonl`.
140
+
141
+ - `cve` comes from cvelistV5 on GitHub.
142
+ - `nvd` comes from the NVD CVE API 2.0. Requests are spaced 6.5 s apart; set `NVD_API_KEY` to
143
+ use the higher limit (0.7 s).
144
+ - `ghsa` is the original file from github/advisory-database, located through the OSV API. If
145
+ the original cannot be fetched, the OSV copy is stored instead and the log says so.
146
+
147
+ ## Querying
148
+
149
+ ```sh
150
+ vulnmirror sql "SELECT kind, count(*) FROM reference GROUP BY kind"
151
+ vulnmirror sql "SELECT * FROM kev LIMIT 5" --format json
152
+ vulnmirror filter --vendor examplevendor --from 2024-06-01 --to 2025-05-31 --format csv -o hits.csv
153
+ vulnmirror filter --cpe-part h --cpe-scope any --from 2024-06-01 --to 2025-05-31 --count
154
+ ```
155
+
156
+ `filter` searches the CNA/ADP `affected` table and the NVD `nvd_cpe` table together; the
157
+ `sources` column says which one matched. Output formats: `tsv` (default), `csv`, `json`, `jsonl`.
158
+
159
+ ### Tables
160
+
161
+ | Table | Grain | Notes |
162
+ |---|---|---|
163
+ | `cve` | one row per CVE ID | from cvelistV5; `state` is `PUBLISHED` or `REJECTED` |
164
+ | `nvd` | one row per CVE ID known to NVD | `status` is NVD's analysis status |
165
+ | `affected` | CVE × provider × product | vendor and product are free text as supplied |
166
+ | `reference` | CVE × provider × URL | `kind` is derived from the URL; `tags` are the provider's own vocabulary |
167
+ | `weakness`, `metric` | CVE × provider × CWE / CVSS version | |
168
+ | `nvd_cpe` | CVE × CPE match | `part` is `a` / `o` / `h`; `vulnerable` = 1 for vulnerable matches |
169
+ | `kev` | one row per KEV entry | |
170
+ | `ghsa` | one row per advisory | `reviewed` = 1 for GitHub-reviewed advisories |
171
+ | `ghsa_alias` | advisory × alias | join to `cve.id` on CVE aliases |
172
+ | `ghsa_affected` | advisory × package | first `introduced` / `fixed` / `last_affected` event; full ranges as JSON |
173
+ | `ghsa_reference`, `ghsa_cwe` | advisory × URL / CWE | |
174
+ | `snapshot` | key / value | source snapshots and sync markers |
175
+
176
+ ### Before you count
177
+
178
+ - **NVD marks device hardware as a non-vulnerable platform.** A device CVE is usually modelled as
179
+ vulnerable firmware (`o:*_firmware`, vulnerable = 1) running on hardware (`h`, vulnerable = 0).
180
+ Use `--cpe-scope any` for hardware queries.
181
+ - **NVD CPE coverage is incomplete** for recent CVEs, so CPE-based counts are lower bounds; the
182
+ CNA `affected` table covers records NVD has not analysed yet.
183
+ - **CNA vendor fields are often `n/a`**, with the vendor only in NVD's CPEs. Query both tables
184
+ (`filter` does).
185
+ - **There is no device-type field.** Selecting CVEs for one kind of device means choosing vendor
186
+ and product criteria and checking a sample by hand.
187
+ - **Dates differ by source.** `cve.date_published`, `nvd.published` and `ghsa.published` can be
188
+ hours to days apart; say which one a count uses.
189
+ - **Only GitHub-reviewed advisories** reliably carry package ecosystems and version ranges.
190
+
191
+ ## Data terms
192
+
193
+ vulnmirror downloads data; it does not redistribute it. Each source has its own terms:
194
+
195
+ - cvelistV5: "You may search, download, and use the content hosted in this repository, per the
196
+ [CVE Program Terms of Use](https://www.cve.org/Legal/TermsOfUse)" (cvelistV5 README).
197
+ - GitHub Advisory Database: [CC BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md);
198
+ attribute GitHub when you publish derived data.
199
+ - NVD and CISA KEV: see the terms published on their sites.
200
+
201
+ ## License
202
+
203
+ vulnmirror is released under the [BSD Zero Clause License](LICENSE) (SPDX `0BSD`). The data it
204
+ downloads is not covered by this license; see [Data terms](#data-terms).
205
+
206
+ ## Development
207
+
208
+ ```sh
209
+ uv sync # environment with dev dependencies
210
+ uv run pytest # unit and integration tests on synthetic data, no network
211
+ VULNMIRROR_REGRESSION=1 uv run pytest -m regression # checks a real mirror against reference counts
212
+ uv run ruff check src tests && uv run ruff format --check src tests
213
+ uv build # sdist and wheel in dist/
214
+ ```
215
+
216
+ The regression tests compare a real mirror with counts computed independently from the NVD API
217
+ on 2026-09-23 (for example 42,612 non-rejected CVEs published 2024-06-01 to 2025-05-31). NVD
218
+ re-analyses records over time, so these counts carry a tolerance.
@@ -0,0 +1,193 @@
1
+ # vulnmirror
2
+
3
+ A local, incrementally updated SQLite mirror of public vulnerability metadata:
4
+
5
+ | Source | What it contributes | Incremental channel |
6
+ |---|---|---|
7
+ | CVE Program [`cvelistV5`](https://github.com/CVEProject/cvelistV5) | the authoritative CVE record: state, dates, CNA, description, CNA/ADP affected vendor + product, references with tags, CWE, CVSS | `cves/deltaLog.json` (about 30 days) |
8
+ | [NVD](https://nvd.nist.gov) CVE API 2.0 feeds | NVD analysis status, CPE configurations, NVD reference tags, NVD CWE and CVSS | `modified` + `recent` feeds (8 days) |
9
+ | [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | known-exploited flag, date added | full reload (small) |
10
+ | [GitHub Advisory Database](https://github.com/github/advisory-database) (OSV format) | package ecosystem and name, affected and fixed versions, CVE aliases, CWE, severity | `git diff` between synced commits |
11
+
12
+ Everything lands in one SQLite file you can query with plain SQL. Every row records which
13
+ provider supplied it (`cna`, `adp:<name>`, `nvd`), so CNA-reported and NVD-enriched facts
14
+ are never silently mixed.
15
+
16
+ ## Requirements
17
+
18
+ - Python 3.11 or newer, `curl` and `git` on `PATH`
19
+ - Disk: about 3.2 GB of raw downloads and a 2.8 GB database (measured 2026-09-24)
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ uv tool install vulnmirror # or: pipx install vulnmirror
25
+ # from a checkout:
26
+ uv tool install --editable .
27
+ ```
28
+
29
+ Or run it without installing:
30
+
31
+ ```sh
32
+ uvx vulnmirror status # from PyPI, in a throwaway environment
33
+ uv run vulnmirror status # inside a checkout
34
+ uv run --project /path/to/vulnmirror vulnmirror status # a checkout, from any directory
35
+ ```
36
+
37
+ Every form reads the same data directory (next section), so they can be mixed freely.
38
+
39
+ ## Quick start
40
+
41
+ ```sh
42
+ vulnmirror init # download everything, clone the advisory database, build, update
43
+ vulnmirror status # sync markers, their age, row counts
44
+ vulnmirror update # afterwards: incremental update, about a minute
45
+ ```
46
+
47
+ `init` retries through network outages, and every download resumes where it stopped, so
48
+ it is safe to interrupt. A first run took roughly half an hour on 2026-09-23: the NVD feeds
49
+ took about 13 minutes (NVD serves them slowly; they are fetched in parallel), the rest went
50
+ to cloning the advisory database and a build of under 3 minutes.
51
+
52
+ ## Where the data lives
53
+
54
+ The data directory ("home") is resolved in this order:
55
+
56
+ 1. `--home PATH`
57
+ 2. `$VULNMIRROR_HOME`
58
+ 3. `home = "..."` in `$XDG_CONFIG_HOME/vulnmirror/config.toml` (default `~/.config/vulnmirror/config.toml`)
59
+ 4. `$XDG_DATA_HOME/vulnmirror` (default `~/.local/share/vulnmirror`)
60
+
61
+ ```sh
62
+ vulnmirror config set-home ~/datasets/vulnmirror
63
+ vulnmirror config show
64
+ ```
65
+
66
+ Layout inside the home: `vulnmirror.sqlite`, `MANIFEST.json` (the raw snapshot), and
67
+ `raw/` (`cvelistV5/`, `nvd/`, `kev/`, `ghsa/advisory-database/`, `cases/`).
68
+
69
+ ## Keeping it current
70
+
71
+ ```sh
72
+ vulnmirror update # all sources
73
+ vulnmirror update --only nvd,ghsa # a subset
74
+ ```
75
+
76
+ Each source is applied in its own transaction. A source that fails is rolled back and keeps
77
+ its sync marker, so running `update` again retries the same window; running it twice in a
78
+ row changes nothing. Run it at least weekly:
79
+
80
+ - NVD older than 7 days: `update` reloads the NVD tables from the yearly feeds by itself.
81
+ - cvelistV5 older than the delta log (about 30 days): `update` refuses; run `vulnmirror fetch`
82
+ and `vulnmirror build`, then `update`.
83
+ - The advisory-database clone missing or re-cloned: `update` clones it again and reloads the
84
+ GHSA tables when the recorded commit is gone.
85
+
86
+ A full rebuild (`vulnmirror fetch && vulnmirror build`) writes to `vulnmirror.sqlite.building`
87
+ and renames it into place when done, so queries keep working meanwhile.
88
+
89
+ ## Individual records: `vulnmirror get`
90
+
91
+ Fetch single records by identifier or URL, one at a time or from lists:
92
+
93
+ ```sh
94
+ vulnmirror get CVE-2024-3094 # CVE record + NVD record
95
+ vulnmirror get --type nvd CVE-2024-3094 # NVD record only
96
+ vulnmirror get https://nvd.nist.gov/vuln/detail/CVE-2021-44228
97
+ vulnmirror get https://github.com/advisories/GHSA-jfh8-c2jp-5v3q
98
+ vulnmirror get -f refs.txt # one reference per line, '#' comments
99
+ cat refs.txt | vulnmirror get -f - # from stdin
100
+ vulnmirror get -f refs.txt --ingest # also upsert into the database
101
+ ```
102
+
103
+ | Reference | Record type |
104
+ |---|---|
105
+ | `CVE-YYYY-NNNN` | `cve` and `nvd` (narrow with `--type`) |
106
+ | `GHSA-xxxx-xxxx-xxxx` | `ghsa` |
107
+ | `cve.org`, `cve.mitre.org`, cvelistV5 file URLs | `cve` |
108
+ | `nvd.nist.gov` pages and NVD API URLs | `nvd` |
109
+ | GitHub advisory pages (global or repository), `osv.dev`, `api.osv.dev`, advisory-database file URLs | `ghsa` |
110
+ | any other URL that contains exactly one CVE or GHSA identifier | inferred from the identifier |
111
+
112
+ Records are stored as `raw/cases/<type>/<ID>.json`. An existing file is kept unless you pass
113
+ `--force`, so re-running a long list only fetches what is missing. Every download is logged
114
+ with its source URL in `raw/cases/index.jsonl`.
115
+
116
+ - `cve` comes from cvelistV5 on GitHub.
117
+ - `nvd` comes from the NVD CVE API 2.0. Requests are spaced 6.5 s apart; set `NVD_API_KEY` to
118
+ use the higher limit (0.7 s).
119
+ - `ghsa` is the original file from github/advisory-database, located through the OSV API. If
120
+ the original cannot be fetched, the OSV copy is stored instead and the log says so.
121
+
122
+ ## Querying
123
+
124
+ ```sh
125
+ vulnmirror sql "SELECT kind, count(*) FROM reference GROUP BY kind"
126
+ vulnmirror sql "SELECT * FROM kev LIMIT 5" --format json
127
+ vulnmirror filter --vendor examplevendor --from 2024-06-01 --to 2025-05-31 --format csv -o hits.csv
128
+ vulnmirror filter --cpe-part h --cpe-scope any --from 2024-06-01 --to 2025-05-31 --count
129
+ ```
130
+
131
+ `filter` searches the CNA/ADP `affected` table and the NVD `nvd_cpe` table together; the
132
+ `sources` column says which one matched. Output formats: `tsv` (default), `csv`, `json`, `jsonl`.
133
+
134
+ ### Tables
135
+
136
+ | Table | Grain | Notes |
137
+ |---|---|---|
138
+ | `cve` | one row per CVE ID | from cvelistV5; `state` is `PUBLISHED` or `REJECTED` |
139
+ | `nvd` | one row per CVE ID known to NVD | `status` is NVD's analysis status |
140
+ | `affected` | CVE × provider × product | vendor and product are free text as supplied |
141
+ | `reference` | CVE × provider × URL | `kind` is derived from the URL; `tags` are the provider's own vocabulary |
142
+ | `weakness`, `metric` | CVE × provider × CWE / CVSS version | |
143
+ | `nvd_cpe` | CVE × CPE match | `part` is `a` / `o` / `h`; `vulnerable` = 1 for vulnerable matches |
144
+ | `kev` | one row per KEV entry | |
145
+ | `ghsa` | one row per advisory | `reviewed` = 1 for GitHub-reviewed advisories |
146
+ | `ghsa_alias` | advisory × alias | join to `cve.id` on CVE aliases |
147
+ | `ghsa_affected` | advisory × package | first `introduced` / `fixed` / `last_affected` event; full ranges as JSON |
148
+ | `ghsa_reference`, `ghsa_cwe` | advisory × URL / CWE | |
149
+ | `snapshot` | key / value | source snapshots and sync markers |
150
+
151
+ ### Before you count
152
+
153
+ - **NVD marks device hardware as a non-vulnerable platform.** A device CVE is usually modelled as
154
+ vulnerable firmware (`o:*_firmware`, vulnerable = 1) running on hardware (`h`, vulnerable = 0).
155
+ Use `--cpe-scope any` for hardware queries.
156
+ - **NVD CPE coverage is incomplete** for recent CVEs, so CPE-based counts are lower bounds; the
157
+ CNA `affected` table covers records NVD has not analysed yet.
158
+ - **CNA vendor fields are often `n/a`**, with the vendor only in NVD's CPEs. Query both tables
159
+ (`filter` does).
160
+ - **There is no device-type field.** Selecting CVEs for one kind of device means choosing vendor
161
+ and product criteria and checking a sample by hand.
162
+ - **Dates differ by source.** `cve.date_published`, `nvd.published` and `ghsa.published` can be
163
+ hours to days apart; say which one a count uses.
164
+ - **Only GitHub-reviewed advisories** reliably carry package ecosystems and version ranges.
165
+
166
+ ## Data terms
167
+
168
+ vulnmirror downloads data; it does not redistribute it. Each source has its own terms:
169
+
170
+ - cvelistV5: "You may search, download, and use the content hosted in this repository, per the
171
+ [CVE Program Terms of Use](https://www.cve.org/Legal/TermsOfUse)" (cvelistV5 README).
172
+ - GitHub Advisory Database: [CC BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md);
173
+ attribute GitHub when you publish derived data.
174
+ - NVD and CISA KEV: see the terms published on their sites.
175
+
176
+ ## License
177
+
178
+ vulnmirror is released under the [BSD Zero Clause License](LICENSE) (SPDX `0BSD`). The data it
179
+ downloads is not covered by this license; see [Data terms](#data-terms).
180
+
181
+ ## Development
182
+
183
+ ```sh
184
+ uv sync # environment with dev dependencies
185
+ uv run pytest # unit and integration tests on synthetic data, no network
186
+ VULNMIRROR_REGRESSION=1 uv run pytest -m regression # checks a real mirror against reference counts
187
+ uv run ruff check src tests && uv run ruff format --check src tests
188
+ uv build # sdist and wheel in dist/
189
+ ```
190
+
191
+ The regression tests compare a real mirror with counts computed independently from the NVD API
192
+ on 2026-09-23 (for example 42,612 non-rejected CVEs published 2024-06-01 to 2025-05-31). NVD
193
+ re-analyses records over time, so these counts carry a tolerance.
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "vulnmirror"
3
+ version = "0.1.0"
4
+ description = "A local, incrementally updated SQLite mirror of CVE Program, NVD, CISA KEV and GitHub Advisory Database metadata."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "0BSD"
8
+ license-files = ["LICENSE"]
9
+ keywords = [
10
+ "cve",
11
+ "nvd",
12
+ "kev",
13
+ "ghsa",
14
+ "osv",
15
+ "vulnerability",
16
+ "sqlite",
17
+ "mirror",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
22
+ "Intended Audience :: Science/Research",
23
+ "Intended Audience :: Information Technology",
24
+ "Operating System :: MacOS",
25
+ "Operating System :: POSIX :: Linux",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Database",
31
+ "Topic :: Security",
32
+ ]
33
+ dependencies = ["certifi>=2024.2.2"]
34
+
35
+ [[project.authors]]
36
+ name = "Nemo Xiong"
37
+ email = "nemo@anzupop.com"
38
+
39
+ [project.scripts]
40
+ vulnmirror = "vulnmirror.cli:main"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.0",
45
+ "ruff>=0.6",
46
+ ]
47
+
48
+ [build-system]
49
+ requires = ["uv_build>=0.12.10,<0.13.0"]
50
+ build-backend = "uv_build"
51
+
52
+ [tool.uv.build-backend]
53
+ source-include = ["tests/**"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ markers = ["regression: checks a real, fully built mirror against reference counts (opt in with VULNMIRROR_REGRESSION=1)"]
58
+
59
+ [tool.ruff]
60
+ line-length = 120
61
+ target-version = "py311"
62
+
63
+ [tool.ruff.lint]
64
+ select = [
65
+ "E",
66
+ "F",
67
+ "W",
68
+ "I",
69
+ "B",
70
+ "UP",
71
+ ]
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "vulnmirror"
3
+ version = "0.1.0"
4
+ description = "A local, incrementally updated SQLite mirror of CVE Program, NVD, CISA KEV and GitHub Advisory Database metadata."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "0BSD"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Nemo Xiong", email = "nemo@anzupop.com" }]
10
+ keywords = ["cve", "nvd", "kev", "ghsa", "osv", "vulnerability", "sqlite", "mirror"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Science/Research",
15
+ "Intended Audience :: Information Technology",
16
+ "Operating System :: MacOS",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Database",
23
+ "Topic :: Security",
24
+ ]
25
+ dependencies = ["certifi>=2024.2.2"]
26
+
27
+ [project.scripts]
28
+ vulnmirror = "vulnmirror.cli:main"
29
+
30
+ [dependency-groups]
31
+ dev = ["pytest>=8.0", "ruff>=0.6"]
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.12.10,<0.13.0"]
35
+ build-backend = "uv_build"
36
+
37
+ [tool.uv.build-backend]
38
+ # Ship the test suite in the sdist so downstream packagers can run it.
39
+ source-include = ["tests/**"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ markers = [
44
+ "regression: checks a real, fully built mirror against reference counts (opt in with VULNMIRROR_REGRESSION=1)",
45
+ ]
46
+
47
+ [tool.ruff]
48
+ line-length = 120
49
+ target-version = "py311"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "W", "I", "B", "UP"]
@@ -0,0 +1 @@
1
+ """vulnmirror: a local, incrementally updated SQLite mirror of CVE, NVD, KEV and GHSA metadata."""
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from vulnmirror.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,105 @@
1
+ """Full build of <home>/vulnmirror.sqlite from the raw mirrors listed in <home>/MANIFEST.json.
2
+
3
+ The database is written to vulnmirror.sqlite.building and renamed into place only when
4
+ complete, so queries against the previous database keep working during a rebuild.
5
+ """
6
+
7
+ import json
8
+ import sqlite3
9
+ import zipfile
10
+ from pathlib import Path
11
+
12
+ from vulnmirror import ghsa, records
13
+ from vulnmirror.config import Paths
14
+
15
+
16
+ def iter_cvelist_export(paths: Paths, asset: str):
17
+ """Yield CVE JSON 5 records from the daily export (a zip that wraps a zip)."""
18
+ with zipfile.ZipFile(paths.cvelist_dir / asset) as outer:
19
+ inner_names = [n for n in outer.namelist() if n.endswith(".zip")]
20
+ if inner_names:
21
+ inner_path = paths.cvelist_dir / "_inner.zip"
22
+ if not inner_path.exists() or inner_path.stat().st_size != outer.getinfo(inner_names[0]).file_size:
23
+ with outer.open(inner_names[0]) as src, open(inner_path, "wb") as dst:
24
+ while chunk := src.read(1 << 20):
25
+ dst.write(chunk)
26
+ z = zipfile.ZipFile(inner_path)
27
+ else:
28
+ z = outer
29
+ with z:
30
+ for name in z.namelist():
31
+ base = name.rsplit("/", 1)[-1]
32
+ if base.startswith("CVE-") and base.endswith(".json"):
33
+ yield json.loads(z.read(name))
34
+
35
+
36
+ def export_cutoff(asset_name: str) -> str:
37
+ """'2026-09-23_all_CVEs_at_midnight.zip.zip' -> '2026-09-23T00:00:00Z'."""
38
+ return asset_name[:10] + "T00:00:00Z"
39
+
40
+
41
+ def build(paths: Paths, log=print) -> dict:
42
+ if not paths.manifest.exists():
43
+ raise FileNotFoundError(f"no {paths.manifest}; run `vulnmirror fetch` first")
44
+ manifest = json.loads(paths.manifest.read_text())
45
+ tmp = Path(str(paths.db) + ".building")
46
+ tmp.unlink(missing_ok=True)
47
+ db = sqlite3.connect(tmp)
48
+ db.executescript(records.sql_text("schema.sql"))
49
+ db.execute("PRAGMA journal_mode=OFF")
50
+ db.execute("PRAGMA synchronous=OFF")
51
+
52
+ cl = manifest["cvelistV5"]
53
+ log(f"cvelistV5 {cl['release_tag']}")
54
+ n_cl = 0
55
+ for rec in iter_cvelist_export(paths, cl["asset"]):
56
+ if records.insert_cvelist_record(db, rec):
57
+ n_cl += 1
58
+ if n_cl % 100000 == 0:
59
+ log(f" cvelistV5: {n_cl} records")
60
+ db.commit()
61
+
62
+ # 'modified' and 'recent' first: their copy of a CVE is the newest, and the first copy wins.
63
+ feeds = [paths.nvd_dir / f"nvdcve-2.0-{n}.json.gz" for n in ("modified", "recent")]
64
+ feeds = [f for f in feeds if f.exists()] + sorted(paths.nvd_dir.glob("nvdcve-2.0-[0-9]*.json.gz"))
65
+ seen, n_nvd = set(), 0
66
+ for f in feeds:
67
+ for c in records.iter_nvd_feed(f):
68
+ if c["id"] in seen:
69
+ continue
70
+ seen.add(c["id"])
71
+ records.insert_nvd_record(db, c)
72
+ n_nvd += 1
73
+ db.commit()
74
+ log(f" nvd: {f.name} done ({n_nvd} total)")
75
+
76
+ n_kev = records.load_kev(db, paths.kev_file) if paths.kev_file.exists() else 0
77
+
78
+ n_ghsa, commit = 0, ""
79
+ if paths.ghsa_repo.exists():
80
+ n_ghsa, commit = ghsa.load_all(db, paths.ghsa_repo, log=log)
81
+ log(f" ghsa: {n_ghsa} advisories @ {commit[:12]}")
82
+
83
+ nvd_feeds = {f["feed"]: f["lastModifiedDate"] for f in manifest.get("nvd", [])}
84
+ state = dict(
85
+ cvelistV5_release=cl["release_tag"],
86
+ cvelist_synced_through=export_cutoff(cl["asset"]),
87
+ nvd_synced_at=nvd_feeds.get("modified", ""),
88
+ nvd_feeds=json.dumps(nvd_feeds),
89
+ kev_catalog=manifest.get("kev", {}).get("catalogVersion", ""),
90
+ fetched_at=manifest["fetched_at"],
91
+ n_cvelist=n_cl,
92
+ n_nvd=n_nvd,
93
+ n_kev=n_kev,
94
+ n_ghsa=n_ghsa,
95
+ )
96
+ if commit:
97
+ state["ghsa_commit"] = commit
98
+ records.set_state(db, **state)
99
+ db.executescript(records.sql_text("indexes.sql"))
100
+ db.commit()
101
+ db.close()
102
+ tmp.replace(paths.db)
103
+ summary = {"cvelist": n_cl, "nvd": n_nvd, "kev": n_kev, "ghsa": n_ghsa}
104
+ log(f"built {paths.db}: " + " ".join(f"{k}={v}" for k, v in summary.items()))
105
+ return summary