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/query.py ADDED
@@ -0,0 +1,551 @@
1
+ """Read-only query layer. Every function returns plain dicts (JSON-ready)."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import sqlite3
6
+ import urllib.request
7
+
8
+ from . import datasets, db, paths
9
+
10
+ CVE_RE = re.compile(r"CVE-\d{4}-\d{4,7}", re.I)
11
+
12
+
13
+ # ---------------------------------------------------------------- helpers
14
+ def fts_query(q: str) -> tuple[str, list[str]]:
15
+ toks = [t for t in re.findall(r"[A-Za-z0-9_.]{2,}", q)]
16
+ if not toks:
17
+ raise ValueError("query too short")
18
+ terms = [f'"{t}"*' if len(t) >= 4 else f'"{t}"' for t in toks]
19
+ return " AND ".join(terms), toks
20
+
21
+
22
+ def vkey(v: str):
23
+ """Orderable version key: 1.2.3p1 -> [(1,''),(2,''),(3,'p'),(1,'')]."""
24
+ out = []
25
+ for part in re.split(r"[.\-_+~]", v or ""):
26
+ if not part:
27
+ continue
28
+ if part.isdigit():
29
+ out.append((int(part), ""))
30
+ else:
31
+ m = re.match(r"^(\d+)(.*)$", part)
32
+ out.append((int(m.group(1)), m.group(2).lower()) if m else (10 ** 9, part.lower()))
33
+ return out
34
+
35
+
36
+ def version_affected(row: sqlite3.Row, version: str) -> bool:
37
+ ver = (row["version"] or "").strip()
38
+ if ver not in ("*", "-", ""):
39
+ return ver.lower() == version.lower()
40
+ try:
41
+ v = vkey(version)
42
+ if row["v_start_incl"] and v < vkey(row["v_start_incl"]):
43
+ return False
44
+ if row["v_start_excl"] and v <= vkey(row["v_start_excl"]):
45
+ return False
46
+ if row["v_end_incl"] and v > vkey(row["v_end_incl"]):
47
+ return False
48
+ if row["v_end_excl"] and v >= vkey(row["v_end_excl"]):
49
+ return False
50
+ except (TypeError, ValueError):
51
+ return False
52
+ return True
53
+
54
+
55
+ def range_label(row: sqlite3.Row) -> str:
56
+ base = f"{row['vendor']}:{row['product']}"
57
+ ver = (row["version"] or "").strip()
58
+ if ver not in ("*", "-", ""):
59
+ return f"{base} {ver}"
60
+ bits = []
61
+ if row["v_start_incl"]:
62
+ bits.append(f">={row['v_start_incl']}")
63
+ if row["v_start_excl"]:
64
+ bits.append(f">{row['v_start_excl']}")
65
+ if row["v_end_incl"]:
66
+ bits.append(f"<={row['v_end_incl']}")
67
+ if row["v_end_excl"]:
68
+ bits.append(f"<{row['v_end_excl']}")
69
+ return base + (" " + " ".join(bits) if bits else " (any)")
70
+
71
+
72
+ # ---------------------------------------------------------------- search
73
+ def search(con: sqlite3.Connection, query: str = "", *, limit: int = 20,
74
+ offset: int = 0, sort: str = "relevance", severity: str | None = None,
75
+ year: int | None = None, since: str | None = None, kev: bool = False,
76
+ exploit: bool = False, epss: float | None = None, cvss: float | None = None,
77
+ cwe: str | None = None, product: str | None = None,
78
+ vendor: str | None = None, ssvc: str | None = None,
79
+ poc: bool = False) -> list[dict]:
80
+ where, params = [], []
81
+ use_fts = bool(query)
82
+ toks: list[str] = []
83
+ if use_fts:
84
+ fq, toks = fts_query(query)
85
+ where.append("cve_fts MATCH ?")
86
+ params.append(fq)
87
+ if severity:
88
+ sevs = [s.upper() for s in severity.split(",")]
89
+ where.append(f"c.severity IN ({','.join('?' * len(sevs))})")
90
+ params += sevs
91
+ if year:
92
+ where.append("c.year = ?")
93
+ params.append(int(year))
94
+ if since:
95
+ where.append("c.published >= ?")
96
+ params.append(since)
97
+ if kev:
98
+ where.append("c.kev = 1")
99
+ if exploit:
100
+ where.append("c.has_exploit = 1")
101
+ if epss is not None:
102
+ where.append("c.epss >= ?")
103
+ params.append(epss)
104
+ if cvss is not None:
105
+ where.append("c.cvss >= ?")
106
+ params.append(cvss)
107
+ if cwe:
108
+ where.append("EXISTS(SELECT 1 FROM cve_cwe w WHERE w.cve_id=c.id AND w.cwe=?)")
109
+ params.append(cwe.upper())
110
+ if product:
111
+ where.append("EXISTS(SELECT 1 FROM cve_product p WHERE p.cve_id=c.id AND p.product LIKE ?)")
112
+ params.append(f"%{product.lower()}%")
113
+ if vendor:
114
+ where.append("EXISTS(SELECT 1 FROM cve_product p WHERE p.cve_id=c.id AND p.vendor LIKE ?)")
115
+ params.append(f"%{vendor.lower()}%")
116
+ if ssvc:
117
+ where.append("EXISTS(SELECT 1 FROM ssvc s WHERE s.cve_id=c.id AND s.exploitation=?)")
118
+ params.append(ssvc)
119
+ if poc:
120
+ where.append("EXISTS(SELECT 1 FROM poc p WHERE p.cve_id=c.id)")
121
+
122
+ sql_where = (" WHERE " + " AND ".join(where)) if where else ""
123
+ base = "FROM cve_fts JOIN cve c ON c.id = cve_fts.id" if use_fts else "FROM cve c"
124
+ sel = ("SELECT c.*, (SELECT count(*) FROM cve_exploit x WHERE x.cve_id=c.id) AS exploits,"
125
+ " (SELECT count(*) FROM cve_nuclei n WHERE n.cve_id=c.id) AS nuclei_count,"
126
+ " (SELECT coalesce(poc_count,0) FROM cve_meta m WHERE m.cve_id=c.id) AS poc_count"
127
+ + (", bm25(cve_fts) AS rank" if use_fts else ""))
128
+ sql = f"{sel} {base}{sql_where}"
129
+ order = {
130
+ "relevance": "ORDER BY c.kev DESC, c.epss DESC",
131
+ "newest": "ORDER BY c.published DESC",
132
+ "oldest": "ORDER BY c.published ASC",
133
+ "cvss": "ORDER BY c.cvss DESC",
134
+ "epss": "ORDER BY c.epss DESC",
135
+ }[sort]
136
+
137
+ if use_fts and sort == "relevance":
138
+ pool = min(max(300, (offset + limit) * 8), 2000)
139
+ rows = con.execute(sql + " ORDER BY rank LIMIT ?", [*params, pool]).fetchall()
140
+ if not rows and len(toks) > 1:
141
+ params[0] = f'"{max(toks, key=len)}"*'
142
+ rows = con.execute(sql + " ORDER BY rank LIMIT ?", [*params, pool]).fetchall()
143
+ if rows:
144
+ top = max(abs(r["rank"]) for r in rows) or 1.0
145
+ scored = [(10.0 * abs(r["rank"]) / top + 4.0 * r["kev"]
146
+ + 5.0 * (r["epss"] or 0) + 1.5 * min(r["exploits"], 3)
147
+ + 1.0 * min(r["poc_count"] or 0, 3), r)
148
+ for r in rows]
149
+ scored.sort(key=lambda pair: -pair[0])
150
+ rows = [r for _, r in scored][offset:offset + limit]
151
+ else:
152
+ rows = con.execute(sql + f" {order} LIMIT ? OFFSET ?",
153
+ [*params, limit, offset]).fetchall()
154
+ if not rows and use_fts and len(toks) > 1 and offset == 0:
155
+ params[0] = f'"{max(toks, key=len)}"*'
156
+ rows = con.execute(sql + f" {order} LIMIT ? OFFSET ?",
157
+ [*params, limit, offset]).fetchall()
158
+ return [{k: v for k, v in dict(r).items() if k != "rank"} for r in rows]
159
+
160
+
161
+ # Per-CVE array caps keep cb_cve responses small enough to call every turn.
162
+ # The *_total fields tell the caller how many rows exist beyond the cap;
163
+ # use `crowbar sql` / the CLI for the full list.
164
+ CVE_ARRAY_CAP = 50
165
+
166
+ # ---------------------------------------------------------------- one CVE
167
+ def get_cve(con: sqlite3.Connection, cid: str) -> dict:
168
+ cid = cid.strip().upper()
169
+ if not cid.startswith("CVE-"):
170
+ cid = "CVE-" + cid
171
+ r = con.execute("SELECT * FROM cve WHERE id=?", (cid,)).fetchone()
172
+ if not r:
173
+ r = con.execute(
174
+ "SELECT c.* FROM cve_alias a JOIN cve c ON c.id=a.cve_id "
175
+ "WHERE a.alias=?", (cid,)).fetchone()
176
+ if not r:
177
+ k = con.execute("SELECT date_added, epss FROM kev WHERE cve=?", (cid,)).fetchone()
178
+ if k:
179
+ raise KeyError(f"{cid} has no NVD record in this index, but it IS in CISA KEV "
180
+ f"(added {k['date_added']}, EPSS {k['epss']}). A lite index only "
181
+ f"carries recent NVD years; use cb_kev, or build a bigger tier.")
182
+ raise KeyError(cid)
183
+ out = dict(r)
184
+ cid = out["id"]
185
+ meta = con.execute("SELECT * FROM cve_meta WHERE cve_id=?", (cid,)).fetchone()
186
+ out["published_year"] = meta["published_year"] if meta else None
187
+ out["cve_year"] = meta["cve_year"] if meta else None
188
+ out["poc_count"] = meta["poc_count"] if meta else 0
189
+ out["pocs"] = [dict(x) for x in con.execute(
190
+ "SELECT source, repo, url, stars, language, created_at FROM poc "
191
+ "WHERE cve_id=? ORDER BY stars DESC, repo LIMIT 50", (cid,))]
192
+ cna = con.execute("SELECT * FROM cve_cna WHERE cve_id=?", (cid,)).fetchone()
193
+ out["cna"] = dict(cna) if cna else None
194
+ out["aliases"] = [x[0] for x in con.execute(
195
+ "SELECT alias FROM cve_alias WHERE cve_id=? ORDER BY alias", (cid,))]
196
+ out["cwe"] = [x[0] for x in con.execute("SELECT cwe FROM cve_cwe WHERE cve_id=?", (cid,))]
197
+ out["products_total"] = con.execute(
198
+ "SELECT count(*) FROM cve_product WHERE cve_id=?", (cid,)).fetchone()[0]
199
+ out["products"] = [dict(x) for x in con.execute(
200
+ "SELECT vendor, product FROM cve_product WHERE cve_id=? ORDER BY product LIMIT ?",
201
+ (cid, CVE_ARRAY_CAP))]
202
+ out["affects_total"] = con.execute(
203
+ "SELECT count(*) FROM cpe_match WHERE cve_id=?", (cid,)).fetchone()[0]
204
+ out["affects"] = [{**dict(x), "label": range_label(x)} for x in con.execute(
205
+ "SELECT * FROM cpe_match WHERE cve_id=? ORDER BY vendor, product, version LIMIT ?",
206
+ (cid, CVE_ARRAY_CAP))]
207
+ out["references_total"] = con.execute(
208
+ "SELECT count(*) FROM cve_ref WHERE cve_id=?", (cid,)).fetchone()[0]
209
+ out["references"] = [dict(x) for x in con.execute(
210
+ "SELECT url, tags FROM cve_ref WHERE cve_id=? LIMIT ?", (cid, CVE_ARRAY_CAP))]
211
+ kev = con.execute("SELECT * FROM kev WHERE cve=?", (cid,)).fetchone()
212
+ out["kev_detail"] = dict(kev) if kev else None
213
+ out["exploits_total"] = con.execute(
214
+ "SELECT count(*) FROM cve_exploit WHERE cve_id=?", (cid,)).fetchone()[0]
215
+ out["exploits"] = [dict(x) for x in con.execute(
216
+ "SELECT e.id, e.description, e.type, e.platform, e.verified, e.url FROM exploit e "
217
+ "JOIN cve_exploit x ON x.exploit_id=e.id WHERE x.cve_id=? ORDER BY e.id LIMIT ?", (cid, CVE_ARRAY_CAP))]
218
+ out["nuclei"] = [dict(x) for x in con.execute(
219
+ "SELECT n.id, n.name, n.severity, n.path, n.domain FROM nuclei n "
220
+ "JOIN cve_nuclei x ON x.template_id=n.id WHERE x.cve_id=?", (cid,))]
221
+ out["metasploit"] = [dict(x) for x in con.execute(
222
+ "SELECT m.path, m.type, m.name FROM msf m JOIN cve_msf x ON x.module_path=m.path "
223
+ "WHERE x.cve_id=?", (cid,))]
224
+ s = con.execute("SELECT * FROM ssvc WHERE cve_id=?", (cid,)).fetchone()
225
+ out["ssvc"] = dict(s) if s else None
226
+ out["os"] = [dict(x) for x in con.execute(
227
+ "SELECT source, platform, product, version, title, severity, impact, exploited, url "
228
+ "FROM os_advisory WHERE cve=? ORDER BY platform, released DESC", (cid,))]
229
+ out["advisories"] = [dict(x) for x in con.execute(
230
+ "SELECT source, id, ecosystem, package, severity, summary, url "
231
+ "FROM advisory WHERE cve=? LIMIT 20", (cid,))]
232
+ out["debian"] = [dict(x) for x in con.execute(
233
+ "SELECT package, release, status, fixed, urgency FROM debian WHERE cve=? LIMIT 20", (cid,))]
234
+ return out
235
+
236
+
237
+ # ---------------------------------------------------------------- product
238
+ def products(con: sqlite3.Connection, name: str, version: str | None = None,
239
+ limit: int = 25) -> list[dict]:
240
+ q = f"%{name.lower()}%"
241
+ rows = con.execute(
242
+ "SELECT * FROM cpe_match WHERE product LIKE ? OR vendor LIKE ? LIMIT 20000",
243
+ (q, q)).fetchall()
244
+ grouped: dict[str, list[sqlite3.Row]] = {}
245
+ for r in rows:
246
+ grouped.setdefault(r["cve_id"], []).append(r)
247
+
248
+ matches = []
249
+ for cid, crows in grouped.items():
250
+ hits = [r for r in crows if version_affected(r, version)] if version else crows
251
+ if version and not hits:
252
+ continue
253
+ matches.append((cid, hits))
254
+ if version:
255
+ matches.sort(key=lambda p: (-len(p[1]), p[0]))
256
+ out = []
257
+ for cid, hits in matches[:limit]:
258
+ c = con.execute("SELECT * FROM cve WHERE id=?", (cid,)).fetchone()
259
+ if not c:
260
+ continue
261
+ d = dict(c)
262
+ d["matched"] = sorted({range_label(h) for h in hits})[:6]
263
+ d["exploits"] = con.execute(
264
+ "SELECT count(*) FROM cve_exploit WHERE cve_id=?", (cid,)).fetchone()[0]
265
+ d["nuclei_count"] = con.execute(
266
+ "SELECT count(*) FROM cve_nuclei WHERE cve_id=?", (cid,)).fetchone()[0]
267
+ out.append(d)
268
+ return out
269
+
270
+
271
+ # ---------------------------------------------------------------- intel
272
+ def kev_list(con: sqlite3.Connection, q: str | None = None, limit: int = 25) -> list[dict]:
273
+ sql = "SELECT * FROM kev"
274
+ params: list = []
275
+ if q:
276
+ sql += " WHERE cve LIKE ? OR name LIKE ? OR vendor LIKE ? OR product LIKE ? OR notes LIKE ?"
277
+ params = [f"%{q}%"] * 5
278
+ sql += " ORDER BY date_added DESC LIMIT ?"
279
+ params.append(limit)
280
+ return [dict(r) for r in con.execute(sql, params)]
281
+
282
+
283
+ def exploit_search(con: sqlite3.Connection, q: str, limit: int = 20) -> list[dict]:
284
+ return [dict(r) for r in con.execute(
285
+ "SELECT e.*, bm25(exploit_fts) AS rank FROM exploit_fts "
286
+ "JOIN exploit e ON e.id=exploit_fts.id WHERE exploit_fts MATCH ? "
287
+ "ORDER BY rank LIMIT ?", (fts_query(q)[0], limit))]
288
+
289
+
290
+ def exploit_lookup(con: sqlite3.Connection, eid: str) -> dict:
291
+ """Offline: return the indexed Exploit-DB row for an id, with a hint to fetch
292
+ the source via the CLI. Never touches the network (safe for the MCP server)."""
293
+ r = con.execute("SELECT * FROM exploit WHERE id=?", (str(eid),)).fetchone()
294
+ if not r:
295
+ return {"id": eid, "error": "not_found"}
296
+ out = dict(r)
297
+ out["fetch"] = f"crowbar exploit {eid} # downloads the source locally (CLI only)"
298
+ return out
299
+
300
+
301
+ def exploit_fetch(con: sqlite3.Connection, eid: str) -> dict:
302
+ e = con.execute("SELECT * FROM exploit WHERE id=?", (eid,)).fetchone()
303
+ if not e:
304
+ raise KeyError(eid)
305
+ base = (paths.DATA / "exploitdb" / "files").resolve()
306
+ dest = paths.DATA / "exploitdb" / "files" / (e["file"] or f"{e['id']}.txt")
307
+ try: # reject a poisoned ../ path in the index
308
+ dest.resolve().relative_to(base)
309
+ except ValueError:
310
+ raise ValueError(f"unsafe exploit file path: {e['file']!r}")
311
+ dest.parent.mkdir(parents=True, exist_ok=True)
312
+ if not dest.exists():
313
+ req = urllib.request.Request(e["raw"], headers={"User-Agent": datasets.UA})
314
+ with urllib.request.urlopen(req, timeout=60) as resp:
315
+ dest.write_bytes(resp.read())
316
+ return {**dict(e), "local_path": str(dest)}
317
+
318
+
319
+ def nuclei_search(con: sqlite3.Connection, q: str, limit: int = 20,
320
+ domain: str | None = None) -> list[dict]:
321
+ sql = ("SELECT n.*, bm25(nuclei_fts) AS rank FROM nuclei_fts "
322
+ "JOIN nuclei n ON n.id=nuclei_fts.id WHERE nuclei_fts MATCH ?")
323
+ params: list = [fts_query(q)[0]]
324
+ if domain:
325
+ sql += " AND n.domain = ?"
326
+ params.append(domain)
327
+ sql += " ORDER BY rank LIMIT ?"
328
+ params.append(limit)
329
+ return [{k: v for k, v in dict(r).items() if k != "rank"}
330
+ for r in con.execute(sql, params)]
331
+
332
+
333
+ def msf_search(con: sqlite3.Connection, q: str, limit: int = 30) -> list[dict]:
334
+ return [dict(r) for r in con.execute(
335
+ "SELECT * FROM msf WHERE name LIKE ? ORDER BY type, name LIMIT ?",
336
+ (f"%{q}%", limit))]
337
+
338
+
339
+ def models_list(con: sqlite3.Connection, q: str | None = None, provider: str | None = None,
340
+ limit: int = 40) -> list[dict]:
341
+ sql = "SELECT * FROM model"
342
+ params: list = []
343
+ if q:
344
+ sql += " WHERE (id LIKE ? OR name LIKE ? OR provider LIKE ? OR provider_name LIKE ?)"
345
+ params = [f"%{q}%"] * 4
346
+ if provider:
347
+ sql += (" AND " if q else " WHERE ") + "provider = ?"
348
+ params.append(provider)
349
+ boost = "lower(id) = lower(?) DESC, " if q else ""
350
+ sql += (f" ORDER BY {boost}"
351
+ "CASE WHEN input_cost IS NULL OR input_cost = 0 THEN 1 ELSE 0 END,"
352
+ " input_cost, context DESC LIMIT ?")
353
+ if q:
354
+ params.append(q)
355
+ params.append(limit)
356
+ return [dict(r) for r in con.execute(sql, params)]
357
+
358
+
359
+ def attack_lookup(con: sqlite3.Connection, q: str, limit: int = 10,
360
+ domain: str | None = None) -> list[dict]:
361
+ sql = ("SELECT * FROM attack WHERE (id LIKE ? OR name LIKE ? OR tactics LIKE ?"
362
+ " OR description LIKE ?)")
363
+ params: list = [f"%{q}%"] * 4
364
+ if domain:
365
+ sql += " AND domain = ?"
366
+ params.append(domain)
367
+ sql += " LIMIT ?"
368
+ params.append(limit)
369
+ return [dict(r) for r in con.execute(sql, params)]
370
+
371
+
372
+ def cwe_get(con: sqlite3.Connection, q: str | None, limit: int = 30) -> dict | list[dict]:
373
+ if q and re.fullmatch(r"(\d+|CWE-\d+)", q, re.I):
374
+ cid = "CWE-" + q.upper().removeprefix("CWE-")
375
+ r = con.execute("SELECT * FROM cwe WHERE id=?", (cid,)).fetchone()
376
+ if not r:
377
+ raise KeyError(cid)
378
+ return dict(r)
379
+ sql = "SELECT * FROM cwe"
380
+ params: list = []
381
+ if q:
382
+ sql += " WHERE name LIKE ? OR description LIKE ?"
383
+ params = [f"%{q}%"] * 2
384
+ sql += " ORDER BY CAST(substr(id,5) AS INTEGER) LIMIT ?"
385
+ params.append(limit)
386
+ return [dict(r) for r in con.execute(sql, params)]
387
+
388
+
389
+ def capec_search(con: sqlite3.Connection, q: str, limit: int = 15) -> list[dict]:
390
+ return [dict(r) for r in con.execute(
391
+ "SELECT * FROM capec WHERE id LIKE ? OR name LIKE ? OR cwe LIKE ?"
392
+ " OR description LIKE ? LIMIT ?", [*([f"%{q}%"] * 4), limit])]
393
+
394
+
395
+ # ---------------------------------------------------------------- bulk intel
396
+ def os_search(con: sqlite3.Connection, q: str | None = None, platform: str | None = None,
397
+ impact: str | None = None, exploited: bool = False,
398
+ since: str | None = None, limit: int = 40) -> list[dict]:
399
+ """Vendor OS advisories: MSRC (windows) and Apple (ios/macos)."""
400
+ sql = "SELECT * FROM os_advisory WHERE 1=1"
401
+ params: list = []
402
+ if platform:
403
+ sql += " AND platform = ?"
404
+ params.append(platform.lower())
405
+ if q:
406
+ sql += (" AND (cve LIKE ? OR product LIKE ? OR title LIKE ? OR version LIKE ?)")
407
+ params += [f"%{q}%"] * 4
408
+ if impact:
409
+ sql += " AND impact = ?"
410
+ params.append(impact)
411
+ if exploited:
412
+ sql += " AND exploited != ''"
413
+ if since:
414
+ sql += " AND released >= ?"
415
+ params.append(since)
416
+ sql += " ORDER BY released DESC, cve DESC LIMIT ?"
417
+ params.append(limit)
418
+ return [dict(r) for r in con.execute(sql, params)]
419
+
420
+
421
+ def advisory_search(con: sqlite3.Connection, q: str | None = None,
422
+ ecosystem: str | None = None, package: str | None = None,
423
+ limit: int = 30) -> list[dict]:
424
+ """OSV advisories (GHSA + per-ecosystem databases)."""
425
+ sql = "SELECT * FROM advisory WHERE source='osv'"
426
+ params: list = []
427
+ if ecosystem:
428
+ sql += " AND ecosystem = ?"
429
+ params.append(ecosystem)
430
+ if package:
431
+ sql += " AND package = ?"
432
+ params.append(package.lower())
433
+ if q:
434
+ sql += " AND (id LIKE ? OR package LIKE ? OR summary LIKE ? OR cve LIKE ? OR aliases LIKE ?)"
435
+ params += [f"%{q}%"] * 5
436
+ sql += " ORDER BY COALESCE(published, '') DESC LIMIT ?"
437
+ params.append(limit)
438
+ rows = [dict(r) for r in con.execute(sql, params)]
439
+ if rows:
440
+ ids = [r["id"] for r in rows]
441
+ marks = ",".join("?" * len(ids))
442
+ ranges: dict[str, list[dict]] = {}
443
+ for r in con.execute(
444
+ f"SELECT advisory_id, ecosystem, package, introduced, fixed, last_affected, versions "
445
+ f"FROM package_range WHERE advisory_id IN ({marks})", ids):
446
+ ranges.setdefault(r["advisory_id"], []).append({
447
+ "ecosystem": r["ecosystem"], "package": r["package"],
448
+ "introduced": r["introduced"], "fixed": r["fixed"],
449
+ "last_affected": r["last_affected"], "versions": r["versions"],
450
+ })
451
+ for r in rows:
452
+ r["ranges"] = ranges.get(r["id"], [])
453
+ return rows
454
+
455
+
456
+ def code_search(con: sqlite3.Connection, q: str, limit: int = 20) -> list[dict]:
457
+ """Full-text search inside Exploit-DB sources."""
458
+ sql = ("SELECT exploit_id, path, snippet(exploit_code_fts, 2, '[', ']', ' … ', 20) AS snippet,"
459
+ " bm25(exploit_code_fts) AS rank FROM exploit_code_fts "
460
+ "WHERE exploit_code_fts MATCH ? ORDER BY rank LIMIT ?")
461
+ return [{k: v for k, v in dict(r).items() if k != "rank"}
462
+ for r in con.execute(sql, (fts_query(q)[0], limit))]
463
+
464
+
465
+ def toolkit_search(con: sqlite3.Connection, q: str, source: str | None = None,
466
+ limit: int = 25) -> list[dict]:
467
+ """GTFOBins / LOLBAS / PayloadsAllTheThings."""
468
+ sql = ("SELECT t.source, t.name, t.path, t.description, t.tags "
469
+ "FROM toolkit_fts f JOIN toolkit t ON t.source=f.source AND t.name=f.name "
470
+ "WHERE toolkit_fts MATCH ?")
471
+ params: list = [fts_query(q)[0]]
472
+ if source:
473
+ sql += " AND t.source = ?"
474
+ params.append(source)
475
+ sql += " ORDER BY lower(t.name) = lower(?) DESC, bm25(toolkit_fts) LIMIT ?"
476
+ params.extend([q, limit])
477
+ return [dict(r) for r in con.execute(sql, params)]
478
+
479
+
480
+ def debian_lookup(con: sqlite3.Connection, package: str, cve: str | None = None) -> list[dict]:
481
+ sql = "SELECT * FROM debian WHERE package = ?"
482
+ params: list = [package.lower()]
483
+ if cve:
484
+ sql += " AND cve = ?"
485
+ params.append(cve.upper())
486
+ sql += " ORDER BY cve, release"
487
+ return [dict(r) for r in con.execute(sql, params)]
488
+
489
+
490
+ # ---------------------------------------------------------------- public PoCs
491
+ def poc_search(con: sqlite3.Connection, q: str = "", *, cve: str | None = None,
492
+ language: str | None = None, min_stars: int = 0,
493
+ limit: int = 25) -> list[dict]:
494
+ """Public GitHub PoC repositories. Metadata only - nothing is executed."""
495
+ sql = ("SELECT p.cve_id, p.repo, p.url, p.stars, p.language, p.created_at, "
496
+ "p.pushed_at, p.description, p.source FROM poc p WHERE 1=1")
497
+ params: list = []
498
+ if cve:
499
+ sql += " AND p.cve_id = ?"
500
+ params.append(cve.upper())
501
+ if q:
502
+ sql += " AND (p.repo LIKE ? OR p.description LIKE ? OR p.cve_id LIKE ?)"
503
+ params += [f"%{q}%"] * 3
504
+ if language:
505
+ sql += " AND p.language = ?"
506
+ params.append(language)
507
+ if min_stars:
508
+ sql += " AND p.stars >= ?"
509
+ params.append(int(min_stars))
510
+ sql += " ORDER BY p.stars DESC, p.cve_id LIMIT ?"
511
+ params.append(limit)
512
+ return [dict(r) for r in con.execute(sql, params)]
513
+
514
+
515
+ def poc_cves(con: sqlite3.Connection, limit: int = 25) -> list[dict]:
516
+ """CVEs with the most public PoC repositories."""
517
+ return [dict(r) for r in con.execute(
518
+ "SELECT m.cve_id, m.poc_count, c.severity, c.cvss, c.epss, c.kev, "
519
+ "substr(c.description,1,120) AS description FROM cve_meta m "
520
+ "JOIN cve c ON c.id = m.cve_id WHERE m.has_poc = 1 "
521
+ "ORDER BY m.poc_count DESC LIMIT ?", (limit,))]
522
+
523
+
524
+ # ---------------------------------------------------------------- meta
525
+ def stats(con: sqlite3.Connection) -> dict:
526
+ counts = db.counts(con)
527
+ try:
528
+ counts["cve_with_epss"] = con.execute(
529
+ "SELECT count(*) FROM cve WHERE epss IS NOT NULL").fetchone()[0]
530
+ counts["cve_kev"] = con.execute("SELECT count(*) FROM cve WHERE kev=1").fetchone()[0]
531
+ counts["cve_with_exploit"] = con.execute(
532
+ "SELECT count(*) FROM cve WHERE has_exploit=1").fetchone()[0]
533
+ counts["cve_with_nuclei"] = con.execute(
534
+ "SELECT count(*) FROM cve WHERE has_nuclei=1").fetchone()[0]
535
+ counts["cve_with_msf"] = con.execute(
536
+ "SELECT count(*) FROM cve WHERE has_msf=1").fetchone()[0]
537
+ except sqlite3.DatabaseError:
538
+ pass
539
+ counts["db_bytes"] = paths.DB.stat().st_size if paths.DB.exists() else 0
540
+ counts["meta"] = db.meta(con)
541
+ counts["freshness"] = datasets.freshness()
542
+ return counts
543
+
544
+
545
+ def sql_readonly(con: sqlite3.Connection, q: str, limit: int = 200) -> list[dict]:
546
+ q = q.strip().rstrip(";")
547
+ if not re.match(r"^(select|with|pragma|explain)\b", q, re.I):
548
+ raise ValueError("read-only: only SELECT / WITH / PRAGMA / EXPLAIN")
549
+ cur = con.execute(q)
550
+ cols = [d[0] for d in cur.description] if cur.description else []
551
+ return [{c: r[i] for i, c in enumerate(cols)} for r in cur.fetchmany(limit)]