crowbar-cli 0.1.0__py3-none-any.whl
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.
- crowbar/__init__.py +3 -0
- crowbar/catalog.py +174 -0
- crowbar/cli.py +1417 -0
- crowbar/datasets.py +584 -0
- crowbar/db.py +47 -0
- crowbar/indexer.py +985 -0
- crowbar/intel.py +562 -0
- crowbar/loaders.py +382 -0
- crowbar/mcp.py +387 -0
- crowbar/paths.py +54 -0
- crowbar/query.py +551 -0
- crowbar_cli-0.1.0.dist-info/METADATA +287 -0
- crowbar_cli-0.1.0.dist-info/RECORD +16 -0
- crowbar_cli-0.1.0.dist-info/WHEEL +4 -0
- crowbar_cli-0.1.0.dist-info/entry_points.txt +4 -0
- crowbar_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
crowbar/intel.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Schema-v3 intel adapters: CVE List v5, PoC-in-GitHub, provenance.
|
|
2
|
+
|
|
3
|
+
Everything here treats the downloaded archives as inert data. No repository,
|
|
4
|
+
PoC, notebook, Makefile, or installer is executed. Loaders are deterministic
|
|
5
|
+
and idempotent so an interrupted build/update can simply be run again.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import pathlib
|
|
12
|
+
import re
|
|
13
|
+
import sqlite3
|
|
14
|
+
import tarfile
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from . import datasets, paths
|
|
18
|
+
|
|
19
|
+
BATCH = 1500
|
|
20
|
+
CVE_RE = re.compile(r"^CVE-\d{4}-\d{4,7}$", re.I)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _now() -> str:
|
|
24
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _norm(value: str | None, limit: int = 6000) -> str:
|
|
28
|
+
if not value:
|
|
29
|
+
return ""
|
|
30
|
+
return re.sub(r"\s+", " ", str(value)).strip()[:limit]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sha256(path: pathlib.Path) -> str:
|
|
34
|
+
h = hashlib.sha256()
|
|
35
|
+
with path.open("rb") as fh:
|
|
36
|
+
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
|
37
|
+
h.update(chunk)
|
|
38
|
+
return h.hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _manifest() -> dict:
|
|
42
|
+
p = paths.DATA / "intel" / "manifest.json"
|
|
43
|
+
if not p.exists():
|
|
44
|
+
return {}
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(p.read_text()).get("sources", {})
|
|
47
|
+
except (OSError, json.JSONDecodeError):
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def source_meta(name: str, default_license: str = "", path: pathlib.Path | None = None) -> dict:
|
|
52
|
+
# fetch_intel.py writes data/intel/manifest.json; `crowbar sync` writes index/manifest.json
|
|
53
|
+
entry = _manifest().get(name) or datasets.load_manifest().get("datasets", {}).get(name, {})
|
|
54
|
+
rel = entry.get("path", "")
|
|
55
|
+
disk = path or ((paths.ROOT / rel) if rel else None)
|
|
56
|
+
url = entry.get("url") or next((d.url for d in datasets.CORE_INTEL if d.name == name), "")
|
|
57
|
+
return {
|
|
58
|
+
"name": name,
|
|
59
|
+
"url": url,
|
|
60
|
+
"path": rel,
|
|
61
|
+
"license": entry.get("license") or default_license,
|
|
62
|
+
"sha256": entry.get("sha256") or (_sha256(disk) if disk and disk.exists() else ""),
|
|
63
|
+
"fetched_at": entry.get("fetched_at", ""),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def record_source(con: sqlite3.Connection, source: str, *, path: pathlib.Path | None = None,
|
|
68
|
+
url: str = "", license: str = "", sha256: str = "", fetched_at: str = "",
|
|
69
|
+
count: int = 0, status: str = "loaded", note: str = "") -> None:
|
|
70
|
+
rel = ""
|
|
71
|
+
if path:
|
|
72
|
+
try:
|
|
73
|
+
rel = str(path.relative_to(paths.ROOT))
|
|
74
|
+
except ValueError:
|
|
75
|
+
rel = str(path)
|
|
76
|
+
con.execute(
|
|
77
|
+
"INSERT OR REPLACE INTO source_state"
|
|
78
|
+
"(source,url,path,license,sha256,fetched_at,loaded_at,record_count,status,note) "
|
|
79
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?)",
|
|
80
|
+
(source, url, rel, license, sha256, fetched_at, _now(), count, status, note[:1000]),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------- schema
|
|
85
|
+
V3_SCHEMA = """
|
|
86
|
+
CREATE TABLE IF NOT EXISTS cve_cna(
|
|
87
|
+
cve_id TEXT PRIMARY KEY,
|
|
88
|
+
assigner TEXT, state TEXT, published TEXT, updated TEXT,
|
|
89
|
+
title TEXT, description TEXT,
|
|
90
|
+
cvss REAL, cvss_version TEXT, cvss_vector TEXT, severity TEXT,
|
|
91
|
+
record_path TEXT, source TEXT, source_url TEXT, source_sha256 TEXT, fetched_at TEXT
|
|
92
|
+
);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS cve_cna_ref(
|
|
94
|
+
cve_id TEXT, url TEXT, tags TEXT,
|
|
95
|
+
PRIMARY KEY(cve_id, url)
|
|
96
|
+
);
|
|
97
|
+
CREATE TABLE IF NOT EXISTS cve_cna_product(
|
|
98
|
+
cve_id TEXT, vendor TEXT, product TEXT,
|
|
99
|
+
PRIMARY KEY(cve_id, vendor, product)
|
|
100
|
+
);
|
|
101
|
+
CREATE TABLE IF NOT EXISTS cve_cna_cwe(
|
|
102
|
+
cve_id TEXT, cwe TEXT,
|
|
103
|
+
PRIMARY KEY(cve_id, cwe)
|
|
104
|
+
);
|
|
105
|
+
CREATE TABLE IF NOT EXISTS cve_meta(
|
|
106
|
+
cve_id TEXT PRIMARY KEY,
|
|
107
|
+
published_year INTEGER, cve_year INTEGER,
|
|
108
|
+
has_poc INTEGER DEFAULT 0, poc_count INTEGER DEFAULT 0
|
|
109
|
+
);
|
|
110
|
+
CREATE TABLE IF NOT EXISTS poc(
|
|
111
|
+
source TEXT, cve_id TEXT, repo TEXT, url TEXT, repo_id INTEGER,
|
|
112
|
+
stars INTEGER, language TEXT, created_at TEXT, pushed_at TEXT,
|
|
113
|
+
description TEXT, fetched_at TEXT,
|
|
114
|
+
PRIMARY KEY(source, cve_id, url)
|
|
115
|
+
);
|
|
116
|
+
CREATE TABLE IF NOT EXISTS poc_orphan(cve_id TEXT PRIMARY KEY, count INTEGER);
|
|
117
|
+
CREATE TABLE IF NOT EXISTS cve_alias(
|
|
118
|
+
cve_id TEXT, alias TEXT, source TEXT,
|
|
119
|
+
PRIMARY KEY(cve_id, alias, source)
|
|
120
|
+
);
|
|
121
|
+
CREATE TABLE IF NOT EXISTS source_state(
|
|
122
|
+
source TEXT PRIMARY KEY, url TEXT, path TEXT, license TEXT, sha256 TEXT,
|
|
123
|
+
fetched_at TEXT, loaded_at TEXT, record_count INTEGER, status TEXT, note TEXT
|
|
124
|
+
);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS cve_cna_assigner ON cve_cna(assigner);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS cve_meta_pubyear ON cve_meta(published_year);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS cve_meta_cveyear ON cve_meta(cve_year);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS cve_meta_poc ON cve_meta(has_poc);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS poc_cve ON poc(cve_id);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS poc_stars ON poc(stars);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS poc_orphan_id ON poc_orphan(cve_id);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS cve_alias_alias ON cve_alias(alias);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS source_state_status ON source_state(status);
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def migrate(con: sqlite3.Connection, progress=None) -> None:
|
|
138
|
+
"""Upgrade a v2 index in place. Safe to call repeatedly."""
|
|
139
|
+
con.executescript(V3_SCHEMA)
|
|
140
|
+
# Exact duplicate CPE rows were an audit finding. Deduplicate before the
|
|
141
|
+
# unique index is created so the migration cannot fail on existing data.
|
|
142
|
+
dedupe = (
|
|
143
|
+
("cve_cwe", "cve_id,cwe"),
|
|
144
|
+
("cve_product", "cve_id,vendor,product"),
|
|
145
|
+
("cpe_match", "cve_id,part,vendor,product,version,"
|
|
146
|
+
"v_start_incl,v_start_excl,v_end_incl,v_end_excl"),
|
|
147
|
+
("cve_ref", "cve_id,url"),
|
|
148
|
+
)
|
|
149
|
+
for table, cols in dedupe:
|
|
150
|
+
con.execute(
|
|
151
|
+
f"DELETE FROM {table} WHERE rowid NOT IN "
|
|
152
|
+
f"(SELECT min(rowid) FROM {table} GROUP BY {cols})"
|
|
153
|
+
)
|
|
154
|
+
con.executescript("""
|
|
155
|
+
CREATE UNIQUE INDEX IF NOT EXISTS cve_cwe_unique ON cve_cwe(cve_id,cwe);
|
|
156
|
+
CREATE UNIQUE INDEX IF NOT EXISTS cve_product_unique ON cve_product(cve_id,vendor,product);
|
|
157
|
+
CREATE UNIQUE INDEX IF NOT EXISTS cpe_match_unique ON cpe_match(
|
|
158
|
+
cve_id,part,vendor,product,version,v_start_incl,v_start_excl,v_end_incl,v_end_excl);
|
|
159
|
+
CREATE UNIQUE INDEX IF NOT EXISTS cve_ref_unique ON cve_ref(cve_id,url);
|
|
160
|
+
""")
|
|
161
|
+
rebuild_cve_meta(con)
|
|
162
|
+
stub_missing_cwes(con)
|
|
163
|
+
con.execute("INSERT OR REPLACE INTO meta VALUES ('schema',?)", (str(paths.SCHEMA_VERSION),))
|
|
164
|
+
con.commit()
|
|
165
|
+
if progress:
|
|
166
|
+
progress("schema v3 migrated")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------- CVE List v5
|
|
170
|
+
def _cna_metrics(cna: dict) -> tuple[float | None, str, str, str]:
|
|
171
|
+
"""CNA CVSS: the schema allows one metric object per key inside a list."""
|
|
172
|
+
buckets: list[dict] = []
|
|
173
|
+
for entry in cna.get("metrics") or []:
|
|
174
|
+
if isinstance(entry, dict):
|
|
175
|
+
buckets.append(entry)
|
|
176
|
+
for key in ("cvssV4_0", "cvssV3_1", "cvssV3_0", "cvssV2_0"):
|
|
177
|
+
for bucket in buckets:
|
|
178
|
+
data = bucket.get(key)
|
|
179
|
+
if not isinstance(data, dict):
|
|
180
|
+
continue
|
|
181
|
+
score = data.get("baseScore")
|
|
182
|
+
if score is None:
|
|
183
|
+
continue
|
|
184
|
+
return (float(score), str(data.get("version") or ""),
|
|
185
|
+
str(data.get("vectorString") or ""),
|
|
186
|
+
str(data.get("baseSeverity") or "").upper())
|
|
187
|
+
data = cna.get(key)
|
|
188
|
+
if isinstance(data, dict) and data.get("baseScore") is not None:
|
|
189
|
+
return (float(data["baseScore"]), str(data.get("version") or ""),
|
|
190
|
+
str(data.get("vectorString") or ""),
|
|
191
|
+
str(data.get("baseSeverity") or "").upper())
|
|
192
|
+
return (None, "", "", "")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def iter_cveorg(doc) -> list[dict]:
|
|
196
|
+
"""One CVE List file can hold a single record or an array of records."""
|
|
197
|
+
out: list[dict] = []
|
|
198
|
+
items = doc if isinstance(doc, list) else [doc]
|
|
199
|
+
for item in items:
|
|
200
|
+
if not isinstance(item, dict):
|
|
201
|
+
continue
|
|
202
|
+
parsed = parse_cveorg(item)
|
|
203
|
+
if parsed:
|
|
204
|
+
out.append(parsed)
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def parse_cveorg(doc: dict | list) -> dict | None:
|
|
209
|
+
if isinstance(doc, list):
|
|
210
|
+
# A handful of CVE List files hold a JSON array of records.
|
|
211
|
+
for item in doc:
|
|
212
|
+
parsed = parse_cveorg(item) if isinstance(item, dict) else None
|
|
213
|
+
if parsed:
|
|
214
|
+
return parsed
|
|
215
|
+
return None
|
|
216
|
+
if not isinstance(doc, dict):
|
|
217
|
+
return None
|
|
218
|
+
meta = doc.get("cveMetadata") or {}
|
|
219
|
+
cid = (meta.get("cveId") or "").upper()
|
|
220
|
+
if not CVE_RE.match(cid):
|
|
221
|
+
return None
|
|
222
|
+
cna = (doc.get("containers") or {}).get("cna") or {}
|
|
223
|
+
desc = ""
|
|
224
|
+
for d in cna.get("descriptions") or []:
|
|
225
|
+
if isinstance(d, dict) and str(d.get("lang", "")).lower().startswith("en"):
|
|
226
|
+
desc = d.get("value") or ""
|
|
227
|
+
break
|
|
228
|
+
title = _norm(cna.get("title"), 1000)
|
|
229
|
+
desc = _norm(desc) or title or "No CNA description provided."
|
|
230
|
+
score, version, vector, severity = _cna_metrics(cna)
|
|
231
|
+
|
|
232
|
+
cwes: list[str] = []
|
|
233
|
+
for pt in cna.get("problemTypes") or []:
|
|
234
|
+
for d in (pt or {}).get("descriptions") or []:
|
|
235
|
+
w = str((d or {}).get("cweId") or "").upper()
|
|
236
|
+
if w.startswith("CWE-") and w not in cwes:
|
|
237
|
+
cwes.append(w)
|
|
238
|
+
|
|
239
|
+
products: list[tuple[str, str]] = []
|
|
240
|
+
for a in cna.get("affected") or []:
|
|
241
|
+
if not isinstance(a, dict):
|
|
242
|
+
continue
|
|
243
|
+
vendor = _norm(a.get("vendor"), 200).lower()
|
|
244
|
+
product = _norm(a.get("product"), 200).lower()
|
|
245
|
+
if not vendor or not product or vendor in ("n/a", "unknown"):
|
|
246
|
+
continue
|
|
247
|
+
pair = (vendor, product)
|
|
248
|
+
if pair not in products and len(products) < 120:
|
|
249
|
+
products.append(pair)
|
|
250
|
+
|
|
251
|
+
refs: list[tuple[str, str]] = []
|
|
252
|
+
seen: set[str] = set()
|
|
253
|
+
for r in cna.get("references") or []:
|
|
254
|
+
if not isinstance(r, dict):
|
|
255
|
+
continue
|
|
256
|
+
url = str(r.get("url") or "").strip()
|
|
257
|
+
if not url or url in seen:
|
|
258
|
+
continue
|
|
259
|
+
seen.add(url)
|
|
260
|
+
refs.append((url, ",".join(str(t) for t in (r.get("tags") or []))[:300]))
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"id": cid,
|
|
264
|
+
"assigner": _norm(meta.get("assignerShortName")
|
|
265
|
+
or (cna.get("providerMetadata") or {}).get("shortName"), 200),
|
|
266
|
+
"state": str(meta.get("state") or "").upper(),
|
|
267
|
+
"published": meta.get("datePublished") or "",
|
|
268
|
+
"updated": meta.get("dateUpdated") or "",
|
|
269
|
+
"title": title,
|
|
270
|
+
"description": desc,
|
|
271
|
+
"cvss": score, "cvss_version": version, "cvss_vector": vector, "severity": severity,
|
|
272
|
+
"cwes": cwes[:80], "products": products, "refs": refs[:60],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def load_cveorg(path: pathlib.Path, con: sqlite3.Connection, *, epss: dict | None = None,
|
|
277
|
+
kev_ids: set[str] | None = None, progress=None) -> int:
|
|
278
|
+
if not path.exists():
|
|
279
|
+
return 0
|
|
280
|
+
src = source_meta("cveorg-cvelist", "MIT", path)
|
|
281
|
+
fetched = src["fetched_at"] or _now()
|
|
282
|
+
epss = epss or {}
|
|
283
|
+
kev_ids = kev_ids or set()
|
|
284
|
+
existing = {r[0] for r in con.execute("SELECT id FROM cve")}
|
|
285
|
+
|
|
286
|
+
con.execute("DELETE FROM cve_cna")
|
|
287
|
+
con.execute("DELETE FROM cve_cna_ref")
|
|
288
|
+
con.execute("DELETE FROM cve_cna_product")
|
|
289
|
+
cna_rows: list[tuple] = []
|
|
290
|
+
cve_rows: list[tuple] = []
|
|
291
|
+
cwe_rows: list[tuple] = []
|
|
292
|
+
prod_rows: list[tuple] = []
|
|
293
|
+
ref_rows: list[tuple] = []
|
|
294
|
+
cna_ref_rows: list[tuple] = []
|
|
295
|
+
cna_prod_rows: list[tuple] = []
|
|
296
|
+
fts_rows: list[tuple] = []
|
|
297
|
+
meta_rows: list[tuple] = []
|
|
298
|
+
inserted = 0
|
|
299
|
+
records = 0
|
|
300
|
+
|
|
301
|
+
def flush() -> None:
|
|
302
|
+
nonlocal cna_rows, cve_rows, cwe_rows, prod_rows, ref_rows, fts_rows, meta_rows
|
|
303
|
+
nonlocal cna_ref_rows, cna_prod_rows
|
|
304
|
+
if cna_rows:
|
|
305
|
+
con.executemany(
|
|
306
|
+
"INSERT OR REPLACE INTO cve_cna(cve_id,assigner,state,published,updated,"
|
|
307
|
+
"title,description,cvss,cvss_version,cvss_vector,severity,record_path,source,"
|
|
308
|
+
"source_url,source_sha256,fetched_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
309
|
+
cna_rows)
|
|
310
|
+
if cve_rows:
|
|
311
|
+
con.executemany(
|
|
312
|
+
"INSERT OR IGNORE INTO cve(id,published,modified,status,description,cvss,"
|
|
313
|
+
"cvss_version,cvss_vector,severity,kev,epss,epss_pct,year) "
|
|
314
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", cve_rows)
|
|
315
|
+
if cwe_rows:
|
|
316
|
+
con.executemany("INSERT OR IGNORE INTO cve_cwe VALUES (?,?)", cwe_rows)
|
|
317
|
+
if prod_rows:
|
|
318
|
+
con.executemany("INSERT OR IGNORE INTO cve_product VALUES (?,?,?)", prod_rows)
|
|
319
|
+
if ref_rows:
|
|
320
|
+
con.executemany("INSERT OR IGNORE INTO cve_ref VALUES (?,?,?)", ref_rows)
|
|
321
|
+
if cna_ref_rows:
|
|
322
|
+
con.executemany("INSERT OR IGNORE INTO cve_cna_ref VALUES (?,?,?)", cna_ref_rows)
|
|
323
|
+
if cna_prod_rows:
|
|
324
|
+
con.executemany("INSERT OR IGNORE INTO cve_cna_product VALUES (?,?,?)",
|
|
325
|
+
cna_prod_rows)
|
|
326
|
+
if fts_rows:
|
|
327
|
+
con.executemany("INSERT INTO cve_fts(id,description,products) VALUES (?,?,?)",
|
|
328
|
+
fts_rows)
|
|
329
|
+
if meta_rows:
|
|
330
|
+
con.executemany(
|
|
331
|
+
"INSERT INTO cve_meta(cve_id,published_year,cve_year) VALUES (?,?,?) "
|
|
332
|
+
"ON CONFLICT(cve_id) DO UPDATE SET published_year=excluded.published_year,"
|
|
333
|
+
" cve_year=excluded.cve_year", meta_rows)
|
|
334
|
+
(cna_rows, cve_rows, cwe_rows, prod_rows, ref_rows, fts_rows, meta_rows,
|
|
335
|
+
cna_ref_rows, cna_prod_rows) = ([], [], [], [], [], [], [], [], [])
|
|
336
|
+
|
|
337
|
+
with tarfile.open(path, "r:gz") as tf:
|
|
338
|
+
for member in tf:
|
|
339
|
+
if not member.isfile() or not member.name.endswith(".json"):
|
|
340
|
+
continue
|
|
341
|
+
if "/cves/" not in member.name:
|
|
342
|
+
continue
|
|
343
|
+
fh = tf.extractfile(member)
|
|
344
|
+
if fh is None:
|
|
345
|
+
continue
|
|
346
|
+
try:
|
|
347
|
+
doc = json.load(fh)
|
|
348
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
349
|
+
continue
|
|
350
|
+
parsed = iter_cveorg(doc)
|
|
351
|
+
if not parsed:
|
|
352
|
+
continue
|
|
353
|
+
for p in parsed:
|
|
354
|
+
records += 1
|
|
355
|
+
cid = p["id"]
|
|
356
|
+
year = int(cid.split("-")[1])
|
|
357
|
+
pub_year = int(p["published"][:4]) if p["published"][:4].isdigit() else None
|
|
358
|
+
e, pct = epss.get(cid, (None, None))
|
|
359
|
+
cna_rows.append((cid, p["assigner"], p["state"], p["published"], p["updated"],
|
|
360
|
+
p["title"], p["description"], p["cvss"], p["cvss_version"],
|
|
361
|
+
p["cvss_vector"], p["severity"], member.name, "cveorg",
|
|
362
|
+
src["url"], src["sha256"], fetched))
|
|
363
|
+
meta_rows.append((cid, pub_year, year))
|
|
364
|
+
for w in p["cwes"]:
|
|
365
|
+
cwe_rows.append((cid, w))
|
|
366
|
+
for vendor, product in p["products"]:
|
|
367
|
+
cna_prod_rows.append((cid, vendor, product))
|
|
368
|
+
for url, tags in p["refs"]:
|
|
369
|
+
cna_ref_rows.append((cid, url, tags))
|
|
370
|
+
if cid not in existing and p["state"] == "PUBLISHED":
|
|
371
|
+
existing.add(cid)
|
|
372
|
+
inserted += 1
|
|
373
|
+
cve_rows.append((cid, p["published"], p["updated"] or p["published"],
|
|
374
|
+
"Published", p["description"], p["cvss"], p["cvss_version"],
|
|
375
|
+
p["cvss_vector"], p["severity"], 1 if cid in kev_ids else 0,
|
|
376
|
+
e, pct, pub_year or year))
|
|
377
|
+
# A CVE that NVD never carried has no NVD refs/products, so its
|
|
378
|
+
# CNA data is the only copy -> promote it into the shared tables.
|
|
379
|
+
for vendor, product in p["products"]:
|
|
380
|
+
prod_rows.append((cid, vendor, product))
|
|
381
|
+
for url, tags in p["refs"]:
|
|
382
|
+
ref_rows.append((cid, url, tags))
|
|
383
|
+
fts_rows.append((cid, p["description"][:4000],
|
|
384
|
+
" ".join(f"{v} {pr}" for v, pr in p["products"])[:4000]))
|
|
385
|
+
if len(cna_rows) >= BATCH:
|
|
386
|
+
flush()
|
|
387
|
+
if progress and records % 20000 == 0:
|
|
388
|
+
progress(f"CVE List {records} records / {inserted} new")
|
|
389
|
+
|
|
390
|
+
flush()
|
|
391
|
+
rebuild_cve_meta(con)
|
|
392
|
+
record_source(con, "cveorg-cvelist", path=path, url=src["url"],
|
|
393
|
+
license=src["license"], sha256=src["sha256"], fetched_at=fetched,
|
|
394
|
+
count=records,
|
|
395
|
+
note=f"{inserted} CVE rows inserted; {len(ref_rows)} CNA refs in last batch")
|
|
396
|
+
con.commit()
|
|
397
|
+
if progress:
|
|
398
|
+
progress(f"CVE List {records} records, {inserted} new CVEs")
|
|
399
|
+
return inserted
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------------------------------------------------------------- PoC-in-GitHub
|
|
403
|
+
def load_poc_github(path: pathlib.Path, con: sqlite3.Connection, progress=None) -> int:
|
|
404
|
+
if not path.exists():
|
|
405
|
+
return 0
|
|
406
|
+
src = source_meta("poc-in-github", "MIT", path)
|
|
407
|
+
fetched = src["fetched_at"] or _now()
|
|
408
|
+
con.execute("DELETE FROM poc WHERE source='poc-in-github'")
|
|
409
|
+
rows: list[tuple] = []
|
|
410
|
+
files = 0
|
|
411
|
+
added = 0
|
|
412
|
+
|
|
413
|
+
def flush() -> None:
|
|
414
|
+
nonlocal rows
|
|
415
|
+
if rows:
|
|
416
|
+
con.executemany(
|
|
417
|
+
"INSERT OR IGNORE INTO poc(source,cve_id,repo,url,repo_id,stars,language,"
|
|
418
|
+
"created_at,pushed_at,description,fetched_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
|
419
|
+
rows)
|
|
420
|
+
rows = []
|
|
421
|
+
|
|
422
|
+
with tarfile.open(path, "r:gz") as tf:
|
|
423
|
+
for member in tf:
|
|
424
|
+
if not member.isfile() or not member.name.endswith(".json"):
|
|
425
|
+
continue
|
|
426
|
+
name = member.name.rsplit("/", 1)[-1][:-5]
|
|
427
|
+
if not CVE_RE.match(name):
|
|
428
|
+
continue
|
|
429
|
+
fh = tf.extractfile(member)
|
|
430
|
+
if fh is None:
|
|
431
|
+
continue
|
|
432
|
+
try:
|
|
433
|
+
repos = json.load(fh)
|
|
434
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
435
|
+
continue
|
|
436
|
+
if not isinstance(repos, list):
|
|
437
|
+
continue
|
|
438
|
+
files += 1
|
|
439
|
+
for repo in repos:
|
|
440
|
+
if not isinstance(repo, dict):
|
|
441
|
+
continue
|
|
442
|
+
url = str(repo.get("html_url") or "").strip()
|
|
443
|
+
if not url:
|
|
444
|
+
continue
|
|
445
|
+
rows.append((
|
|
446
|
+
"poc-in-github", name.upper(),
|
|
447
|
+
_norm(repo.get("full_name"), 300), url,
|
|
448
|
+
repo.get("id") if isinstance(repo.get("id"), int) else None,
|
|
449
|
+
repo.get("stargazers_count") if isinstance(repo.get("stargazers_count"), int) else 0,
|
|
450
|
+
_norm(repo.get("language"), 100),
|
|
451
|
+
repo.get("created_at") or "", repo.get("pushed_at") or "",
|
|
452
|
+
_norm(repo.get("description"), 500), fetched,
|
|
453
|
+
))
|
|
454
|
+
added += 1
|
|
455
|
+
if len(rows) >= BATCH * 2:
|
|
456
|
+
flush()
|
|
457
|
+
if files % 2000 == 0 and progress:
|
|
458
|
+
progress(f"PoC {files} files / {added} repos")
|
|
459
|
+
flush()
|
|
460
|
+
con.execute("DELETE FROM poc_orphan")
|
|
461
|
+
con.execute(
|
|
462
|
+
"INSERT INTO poc_orphan(cve_id,count) SELECT p.cve_id,count(*) FROM poc p "
|
|
463
|
+
"WHERE NOT EXISTS(SELECT 1 FROM cve c WHERE c.id=p.cve_id) GROUP BY p.cve_id")
|
|
464
|
+
con.execute("UPDATE cve_meta SET has_poc=0, poc_count=0")
|
|
465
|
+
con.execute(
|
|
466
|
+
"UPDATE cve_meta SET has_poc=1, poc_count="
|
|
467
|
+
"(SELECT count(*) FROM poc WHERE poc.cve_id=cve_meta.cve_id) "
|
|
468
|
+
"WHERE EXISTS(SELECT 1 FROM poc WHERE poc.cve_id=cve_meta.cve_id)")
|
|
469
|
+
record_source(con, "poc-in-github", path=path, url=src["url"],
|
|
470
|
+
license=src["license"], sha256=src["sha256"], fetched_at=fetched,
|
|
471
|
+
count=added,
|
|
472
|
+
note=f"{files} CVE files parsed; repo metadata only, no code executed")
|
|
473
|
+
con.commit()
|
|
474
|
+
if progress:
|
|
475
|
+
progress(f"PoC-in-GitHub {files} files, {added} repos")
|
|
476
|
+
return added
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# ---------------------------------------------------------------- derived tables
|
|
480
|
+
def rebuild_cve_meta(con: sqlite3.Connection) -> int:
|
|
481
|
+
con.execute(
|
|
482
|
+
"INSERT OR IGNORE INTO cve_meta(cve_id,published_year,cve_year) "
|
|
483
|
+
"SELECT id, NULL, CAST(substr(id,5,4) AS INTEGER) FROM cve")
|
|
484
|
+
con.execute(
|
|
485
|
+
"UPDATE cve_meta SET published_year=CASE WHEN substr("
|
|
486
|
+
"(SELECT published FROM cve WHERE cve.id=cve_meta.cve_id),1,4) GLOB '[0-9][0-9][0-9][0-9]' "
|
|
487
|
+
"THEN CAST(substr((SELECT published FROM cve WHERE cve.id=cve_meta.cve_id),1,4) AS INTEGER) "
|
|
488
|
+
"ELSE NULL END WHERE published_year IS NULL")
|
|
489
|
+
con.execute(
|
|
490
|
+
"UPDATE cve_meta SET cve_year=CAST(substr(cve_id,5,4) AS INTEGER) "
|
|
491
|
+
"WHERE cve_year IS NULL")
|
|
492
|
+
# Only CVEs that are actually in the index carry metadata rows; rejected or
|
|
493
|
+
# reserved CVE List records stay in cve_cna alone.
|
|
494
|
+
con.execute("DELETE FROM cve_meta WHERE cve_id NOT IN (SELECT id FROM cve)")
|
|
495
|
+
return con.execute("SELECT count(*) FROM cve_meta").fetchone()[0]
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def stub_missing_cwes(con: sqlite3.Connection) -> int:
|
|
499
|
+
before = con.execute("SELECT count(*) FROM cwe").fetchone()[0]
|
|
500
|
+
con.execute(
|
|
501
|
+
"INSERT OR IGNORE INTO cwe(id,name,abstraction,status,description) "
|
|
502
|
+
"SELECT DISTINCT cwe,'Deprecated or withdrawn CWE '||cwe,'Unknown','Deprecated',"
|
|
503
|
+
"'Referenced by CVE records but absent from the current CWE catalog; usually a "
|
|
504
|
+
"deprecated CWE retained for historical mapping.' FROM cve_cwe "
|
|
505
|
+
"WHERE cwe NOT IN (SELECT id FROM cwe)")
|
|
506
|
+
return con.execute("SELECT count(*) FROM cwe").fetchone()[0] - before
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def rebuild_aliases(con: sqlite3.Connection) -> int:
|
|
510
|
+
con.execute("DELETE FROM cve_alias WHERE source='osv'")
|
|
511
|
+
rows: list[tuple[str, str, str]] = []
|
|
512
|
+
for cve, aliases in con.execute(
|
|
513
|
+
"SELECT cve,aliases FROM advisory WHERE cve<>'' AND aliases<>''"):
|
|
514
|
+
for alias in str(aliases).split(","):
|
|
515
|
+
alias = alias.strip().upper()
|
|
516
|
+
if alias and alias != cve and len(alias) <= 100:
|
|
517
|
+
rows.append((cve, alias, "osv"))
|
|
518
|
+
if rows:
|
|
519
|
+
con.executemany("INSERT OR IGNORE INTO cve_alias VALUES (?,?,?)", rows)
|
|
520
|
+
con.commit()
|
|
521
|
+
return len(rows)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def already_loaded(con: sqlite3.Connection, source: str, path: pathlib.Path) -> bool:
|
|
525
|
+
"""True when this exact archive was already loaded (idempotent skip)."""
|
|
526
|
+
row = con.execute(
|
|
527
|
+
"SELECT sha256, status FROM source_state WHERE source=?", (source,)).fetchone()
|
|
528
|
+
if not row or row[1] != "loaded" or not row[0]:
|
|
529
|
+
return False
|
|
530
|
+
if not path.exists():
|
|
531
|
+
return False
|
|
532
|
+
return _sha256(path) == row[0]
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def load_all(con: sqlite3.Connection, *, epss: dict | None = None,
|
|
536
|
+
kev_ids: set[str] | None = None, progress=None,
|
|
537
|
+
force: bool = False) -> dict:
|
|
538
|
+
"""Load every staged v3 source that exists. Missing files are skipped.
|
|
539
|
+
|
|
540
|
+
Archives whose SHA-256 already matches `source_state` are skipped, so a
|
|
541
|
+
repeated run is cheap and produces identical rows.
|
|
542
|
+
"""
|
|
543
|
+
counts: dict[str, int] = {}
|
|
544
|
+
cve_tar = paths.DATA / "intel/cveorg/cvelistV5-main.tar.gz"
|
|
545
|
+
poc_tar = paths.DATA / "intel/poc-in-github/PoC-in-GitHub-master.tar.gz"
|
|
546
|
+
if force or not already_loaded(con, "cveorg-cvelist", cve_tar):
|
|
547
|
+
counts["cveorg"] = load_cveorg(cve_tar, con, epss=epss, kev_ids=kev_ids,
|
|
548
|
+
progress=progress)
|
|
549
|
+
else:
|
|
550
|
+
counts["cveorg"] = 0
|
|
551
|
+
if progress:
|
|
552
|
+
progress("CVE List unchanged (sha256 match) - skipped")
|
|
553
|
+
if force or not already_loaded(con, "poc-in-github", poc_tar):
|
|
554
|
+
counts["poc"] = load_poc_github(poc_tar, con, progress=progress)
|
|
555
|
+
else:
|
|
556
|
+
counts["poc"] = 0
|
|
557
|
+
if progress:
|
|
558
|
+
progress("PoC-in-GitHub unchanged (sha256 match) - skipped")
|
|
559
|
+
counts["cwe_stubs"] = stub_missing_cwes(con)
|
|
560
|
+
counts["aliases"] = rebuild_aliases(con)
|
|
561
|
+
con.commit()
|
|
562
|
+
return counts
|