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/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