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