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/indexer.py
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
"""Build and update the crowbar SQLite index."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import csv
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import sqlite3
|
|
11
|
+
import time
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
import zipfile
|
|
14
|
+
|
|
15
|
+
from . import intel, loaders, paths
|
|
16
|
+
|
|
17
|
+
CVE_RE = re.compile(r"CVE-\d{4}-\d{4,7}", re.I)
|
|
18
|
+
|
|
19
|
+
SCHEMA = """
|
|
20
|
+
CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE cve(
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
published TEXT, modified TEXT, status TEXT,
|
|
25
|
+
description TEXT,
|
|
26
|
+
cvss REAL, cvss_version TEXT, cvss_vector TEXT, severity TEXT,
|
|
27
|
+
kev INTEGER DEFAULT 0, epss REAL, epss_pct REAL,
|
|
28
|
+
has_exploit INTEGER DEFAULT 0, has_nuclei INTEGER DEFAULT 0, has_msf INTEGER DEFAULT 0,
|
|
29
|
+
year INTEGER
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE cve_cwe(cve_id TEXT, cwe TEXT);
|
|
32
|
+
CREATE UNIQUE INDEX cve_cwe_unique ON cve_cwe(cve_id, cwe);
|
|
33
|
+
CREATE TABLE cve_product(cve_id TEXT, vendor TEXT, product TEXT);
|
|
34
|
+
CREATE UNIQUE INDEX cve_product_unique ON cve_product(cve_id, vendor, product);
|
|
35
|
+
CREATE TABLE cpe_match(
|
|
36
|
+
cve_id TEXT, part TEXT, vendor TEXT, product TEXT, version TEXT,
|
|
37
|
+
v_start_incl TEXT, v_start_excl TEXT, v_end_incl TEXT, v_end_excl TEXT
|
|
38
|
+
);
|
|
39
|
+
CREATE UNIQUE INDEX cpe_match_unique ON cpe_match(
|
|
40
|
+
cve_id, part, vendor, product, version,
|
|
41
|
+
v_start_incl, v_start_excl, v_end_incl, v_end_excl
|
|
42
|
+
);
|
|
43
|
+
CREATE TABLE cve_ref(cve_id TEXT, url TEXT, tags TEXT);
|
|
44
|
+
CREATE UNIQUE INDEX cve_ref_unique ON cve_ref(cve_id, url);
|
|
45
|
+
CREATE TABLE cve_exploit(cve_id TEXT, exploit_id TEXT, PRIMARY KEY(cve_id, exploit_id));
|
|
46
|
+
CREATE TABLE cve_nuclei(cve_id TEXT, template_id TEXT, PRIMARY KEY(cve_id, template_id));
|
|
47
|
+
CREATE TABLE cve_msf(cve_id TEXT, module_path TEXT, PRIMARY KEY(cve_id, module_path));
|
|
48
|
+
|
|
49
|
+
CREATE TABLE kev(
|
|
50
|
+
cve TEXT PRIMARY KEY, vendor TEXT, product TEXT, name TEXT,
|
|
51
|
+
date_added TEXT, ransomware TEXT, action TEXT, due TEXT, notes TEXT,
|
|
52
|
+
epss REAL, epss_pct REAL
|
|
53
|
+
);
|
|
54
|
+
CREATE TABLE exploit(
|
|
55
|
+
id TEXT PRIMARY KEY, file TEXT, description TEXT, date TEXT, author TEXT,
|
|
56
|
+
type TEXT, platform TEXT, verified INTEGER, codes TEXT, tags TEXT, url TEXT, raw TEXT
|
|
57
|
+
);
|
|
58
|
+
CREATE TABLE shellcode(id TEXT PRIMARY KEY, file TEXT, description TEXT, date TEXT, platform TEXT, url TEXT);
|
|
59
|
+
CREATE TABLE nuclei(
|
|
60
|
+
id TEXT PRIMARY KEY, name TEXT, severity TEXT, tags TEXT, cve TEXT,
|
|
61
|
+
path TEXT, description TEXT, domain TEXT
|
|
62
|
+
);
|
|
63
|
+
CREATE TABLE msf(
|
|
64
|
+
path TEXT PRIMARY KEY, type TEXT, name TEXT, platform TEXT,
|
|
65
|
+
privileged INTEGER, disclosure_date TEXT, description TEXT, cves TEXT
|
|
66
|
+
);
|
|
67
|
+
CREATE TABLE attack(
|
|
68
|
+
id TEXT, domain TEXT, name TEXT, tactics TEXT, platforms TEXT,
|
|
69
|
+
description TEXT, url TEXT, PRIMARY KEY(id, domain)
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE cwe(id TEXT PRIMARY KEY, name TEXT, abstraction TEXT, status TEXT, description TEXT);
|
|
72
|
+
CREATE TABLE capec(id TEXT PRIMARY KEY, name TEXT, severity TEXT, likelihood TEXT, cwe TEXT, description TEXT);
|
|
73
|
+
CREATE TABLE model(
|
|
74
|
+
provider TEXT, provider_name TEXT, id TEXT, name TEXT, context INTEGER,
|
|
75
|
+
output_limit INTEGER, input_cost REAL, output_cost REAL, released TEXT,
|
|
76
|
+
reasoning INTEGER, tool_call INTEGER, vision INTEGER,
|
|
77
|
+
PRIMARY KEY(provider, id)
|
|
78
|
+
);
|
|
79
|
+
CREATE TABLE advisory(
|
|
80
|
+
source TEXT, id TEXT, ecosystem TEXT, package TEXT, cve TEXT,
|
|
81
|
+
summary TEXT, severity TEXT, published TEXT, modified TEXT, url TEXT,
|
|
82
|
+
aliases TEXT, affected_count INTEGER,
|
|
83
|
+
PRIMARY KEY(source, id)
|
|
84
|
+
);
|
|
85
|
+
CREATE TABLE package_range(
|
|
86
|
+
advisory_id TEXT, ecosystem TEXT, package TEXT, introduced TEXT,
|
|
87
|
+
fixed TEXT, last_affected TEXT, versions TEXT
|
|
88
|
+
);
|
|
89
|
+
CREATE TABLE debian(
|
|
90
|
+
package TEXT, release TEXT, cve TEXT, status TEXT, fixed TEXT,
|
|
91
|
+
urgency TEXT, description TEXT
|
|
92
|
+
);
|
|
93
|
+
CREATE TABLE os_advisory(
|
|
94
|
+
source TEXT, cve TEXT, platform TEXT, product TEXT, version TEXT, title TEXT,
|
|
95
|
+
severity TEXT, impact TEXT, exploited TEXT, released TEXT, url TEXT,
|
|
96
|
+
PRIMARY KEY(source, cve, product, version)
|
|
97
|
+
);
|
|
98
|
+
CREATE VIRTUAL TABLE exploit_code_fts USING fts5(
|
|
99
|
+
exploit_id UNINDEXED, path UNINDEXED, content, tokenize='porter unicode61'
|
|
100
|
+
);
|
|
101
|
+
CREATE TABLE toolkit(
|
|
102
|
+
source TEXT, name TEXT, path TEXT, description TEXT, tags TEXT
|
|
103
|
+
);
|
|
104
|
+
CREATE VIRTUAL TABLE toolkit_fts USING fts5(
|
|
105
|
+
source UNINDEXED, name, description, tags, tokenize='porter unicode61'
|
|
106
|
+
);
|
|
107
|
+
CREATE TABLE ssvc(
|
|
108
|
+
cve_id TEXT PRIMARY KEY, exploitation TEXT, automatable TEXT,
|
|
109
|
+
technical_impact TEXT, timestamp TEXT, source TEXT
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
-- Schema v3: CNA source of truth, PoC discovery, provenance.
|
|
113
|
+
CREATE TABLE cve_cna(
|
|
114
|
+
cve_id TEXT PRIMARY KEY,
|
|
115
|
+
assigner TEXT, state TEXT, published TEXT, updated TEXT,
|
|
116
|
+
title TEXT, description TEXT,
|
|
117
|
+
cvss REAL, cvss_version TEXT, cvss_vector TEXT, severity TEXT,
|
|
118
|
+
record_path TEXT, source TEXT, source_url TEXT, source_sha256 TEXT, fetched_at TEXT
|
|
119
|
+
);
|
|
120
|
+
CREATE TABLE cve_cna_ref(
|
|
121
|
+
cve_id TEXT, url TEXT, tags TEXT,
|
|
122
|
+
PRIMARY KEY(cve_id, url)
|
|
123
|
+
);
|
|
124
|
+
CREATE TABLE cve_cna_product(
|
|
125
|
+
cve_id TEXT, vendor TEXT, product TEXT,
|
|
126
|
+
PRIMARY KEY(cve_id, vendor, product)
|
|
127
|
+
);
|
|
128
|
+
CREATE TABLE cve_meta(
|
|
129
|
+
cve_id TEXT PRIMARY KEY,
|
|
130
|
+
published_year INTEGER, cve_year INTEGER,
|
|
131
|
+
has_poc INTEGER DEFAULT 0, poc_count INTEGER DEFAULT 0
|
|
132
|
+
);
|
|
133
|
+
CREATE TABLE poc(
|
|
134
|
+
source TEXT, cve_id TEXT, repo TEXT, url TEXT, repo_id INTEGER,
|
|
135
|
+
stars INTEGER, language TEXT, created_at TEXT, pushed_at TEXT,
|
|
136
|
+
description TEXT, fetched_at TEXT,
|
|
137
|
+
PRIMARY KEY(source, cve_id, url)
|
|
138
|
+
);
|
|
139
|
+
CREATE TABLE poc_orphan(cve_id TEXT PRIMARY KEY, count INTEGER);
|
|
140
|
+
CREATE TABLE cve_alias(
|
|
141
|
+
cve_id TEXT, alias TEXT, source TEXT,
|
|
142
|
+
PRIMARY KEY(cve_id, alias, source)
|
|
143
|
+
);
|
|
144
|
+
CREATE TABLE source_state(
|
|
145
|
+
source TEXT PRIMARY KEY, url TEXT, path TEXT, license TEXT, sha256 TEXT,
|
|
146
|
+
fetched_at TEXT, loaded_at TEXT, record_count INTEGER, status TEXT, note TEXT
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE VIRTUAL TABLE cve_fts USING fts5(
|
|
150
|
+
id UNINDEXED, description, products, tokenize='porter unicode61'
|
|
151
|
+
);
|
|
152
|
+
CREATE VIRTUAL TABLE exploit_fts USING fts5(
|
|
153
|
+
id UNINDEXED, description, tags, codes, tokenize='porter unicode61'
|
|
154
|
+
);
|
|
155
|
+
CREATE VIRTUAL TABLE nuclei_fts USING fts5(
|
|
156
|
+
id, name, tags, cve, tokenize='porter unicode61'
|
|
157
|
+
);
|
|
158
|
+
CREATE VIRTUAL TABLE model_fts USING fts5(
|
|
159
|
+
provider UNINDEXED, id UNINDEXED, name, tokenize='porter unicode61'
|
|
160
|
+
);
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
INDEXES = """
|
|
164
|
+
CREATE INDEX cve_year ON cve(year);
|
|
165
|
+
CREATE INDEX cve_sev ON cve(severity);
|
|
166
|
+
CREATE INDEX cve_cvss ON cve(cvss);
|
|
167
|
+
CREATE INDEX cve_kev ON cve(kev);
|
|
168
|
+
CREATE INDEX cve_epss ON cve(epss);
|
|
169
|
+
CREATE INDEX cve_exploit_flag ON cve(has_exploit);
|
|
170
|
+
CREATE INDEX cve_nuclei_flag ON cve(has_nuclei);
|
|
171
|
+
CREATE INDEX cve_msf_flag ON cve(has_msf);
|
|
172
|
+
CREATE INDEX cve_cwe_cwe ON cve_cwe(cwe);
|
|
173
|
+
CREATE INDEX cve_cwe_id ON cve_cwe(cve_id);
|
|
174
|
+
CREATE INDEX prod_product ON cve_product(product);
|
|
175
|
+
CREATE INDEX prod_vendor ON cve_product(vendor);
|
|
176
|
+
CREATE INDEX prod_id ON cve_product(cve_id);
|
|
177
|
+
CREATE INDEX cpe_product ON cpe_match(product);
|
|
178
|
+
CREATE INDEX cpe_vendor ON cpe_match(vendor);
|
|
179
|
+
CREATE INDEX cpe_id ON cpe_match(cve_id);
|
|
180
|
+
CREATE INDEX ref_id ON cve_ref(cve_id);
|
|
181
|
+
CREATE INDEX exp_date ON exploit(date);
|
|
182
|
+
CREATE INDEX exp_platform ON exploit(platform);
|
|
183
|
+
CREATE INDEX nuc_cve ON nuclei(cve);
|
|
184
|
+
CREATE INDEX nuc_sev ON nuclei(severity);
|
|
185
|
+
CREATE INDEX nuc_domain ON nuclei(domain);
|
|
186
|
+
CREATE INDEX kev_date ON kev(date_added);
|
|
187
|
+
CREATE INDEX cve_nuclei_id ON cve_nuclei(cve_id);
|
|
188
|
+
CREATE INDEX cve_msf_id ON cve_msf(cve_id);
|
|
189
|
+
CREATE INDEX ssvc_expl ON ssvc(exploitation);
|
|
190
|
+
CREATE INDEX adv_cve ON advisory(cve);
|
|
191
|
+
CREATE INDEX adv_pkg ON advisory(package);
|
|
192
|
+
CREATE INDEX adv_eco ON advisory(ecosystem);
|
|
193
|
+
CREATE INDEX pkg_pkg ON package_range(package);
|
|
194
|
+
CREATE INDEX pkg_adv ON package_range(advisory_id);
|
|
195
|
+
CREATE INDEX debian_pkg ON debian(package);
|
|
196
|
+
CREATE INDEX debian_cve ON debian(cve);
|
|
197
|
+
CREATE INDEX os_cve ON os_advisory(cve);
|
|
198
|
+
CREATE INDEX os_plat ON os_advisory(platform);
|
|
199
|
+
CREATE INDEX cve_cna_assigner ON cve_cna(assigner);
|
|
200
|
+
CREATE INDEX cve_meta_pubyear ON cve_meta(published_year);
|
|
201
|
+
CREATE INDEX cve_meta_cveyear ON cve_meta(cve_year);
|
|
202
|
+
CREATE INDEX cve_meta_poc ON cve_meta(has_poc);
|
|
203
|
+
CREATE INDEX poc_cve ON poc(cve_id);
|
|
204
|
+
CREATE INDEX poc_stars ON poc(stars);
|
|
205
|
+
CREATE INDEX poc_orphan_id ON poc_orphan(cve_id);
|
|
206
|
+
CREATE INDEX cve_alias_alias ON cve_alias(alias);
|
|
207
|
+
CREATE INDEX source_state_status ON source_state(status);
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
BATCH = 800
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def log(msg: str) -> None:
|
|
214
|
+
print(f"[index] {msg}", flush=True)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _norm(s: str | None, limit: int = 6000) -> str:
|
|
218
|
+
if not s:
|
|
219
|
+
return ""
|
|
220
|
+
return re.sub(r"\s+", " ", s).strip()[:limit]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------- NVD
|
|
224
|
+
def cpe_rows(cve: dict, cap: int = 400) -> list[tuple]:
|
|
225
|
+
seen: set[tuple] = set()
|
|
226
|
+
out: list[tuple] = []
|
|
227
|
+
for conf in cve.get("configurations") or []:
|
|
228
|
+
for node in conf.get("nodes") or []:
|
|
229
|
+
for m in node.get("cpeMatch") or []:
|
|
230
|
+
if m.get("vulnerable") is False:
|
|
231
|
+
continue
|
|
232
|
+
parts = (m.get("criteria") or "").split(":")
|
|
233
|
+
if len(parts) < 6:
|
|
234
|
+
continue
|
|
235
|
+
vendor, product = parts[3].lower(), parts[4].lower()
|
|
236
|
+
if vendor in ("*", "-") or product in ("*", "-"):
|
|
237
|
+
continue
|
|
238
|
+
row = (parts[2], vendor, product, parts[5],
|
|
239
|
+
m.get("versionStartIncluding"), m.get("versionStartExcluding"),
|
|
240
|
+
m.get("versionEndIncluding"), m.get("versionEndExcluding"))
|
|
241
|
+
if row in seen:
|
|
242
|
+
continue
|
|
243
|
+
seen.add(row)
|
|
244
|
+
out.append(row)
|
|
245
|
+
if len(out) >= cap:
|
|
246
|
+
return out
|
|
247
|
+
return out
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def cvss_of(metrics: dict) -> tuple[float | None, str, str, str]:
|
|
251
|
+
for key in ("cvssMetricV40", "cvssMetricV31", "cvssMetricV30", "cvssMetricV2"):
|
|
252
|
+
for e in metrics.get(key) or []:
|
|
253
|
+
data = e.get("cvssData") or {}
|
|
254
|
+
score = data.get("baseScore")
|
|
255
|
+
if score is None:
|
|
256
|
+
continue
|
|
257
|
+
try:
|
|
258
|
+
score = float(score)
|
|
259
|
+
except (TypeError, ValueError):
|
|
260
|
+
continue # one malformed metric must not abort the whole feed
|
|
261
|
+
sev = data.get("baseSeverity") or e.get("baseSeverity") or ""
|
|
262
|
+
return (score, data.get("version") or key[-3:],
|
|
263
|
+
data.get("vectorString") or "", sev.upper())
|
|
264
|
+
return (None, "", "", "")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def parse_cve(item: dict, epss: dict, kev_ids: set[str]) -> dict:
|
|
268
|
+
c = item.get("cve") or {}
|
|
269
|
+
cid = c.get("id")
|
|
270
|
+
if not cid:
|
|
271
|
+
return {}
|
|
272
|
+
desc = ""
|
|
273
|
+
for d in c.get("descriptions") or []:
|
|
274
|
+
if d.get("lang") == "en":
|
|
275
|
+
desc = d.get("value") or ""
|
|
276
|
+
break
|
|
277
|
+
score, ver, vec, sev = cvss_of(c.get("metrics") or {})
|
|
278
|
+
e, pct = epss.get(cid, (None, None))
|
|
279
|
+
cwes, seen = [], set()
|
|
280
|
+
for w in c.get("weaknesses") or []:
|
|
281
|
+
for d in w.get("description") or []:
|
|
282
|
+
v = (d.get("value") or "").upper()
|
|
283
|
+
if v.startswith("CWE-") and v not in seen:
|
|
284
|
+
seen.add(v)
|
|
285
|
+
cwes.append(v)
|
|
286
|
+
cpes = cpe_rows(c)
|
|
287
|
+
prods = []
|
|
288
|
+
seen_p = set()
|
|
289
|
+
for _, vendor, product, *_ in cpes:
|
|
290
|
+
if (vendor, product) not in seen_p and len(prods) < 120:
|
|
291
|
+
seen_p.add((vendor, product))
|
|
292
|
+
prods.append((vendor, product))
|
|
293
|
+
refs = []
|
|
294
|
+
seen_r = set()
|
|
295
|
+
for r in (c.get("references") or [])[:60]:
|
|
296
|
+
u = r.get("url")
|
|
297
|
+
if u and u not in seen_r:
|
|
298
|
+
seen_r.add(u)
|
|
299
|
+
refs.append((u, ",".join(r.get("tags") or [])))
|
|
300
|
+
pub = c.get("published") or ""
|
|
301
|
+
return {
|
|
302
|
+
"row": (cid, pub, c.get("lastModified"), c.get("vulnStatus"), _norm(desc),
|
|
303
|
+
score, ver, vec, sev, 1 if cid in kev_ids else 0, e, pct,
|
|
304
|
+
int(pub[:4]) if pub[:4].isdigit() else None),
|
|
305
|
+
"cwes": [(cid, w) for w in cwes],
|
|
306
|
+
"prods": [(cid, v, p) for v, p in prods],
|
|
307
|
+
"cpes": [(cid, *r) for r in cpes],
|
|
308
|
+
"refs": [(cid, u, t) for u, t in refs],
|
|
309
|
+
"fts": (cid, _norm(desc, 4000), " ".join(f"{v} {p}" for v, p in prods)[:4000]),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def delete_cves(con: sqlite3.Connection, ids: list[str]) -> None:
|
|
314
|
+
for i in range(0, len(ids), BATCH):
|
|
315
|
+
chunk = ids[i:i + BATCH]
|
|
316
|
+
q = ",".join("?" * len(chunk))
|
|
317
|
+
for table in ("cve_cwe", "cve_product", "cpe_match", "cve_ref",
|
|
318
|
+
"cve_exploit", "cve_nuclei", "cve_msf", "cve_fts", "cve"):
|
|
319
|
+
con.execute(f"DELETE FROM {table} WHERE {'id' if table == 'cve' else 'cve_id' if table != 'cve_fts' else 'id'} IN ({q})", chunk)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def load_nvd(path: pathlib.Path, con: sqlite3.Connection, epss: dict,
|
|
323
|
+
kev_ids: set[str], replace: bool = False, progress=None) -> int:
|
|
324
|
+
if not path.exists():
|
|
325
|
+
return 0
|
|
326
|
+
with gzip.open(path, "rt", encoding="utf-8") as fh:
|
|
327
|
+
doc = json.load(fh)
|
|
328
|
+
items = doc.get("vulnerabilities", [])
|
|
329
|
+
if replace:
|
|
330
|
+
ids = [(i.get("cve") or {}).get("id") for i in items]
|
|
331
|
+
delete_cves(con, [i for i in ids if i])
|
|
332
|
+
|
|
333
|
+
rows, cwes, prods, cpes, refs, fts = [], [], [], [], [], []
|
|
334
|
+
for item in items:
|
|
335
|
+
p = parse_cve(item, epss, kev_ids)
|
|
336
|
+
if not p:
|
|
337
|
+
continue
|
|
338
|
+
rows.append(p["row"])
|
|
339
|
+
cwes += p["cwes"]
|
|
340
|
+
prods += p["prods"]
|
|
341
|
+
cpes += p["cpes"]
|
|
342
|
+
refs += p["refs"]
|
|
343
|
+
fts.append(p["fts"])
|
|
344
|
+
if len(rows) >= BATCH:
|
|
345
|
+
done = len(fts)
|
|
346
|
+
_flush(con, rows, cwes, prods, cpes, refs, fts)
|
|
347
|
+
rows, cwes, prods, cpes, refs, fts = [], [], [], [], [], []
|
|
348
|
+
if progress:
|
|
349
|
+
progress(f"{done} rows")
|
|
350
|
+
_flush(con, rows, cwes, prods, cpes, refs, fts)
|
|
351
|
+
con.commit()
|
|
352
|
+
return len(items)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _flush(con, rows, cwes, prods, cpes, refs, fts) -> None:
|
|
356
|
+
if rows:
|
|
357
|
+
con.executemany(
|
|
358
|
+
"INSERT OR REPLACE INTO cve(id,published,modified,status,description,cvss,"
|
|
359
|
+
"cvss_version,cvss_vector,severity,kev,epss,epss_pct,year) "
|
|
360
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", rows)
|
|
361
|
+
if cwes:
|
|
362
|
+
con.executemany("INSERT OR IGNORE INTO cve_cwe VALUES (?,?)", cwes)
|
|
363
|
+
if prods:
|
|
364
|
+
con.executemany("INSERT OR IGNORE INTO cve_product VALUES (?,?,?)", prods)
|
|
365
|
+
if cpes:
|
|
366
|
+
con.executemany("INSERT OR IGNORE INTO cpe_match VALUES (?,?,?,?,?,?,?,?,?)", cpes)
|
|
367
|
+
if refs:
|
|
368
|
+
con.executemany("INSERT OR IGNORE INTO cve_ref VALUES (?,?,?)", refs)
|
|
369
|
+
if fts:
|
|
370
|
+
con.executemany("INSERT INTO cve_fts(id, description, products) VALUES (?,?,?)", fts)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ---------------------------------------------------------------- intel
|
|
374
|
+
def load_kev(path: pathlib.Path, con: sqlite3.Connection, epss: dict | None = None) -> int:
|
|
375
|
+
if not path.exists():
|
|
376
|
+
return 0
|
|
377
|
+
doc = json.loads(path.read_text())
|
|
378
|
+
# indexes built before KEV carried EPSS: add the columns in place, no rebuild
|
|
379
|
+
have = {r[1] for r in con.execute("PRAGMA table_info(kev)")}
|
|
380
|
+
for col in ("epss", "epss_pct"):
|
|
381
|
+
if col not in have:
|
|
382
|
+
con.execute(f"ALTER TABLE kev ADD COLUMN {col} REAL")
|
|
383
|
+
epss = epss or {}
|
|
384
|
+
con.execute("DELETE FROM kev")
|
|
385
|
+
rows = [(v.get("cveID"), v.get("vendorProject"), v.get("product"),
|
|
386
|
+
v.get("vulnerabilityName"), v.get("dateAdded"),
|
|
387
|
+
v.get("knownRansomwareCampaignUse"), v.get("requiredAction"),
|
|
388
|
+
v.get("dueDate"), _norm(v.get("notes"), 1000),
|
|
389
|
+
*epss.get(v.get("cveID"), (None, None)))
|
|
390
|
+
for v in doc.get("vulnerabilities", [])]
|
|
391
|
+
con.executemany("INSERT OR REPLACE INTO kev(cve,vendor,product,name,date_added,ransomware,"
|
|
392
|
+
"action,due,notes,epss,epss_pct) VALUES (?,?,?,?,?,?,?,?,?,?,?)", rows)
|
|
393
|
+
con.execute("UPDATE cve SET kev=0 WHERE kev=1 AND id NOT IN (SELECT cve FROM kev)")
|
|
394
|
+
con.execute("UPDATE cve SET kev=1 WHERE id IN (SELECT cve FROM kev)")
|
|
395
|
+
con.commit()
|
|
396
|
+
return len(rows)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def read_epss(path: pathlib.Path) -> dict[str, tuple[float, float]]:
|
|
400
|
+
out: dict[str, tuple[float, float]] = {}
|
|
401
|
+
if not path.exists():
|
|
402
|
+
return out
|
|
403
|
+
opener = gzip.open if path.suffix == ".gz" else open
|
|
404
|
+
with opener(path, "rt", errors="replace") as fh:
|
|
405
|
+
for row in csv.reader(fh):
|
|
406
|
+
if not row or row[0].startswith("#") or row[0].lower() == "cve":
|
|
407
|
+
continue
|
|
408
|
+
if len(row) >= 3:
|
|
409
|
+
try:
|
|
410
|
+
out[row[0].upper()] = (float(row[1]), float(row[2]))
|
|
411
|
+
except ValueError:
|
|
412
|
+
continue
|
|
413
|
+
return out
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def load_epss(path: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
417
|
+
rows = read_epss(path)
|
|
418
|
+
if not rows:
|
|
419
|
+
return 0
|
|
420
|
+
con.execute("CREATE TEMP TABLE IF NOT EXISTS _epss_tmp(cve TEXT PRIMARY KEY, epss REAL, pct REAL)")
|
|
421
|
+
con.execute("DELETE FROM _epss_tmp")
|
|
422
|
+
con.executemany("INSERT OR REPLACE INTO _epss_tmp VALUES (?,?,?)",
|
|
423
|
+
[(k, v[0], v[1]) for k, v in rows.items()])
|
|
424
|
+
con.execute("UPDATE cve AS c SET epss=(SELECT epss FROM _epss_tmp WHERE cve=c.id),"
|
|
425
|
+
" epss_pct=(SELECT pct FROM _epss_tmp WHERE cve=c.id)"
|
|
426
|
+
" WHERE id IN (SELECT cve FROM _epss_tmp)")
|
|
427
|
+
con.execute("DROP TABLE _epss_tmp")
|
|
428
|
+
con.commit()
|
|
429
|
+
return len(rows)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def load_exploitdb(path: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
433
|
+
if not path.exists():
|
|
434
|
+
return 0
|
|
435
|
+
con.execute("DELETE FROM exploit")
|
|
436
|
+
con.execute("DELETE FROM exploit_fts")
|
|
437
|
+
rows, fts = [], []
|
|
438
|
+
with path.open(encoding="utf-8", errors="replace", newline="") as fh:
|
|
439
|
+
for r in csv.DictReader(fh):
|
|
440
|
+
rid = (r.get("id") or "").strip()
|
|
441
|
+
if not rid:
|
|
442
|
+
continue
|
|
443
|
+
desc = _norm(r.get("description"), 500)
|
|
444
|
+
tags = (r.get("tags") or "").strip()
|
|
445
|
+
codes = (r.get("codes") or "").strip()
|
|
446
|
+
file = (r.get("file") or "").strip()
|
|
447
|
+
rows.append((rid, file, desc, r.get("date_published"), r.get("author"),
|
|
448
|
+
r.get("type"), r.get("platform"),
|
|
449
|
+
1 if (r.get("verified") or "0").strip() in ("1", "true", "True") else 0,
|
|
450
|
+
codes, tags, f"https://www.exploit-db.com/exploits/{rid}",
|
|
451
|
+
f"https://gitlab.com/exploit-database/exploitdb/-/raw/main/{file}"))
|
|
452
|
+
fts.append((rid, desc, tags, codes))
|
|
453
|
+
# Exploit-DB occasionally lists the same id twice; keep one row and one FTS row.
|
|
454
|
+
seen, urows, ufts = set(), [], []
|
|
455
|
+
for row, frow in zip(rows, fts):
|
|
456
|
+
if row[0] in seen:
|
|
457
|
+
continue
|
|
458
|
+
seen.add(row[0])
|
|
459
|
+
urows.append(row)
|
|
460
|
+
ufts.append(frow)
|
|
461
|
+
con.executemany("INSERT OR REPLACE INTO exploit VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", urows)
|
|
462
|
+
con.executemany("INSERT INTO exploit_fts(id, description, tags, codes) VALUES (?,?,?,?)", ufts)
|
|
463
|
+
con.commit()
|
|
464
|
+
return len(rows)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def load_shellcodes(path: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
468
|
+
if not path.exists():
|
|
469
|
+
return 0
|
|
470
|
+
con.execute("DELETE FROM shellcode")
|
|
471
|
+
rows = []
|
|
472
|
+
with path.open(encoding="utf-8", errors="replace", newline="") as fh:
|
|
473
|
+
for r in csv.DictReader(fh):
|
|
474
|
+
rid = (r.get("id") or "").strip()
|
|
475
|
+
if rid:
|
|
476
|
+
rows.append((rid, r.get("file"), _norm(r.get("description"), 300),
|
|
477
|
+
r.get("date_published"), r.get("platform"),
|
|
478
|
+
f"https://www.exploit-db.com/shellcodes/{rid}"))
|
|
479
|
+
con.executemany("INSERT OR REPLACE INTO shellcode VALUES (?,?,?,?,?,?)", rows)
|
|
480
|
+
con.commit()
|
|
481
|
+
return len(rows)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def link_exploits(con: sqlite3.Connection) -> int:
|
|
485
|
+
pairs = []
|
|
486
|
+
for rid, codes in con.execute("SELECT id, codes FROM exploit WHERE codes <> ''"):
|
|
487
|
+
for cid in set(x.upper() for x in CVE_RE.findall(codes)):
|
|
488
|
+
pairs.append((cid, rid))
|
|
489
|
+
con.execute("DELETE FROM cve_exploit")
|
|
490
|
+
con.executemany("INSERT OR IGNORE INTO cve_exploit VALUES (?,?)", pairs)
|
|
491
|
+
con.execute("UPDATE cve SET has_exploit=0 WHERE has_exploit=1")
|
|
492
|
+
con.execute("UPDATE cve SET has_exploit=1 WHERE id IN (SELECT cve_id FROM cve_exploit)")
|
|
493
|
+
con.commit()
|
|
494
|
+
return len(pairs)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
NUCLEI_SKIP = {"workflows", "helpers", "profiles", "templates", ".github", "docs"}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _block_value(lines: list[str], start: int, indent: int) -> str:
|
|
501
|
+
out = []
|
|
502
|
+
for ln in lines[start + 1:]:
|
|
503
|
+
if not ln.strip():
|
|
504
|
+
continue
|
|
505
|
+
if len(ln) - len(ln.lstrip()) <= indent:
|
|
506
|
+
break
|
|
507
|
+
out.append(ln.strip())
|
|
508
|
+
return " ".join(out)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def parse_nuclei(text: str) -> dict:
|
|
512
|
+
info = {"id": "", "name": "", "severity": "", "tags": "", "cve": "", "description": ""}
|
|
513
|
+
in_info, in_class = False, False
|
|
514
|
+
for i, ln in enumerate(text.splitlines()[:400]):
|
|
515
|
+
s = ln.strip()
|
|
516
|
+
ind = len(ln) - len(ln.lstrip())
|
|
517
|
+
if ind == 0 and s.startswith("id:"):
|
|
518
|
+
info["id"] = s.split(":", 1)[1].strip().strip("'\"")
|
|
519
|
+
elif ind == 0 and s.startswith("info:"):
|
|
520
|
+
in_info = True
|
|
521
|
+
continue
|
|
522
|
+
elif ind == 0 and s and not s.startswith("#"):
|
|
523
|
+
in_info = False
|
|
524
|
+
if not in_info:
|
|
525
|
+
continue
|
|
526
|
+
if s.startswith("name:") and not info["name"]:
|
|
527
|
+
info["name"] = s.split(":", 1)[1].strip().strip("'\"")
|
|
528
|
+
elif s.startswith("severity:") and not info["severity"]:
|
|
529
|
+
info["severity"] = s.split(":", 1)[1].strip().strip("'\"")
|
|
530
|
+
elif s.startswith("tags:") and not info["tags"]:
|
|
531
|
+
info["tags"] = s.split(":", 1)[1].strip().strip("'\"")
|
|
532
|
+
elif s.startswith("description:") and not info["description"]:
|
|
533
|
+
rest = s.split(":", 1)[1].strip()
|
|
534
|
+
info["description"] = _norm(rest if rest not in ("|", ">", "|-", ">-")
|
|
535
|
+
else _block_value(text.splitlines(), i, ind), 800)
|
|
536
|
+
elif s.startswith("classification:"):
|
|
537
|
+
in_class = True
|
|
538
|
+
elif in_class and s.startswith("cve-id:"):
|
|
539
|
+
rest = s.split(":", 1)[1].strip()
|
|
540
|
+
if rest.startswith("["):
|
|
541
|
+
info["cve"] = ",".join(CVE_RE.findall(rest.upper()))
|
|
542
|
+
elif rest:
|
|
543
|
+
info["cve"] = rest.strip("'\"").upper()
|
|
544
|
+
else:
|
|
545
|
+
got = []
|
|
546
|
+
for nxt in text.splitlines()[i + 1:i + 12]:
|
|
547
|
+
m = re.match(r"\s*-\s*(CVE-\d{4}-\d+)", nxt, re.I)
|
|
548
|
+
if m:
|
|
549
|
+
got.append(m.group(1).upper())
|
|
550
|
+
elif nxt.strip() and not nxt.strip().startswith("-"):
|
|
551
|
+
break
|
|
552
|
+
info["cve"] = ",".join(got)
|
|
553
|
+
return info
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def load_nuclei(root: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
557
|
+
if not root.exists():
|
|
558
|
+
return 0
|
|
559
|
+
con.execute("DELETE FROM nuclei")
|
|
560
|
+
con.execute("DELETE FROM nuclei_fts")
|
|
561
|
+
rows, fts = [], []
|
|
562
|
+
for p in root.rglob("*.y*ml"):
|
|
563
|
+
rel = p.relative_to(root)
|
|
564
|
+
if not rel.parts or rel.parts[0] in NUCLEI_SKIP:
|
|
565
|
+
continue
|
|
566
|
+
if p.stat().st_size > 400_000:
|
|
567
|
+
continue
|
|
568
|
+
try:
|
|
569
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
570
|
+
except OSError:
|
|
571
|
+
continue
|
|
572
|
+
if "id:" not in text[:2000]:
|
|
573
|
+
continue
|
|
574
|
+
info = parse_nuclei(text)
|
|
575
|
+
if not info["id"] or not info["name"]:
|
|
576
|
+
continue
|
|
577
|
+
domain = rel.parts[1 if len(rel.parts) > 2 else 0]
|
|
578
|
+
rows.append((info["id"], info["name"], info["severity"].lower(), info["tags"],
|
|
579
|
+
info["cve"], str(rel), info["description"], domain))
|
|
580
|
+
fts.append((info["id"], info["name"], info["tags"], info["cve"]))
|
|
581
|
+
con.executemany("INSERT OR REPLACE INTO nuclei VALUES (?,?,?,?,?,?,?,?)", rows)
|
|
582
|
+
con.executemany("INSERT INTO nuclei_fts(id, name, tags, cve) VALUES (?,?,?,?)", fts)
|
|
583
|
+
pair = []
|
|
584
|
+
for tid, cves in con.execute("SELECT id, cve FROM nuclei WHERE cve <> ''"):
|
|
585
|
+
for cid in set(cves.upper().split(",")):
|
|
586
|
+
if CVE_RE.fullmatch(cid):
|
|
587
|
+
pair.append((cid, tid))
|
|
588
|
+
con.execute("DELETE FROM cve_nuclei")
|
|
589
|
+
con.executemany("INSERT OR IGNORE INTO cve_nuclei VALUES (?,?)", pair)
|
|
590
|
+
con.execute("UPDATE cve SET has_nuclei=0 WHERE has_nuclei=1")
|
|
591
|
+
con.execute("UPDATE cve SET has_nuclei=1 WHERE id IN (SELECT cve_id FROM cve_nuclei)")
|
|
592
|
+
con.commit()
|
|
593
|
+
return len(rows)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def load_msf(tree: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
597
|
+
if not tree.exists():
|
|
598
|
+
return 0
|
|
599
|
+
con.execute("DELETE FROM msf")
|
|
600
|
+
rows = []
|
|
601
|
+
for ent in json.loads(tree.read_text()).get("tree", []):
|
|
602
|
+
p = ent.get("path") or ""
|
|
603
|
+
if not p.startswith("modules/") or not p.endswith(".rb"):
|
|
604
|
+
continue
|
|
605
|
+
parts = p.split("/")
|
|
606
|
+
if len(parts) < 3:
|
|
607
|
+
continue
|
|
608
|
+
cves = ",".join(sorted({c.upper() for c in CVE_RE.findall(p)}))
|
|
609
|
+
rows.append((p, parts[1], "/".join(parts[2:]).removesuffix(".rb"),
|
|
610
|
+
"", 0, "", "", cves))
|
|
611
|
+
con.executemany("INSERT OR REPLACE INTO msf(path,type,name,platform,privileged,"
|
|
612
|
+
"disclosure_date,description,cves) VALUES (?,?,?,?,?,?,?,?)", rows)
|
|
613
|
+
pairs = []
|
|
614
|
+
for p, name in con.execute("SELECT path, name FROM msf"):
|
|
615
|
+
for cid in set(x.upper() for x in CVE_RE.findall(f"{p} {name}")):
|
|
616
|
+
pairs.append((cid, p))
|
|
617
|
+
con.execute("DELETE FROM cve_msf")
|
|
618
|
+
con.executemany("INSERT OR IGNORE INTO cve_msf VALUES (?,?)", pairs)
|
|
619
|
+
con.execute("UPDATE cve SET has_msf=0 WHERE has_msf=1")
|
|
620
|
+
con.execute("UPDATE cve SET has_msf=1 WHERE id IN (SELECT cve_id FROM cve_msf)")
|
|
621
|
+
con.commit()
|
|
622
|
+
return len(rows)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def load_attack(path: pathlib.Path, domain: str, con: sqlite3.Connection) -> int:
|
|
626
|
+
if not path.exists():
|
|
627
|
+
return 0
|
|
628
|
+
rows = []
|
|
629
|
+
for obj in json.loads(path.read_text()).get("objects", []):
|
|
630
|
+
if obj.get("type") != "attack-pattern" or obj.get("revoked") or obj.get("x_mitre_deprecated"):
|
|
631
|
+
continue
|
|
632
|
+
ext = ""
|
|
633
|
+
for ref in obj.get("external_references") or []:
|
|
634
|
+
if ref.get("source_name") == "mitre-attack":
|
|
635
|
+
ext = ref.get("external_id") or ""
|
|
636
|
+
if not ext:
|
|
637
|
+
continue
|
|
638
|
+
tactics = ",".join(p.get("phase_name", "") for p in obj.get("kill_chain_phases") or [])
|
|
639
|
+
rows.append((ext, domain, obj.get("name"), tactics,
|
|
640
|
+
",".join(obj.get("x_mitre_platforms") or []),
|
|
641
|
+
_norm(obj.get("description"), 3000),
|
|
642
|
+
f"https://attack.mitre.org/techniques/{ext.replace('.', '/')}/"))
|
|
643
|
+
con.execute("DELETE FROM attack WHERE domain=?", (domain,))
|
|
644
|
+
con.executemany("INSERT OR REPLACE INTO attack VALUES (?,?,?,?,?,?,?)", rows)
|
|
645
|
+
con.commit()
|
|
646
|
+
return len(rows)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _text(el) -> str:
|
|
650
|
+
return _norm(" ".join("".join(el.itertext()).split()), 4000)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def load_cwe(path: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
654
|
+
if not path.exists():
|
|
655
|
+
return 0
|
|
656
|
+
zf = zipfile.ZipFile(path)
|
|
657
|
+
xml = zf.read(next(n for n in zf.namelist() if n.endswith(".xml")))
|
|
658
|
+
root = ET.fromstring(xml)
|
|
659
|
+
rows = []
|
|
660
|
+
for el in root.iter():
|
|
661
|
+
if not el.tag.endswith("Weakness") or not el.get("ID"):
|
|
662
|
+
continue
|
|
663
|
+
desc = next((_text(c) for c in el if c.tag.endswith("Description")), "")
|
|
664
|
+
rows.append(("CWE-" + el.attrib["ID"], el.get("Name"), el.get("Abstraction"),
|
|
665
|
+
el.get("Status"), desc))
|
|
666
|
+
con.execute("DELETE FROM cwe")
|
|
667
|
+
con.executemany("INSERT OR REPLACE INTO cwe VALUES (?,?,?,?,?)", rows)
|
|
668
|
+
con.commit()
|
|
669
|
+
return len(rows)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def load_capec(path: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
673
|
+
if not path.exists():
|
|
674
|
+
return 0
|
|
675
|
+
root = ET.parse(path).getroot()
|
|
676
|
+
rows = []
|
|
677
|
+
for el in root.iter():
|
|
678
|
+
if not el.tag.endswith("Attack_Pattern") or not el.get("ID"):
|
|
679
|
+
continue
|
|
680
|
+
desc, cwes = "", []
|
|
681
|
+
for child in el:
|
|
682
|
+
tag = child.tag.split("}")[-1]
|
|
683
|
+
if tag == "Description":
|
|
684
|
+
desc = _text(child)
|
|
685
|
+
elif tag == "Related_Weaknesses":
|
|
686
|
+
cwes += ["CWE-" + w.attrib["CWE_ID"] for w in child if w.get("CWE_ID")]
|
|
687
|
+
rows.append(("CAPEC-" + el.attrib["ID"], el.get("Name"), el.get("Severity"),
|
|
688
|
+
el.get("Likelihood_Of_Attack"), ",".join(sorted(set(cwes))), desc))
|
|
689
|
+
con.execute("DELETE FROM capec")
|
|
690
|
+
con.executemany("INSERT OR REPLACE INTO capec VALUES (?,?,?,?,?,?)", rows)
|
|
691
|
+
con.commit()
|
|
692
|
+
return len(rows)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def load_models(path: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
696
|
+
if not path.exists():
|
|
697
|
+
return 0
|
|
698
|
+
doc = json.loads(path.read_text())
|
|
699
|
+
rows, fts = [], []
|
|
700
|
+
for pid, prov in doc.items():
|
|
701
|
+
pname = prov.get("name") or pid
|
|
702
|
+
for mid, m in (prov.get("models") or {}).items():
|
|
703
|
+
cost, limit = m.get("cost") or {}, m.get("limit") or {}
|
|
704
|
+
modality = m.get("modalities") or {}
|
|
705
|
+
rows.append((pid, pname, mid, m.get("name") or mid,
|
|
706
|
+
limit.get("context"), limit.get("output"),
|
|
707
|
+
cost.get("input"), cost.get("output"), str(m.get("release_date") or ""),
|
|
708
|
+
1 if m.get("reasoning") else 0, 1 if m.get("tool_call") else 0,
|
|
709
|
+
1 if "image" in (modality.get("input") or []) else 0))
|
|
710
|
+
fts.append((pid, mid, f"{m.get('name') or mid} {pid} {pname}"))
|
|
711
|
+
con.execute("DELETE FROM model")
|
|
712
|
+
con.execute("DELETE FROM model_fts")
|
|
713
|
+
con.executemany("INSERT OR REPLACE INTO model VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", rows)
|
|
714
|
+
con.executemany("INSERT INTO model_fts(provider, id, name) VALUES (?,?,?)", fts)
|
|
715
|
+
con.commit()
|
|
716
|
+
return len(rows)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def load_ssvc(root: pathlib.Path, con: sqlite3.Connection) -> int:
|
|
720
|
+
"""CISA Vulnrichment: SSVC decision points (Exploitation/Automatable/Technical Impact)."""
|
|
721
|
+
if not root.exists():
|
|
722
|
+
return 0
|
|
723
|
+
rows = []
|
|
724
|
+
for p in root.rglob("CVE-*.json"):
|
|
725
|
+
try:
|
|
726
|
+
doc = json.loads(p.read_text())
|
|
727
|
+
except (OSError, json.JSONDecodeError):
|
|
728
|
+
continue
|
|
729
|
+
cid = (doc.get("cveMetadata") or {}).get("cveId")
|
|
730
|
+
if not cid:
|
|
731
|
+
continue
|
|
732
|
+
for adp in (doc.get("containers") or {}).get("adp") or []:
|
|
733
|
+
for metric in adp.get("metrics") or []:
|
|
734
|
+
other = metric.get("other") or {}
|
|
735
|
+
if other.get("type") != "ssvc":
|
|
736
|
+
continue
|
|
737
|
+
content = other.get("content") or {}
|
|
738
|
+
opts = {}
|
|
739
|
+
for o in content.get("options") or []:
|
|
740
|
+
if isinstance(o, dict):
|
|
741
|
+
opts.update(o)
|
|
742
|
+
rows.append((cid, opts.get("Exploitation"), opts.get("Automatable"),
|
|
743
|
+
opts.get("Technical Impact"), content.get("timestamp"), "CISA-ADP"))
|
|
744
|
+
if rows:
|
|
745
|
+
con.execute("DELETE FROM ssvc")
|
|
746
|
+
con.executemany("INSERT OR REPLACE INTO ssvc VALUES (?,?,?,?,?,?)", rows)
|
|
747
|
+
con.commit()
|
|
748
|
+
return len(rows)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# ---------------------------------------------------------------- build/update
|
|
752
|
+
def create(path: pathlib.Path) -> sqlite3.Connection:
|
|
753
|
+
con = sqlite3.connect(path)
|
|
754
|
+
con.executescript("PRAGMA journal_mode=MEMORY; PRAGMA synchronous=OFF; PRAGMA temp_store=MEMORY;")
|
|
755
|
+
con.executescript(SCHEMA)
|
|
756
|
+
return con
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def load_bulk(con: sqlite3.Connection, progress=None, heavy: bool = True) -> dict:
|
|
760
|
+
"""Load every bulk corpus that is present on disk. Missing dirs are skipped."""
|
|
761
|
+
counts: dict[str, int] = {}
|
|
762
|
+
if heavy:
|
|
763
|
+
for z in sorted((paths.DATA / "osv").glob("*.zip")):
|
|
764
|
+
n = loaders.load_osv_zip(z, con)
|
|
765
|
+
counts[f"osv-{z.stem}"] = n
|
|
766
|
+
if progress:
|
|
767
|
+
progress(f"OSV {z.stem}: {n}")
|
|
768
|
+
|
|
769
|
+
if (paths.DATA / "debian/tracker.json").exists():
|
|
770
|
+
counts["debian"] = loaders.load_debian(paths.DATA / "debian/tracker.json", con)
|
|
771
|
+
if progress:
|
|
772
|
+
progress(f"Debian {counts['debian']}")
|
|
773
|
+
|
|
774
|
+
if (paths.DATA / "msrc/sug.json").exists():
|
|
775
|
+
counts["msrc"] = loaders.load_msrc(paths.DATA / "msrc/sug.json", con)
|
|
776
|
+
if progress:
|
|
777
|
+
progress(f"MSRC {counts['msrc']}")
|
|
778
|
+
|
|
779
|
+
if (paths.DATA / "apple").exists():
|
|
780
|
+
counts["apple"] = loaders.load_apple(paths.DATA / "apple", con)
|
|
781
|
+
if progress:
|
|
782
|
+
progress(f"Apple {counts['apple']}")
|
|
783
|
+
|
|
784
|
+
for src in ("gtfobins", "lolbas", "payloads"):
|
|
785
|
+
root = paths.DATA / "toolkit" / src
|
|
786
|
+
if root.exists():
|
|
787
|
+
counts[src] = loaders.load_toolkit(root, src, con)
|
|
788
|
+
if progress:
|
|
789
|
+
progress(f"{src} {counts[src]}")
|
|
790
|
+
|
|
791
|
+
src = paths.DATA / "metasploit/source"
|
|
792
|
+
if (src / "modules").exists():
|
|
793
|
+
counts["msf-source"] = loaders.load_msf_source(src, con)
|
|
794
|
+
if progress:
|
|
795
|
+
progress(f"Metasploit source {counts['msf-source']}")
|
|
796
|
+
|
|
797
|
+
if (paths.DATA / "exploitdb/full/exploits").exists() and heavy:
|
|
798
|
+
counts["exploit-code"] = loaders.load_exploit_code(paths.DATA / "exploitdb/full", con)
|
|
799
|
+
if progress:
|
|
800
|
+
progress(f"Exploit-DB code {counts['exploit-code']} files")
|
|
801
|
+
return counts
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def build(quick: bool = False, with_extra: bool = False,
|
|
805
|
+
with_bulk: bool = False, progress=None) -> dict:
|
|
806
|
+
t0 = time.time()
|
|
807
|
+
paths.ensure_dirs()
|
|
808
|
+
tmp = paths.DB.with_suffix(".db.tmp")
|
|
809
|
+
if tmp.exists():
|
|
810
|
+
tmp.unlink()
|
|
811
|
+
con = create(tmp)
|
|
812
|
+
|
|
813
|
+
epss = read_epss(paths.DATA / "epss/epss_scores-current.csv.gz")
|
|
814
|
+
if progress:
|
|
815
|
+
progress(f"EPSS {len(epss)}")
|
|
816
|
+
kev_path = paths.DATA / "kev/known_exploited_vulnerabilities.json"
|
|
817
|
+
n = load_kev(kev_path, con, epss)
|
|
818
|
+
if progress:
|
|
819
|
+
progress(f"KEV {n}")
|
|
820
|
+
kev_ids = {r[0] for r in con.execute("SELECT cve FROM kev")}
|
|
821
|
+
|
|
822
|
+
total = 0
|
|
823
|
+
for y in paths.YEARS:
|
|
824
|
+
p = paths.DATA / f"nvd/nvdcve-2.0-{y}.json.gz"
|
|
825
|
+
if not p.exists():
|
|
826
|
+
continue
|
|
827
|
+
n = load_nvd(p, con, epss, kev_ids)
|
|
828
|
+
total += n
|
|
829
|
+
if progress:
|
|
830
|
+
progress(f"NVD {y}: {n} ({total})")
|
|
831
|
+
|
|
832
|
+
intel.load_all(con, epss=epss, kev_ids=kev_ids, progress=progress)
|
|
833
|
+
|
|
834
|
+
n = load_exploitdb(paths.DATA / "exploitdb/files_exploits.csv", con)
|
|
835
|
+
load_shellcodes(paths.DATA / "exploitdb/files_shellcodes.csv", con)
|
|
836
|
+
if progress:
|
|
837
|
+
progress(f"Exploit-DB {n}")
|
|
838
|
+
if progress:
|
|
839
|
+
progress(f"exploit links {link_exploits(con)}")
|
|
840
|
+
|
|
841
|
+
if not quick:
|
|
842
|
+
n = load_nuclei(paths.DATA / "nuclei/templates", con)
|
|
843
|
+
if progress:
|
|
844
|
+
progress(f"Nuclei {n}")
|
|
845
|
+
if not (paths.DATA / "metasploit/source/modules").exists():
|
|
846
|
+
n = load_msf(paths.DATA / "metasploit/tree.json", con)
|
|
847
|
+
if progress:
|
|
848
|
+
progress(f"Metasploit {n}")
|
|
849
|
+
load_attack(paths.DATA / "attack/enterprise-attack.json", "enterprise", con)
|
|
850
|
+
for extra in ("mobile", "ics"):
|
|
851
|
+
load_attack(paths.DATA / f"attack/{extra}-attack.json", extra, con)
|
|
852
|
+
if progress:
|
|
853
|
+
progress(f"ATT&CK {con.execute('SELECT count(*) FROM attack').fetchone()[0]}")
|
|
854
|
+
|
|
855
|
+
load_cwe(paths.DATA / "cwe/cwec_latest.xml.zip", con)
|
|
856
|
+
load_capec(paths.DATA / "capec/capec_latest.xml", con)
|
|
857
|
+
load_models(paths.DATA / "models/models.dev.json", con)
|
|
858
|
+
if with_extra or (paths.DATA / "vulnrichment").exists():
|
|
859
|
+
n = load_ssvc(paths.DATA / "vulnrichment", con)
|
|
860
|
+
if progress:
|
|
861
|
+
progress(f"SSVC {n}")
|
|
862
|
+
if not quick:
|
|
863
|
+
load_bulk(con, progress)
|
|
864
|
+
|
|
865
|
+
intel.rebuild_cve_meta(con)
|
|
866
|
+
intel.stub_missing_cwes(con)
|
|
867
|
+
intel.rebuild_aliases(con)
|
|
868
|
+
con.commit()
|
|
869
|
+
con.executescript(INDEXES)
|
|
870
|
+
for k, v in (("schema", str(paths.SCHEMA_VERSION)),
|
|
871
|
+
("built_at", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())),
|
|
872
|
+
("updated_at", time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))):
|
|
873
|
+
con.execute("INSERT OR REPLACE INTO meta VALUES (?,?)", (k, v))
|
|
874
|
+
con.commit()
|
|
875
|
+
con.execute("PRAGMA journal_mode=DELETE")
|
|
876
|
+
con.execute("VACUUM")
|
|
877
|
+
con.close()
|
|
878
|
+
tmp.replace(paths.DB)
|
|
879
|
+
|
|
880
|
+
stats = summarize()
|
|
881
|
+
stats["build_seconds"] = round(time.time() - t0, 1)
|
|
882
|
+
_write_stats(stats)
|
|
883
|
+
paths.save_state(schema=paths.SCHEMA_VERSION)
|
|
884
|
+
return stats
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def update(deep: bool = False, progress=None) -> dict:
|
|
888
|
+
"""Apply the NVD `modified` feed + refresh intel tables, then swap atomically.
|
|
889
|
+
|
|
890
|
+
All work happens on a copy (``crowbar.db.tmp``); only a complete run replaces the
|
|
891
|
+
live index via ``os.replace``, so a crash or kill leaves the old DB intact.
|
|
892
|
+
"""
|
|
893
|
+
t0 = time.time()
|
|
894
|
+
if not paths.DB.exists():
|
|
895
|
+
raise FileNotFoundError(f"no index to update: {paths.DB} (run: crowbar build)")
|
|
896
|
+
tmp = paths.DB.with_suffix(".db.tmp")
|
|
897
|
+
if tmp.exists():
|
|
898
|
+
tmp.unlink()
|
|
899
|
+
shutil.copy2(paths.DB, tmp)
|
|
900
|
+
con = sqlite3.connect(tmp, timeout=90)
|
|
901
|
+
con.row_factory = sqlite3.Row
|
|
902
|
+
try:
|
|
903
|
+
try:
|
|
904
|
+
schema = int(con.execute("SELECT value FROM meta WHERE key='schema'").fetchone()[0])
|
|
905
|
+
except (sqlite3.DatabaseError, TypeError, ValueError):
|
|
906
|
+
schema = 0
|
|
907
|
+
if schema < paths.SCHEMA_VERSION:
|
|
908
|
+
intel.migrate(con, progress)
|
|
909
|
+
|
|
910
|
+
epss = read_epss(paths.DATA / "epss/epss_scores-current.csv.gz")
|
|
911
|
+
if progress:
|
|
912
|
+
progress(f"EPSS {len(epss)}")
|
|
913
|
+
n = load_kev(paths.DATA / "kev/known_exploited_vulnerabilities.json", con, epss)
|
|
914
|
+
if progress:
|
|
915
|
+
progress(f"KEV {n}")
|
|
916
|
+
kev_ids = {r[0] for r in con.execute("SELECT cve FROM kev")}
|
|
917
|
+
|
|
918
|
+
changed = 0
|
|
919
|
+
for feed in ("modified", "recent"):
|
|
920
|
+
p = paths.DATA / f"nvd/nvdcve-2.0-{feed}.json.gz"
|
|
921
|
+
if p.exists():
|
|
922
|
+
changed += load_nvd(p, con, epss, kev_ids, replace=True)
|
|
923
|
+
if progress:
|
|
924
|
+
progress(f"CVE rows touched {changed}")
|
|
925
|
+
|
|
926
|
+
intel.load_all(con, epss=epss, kev_ids=kev_ids, progress=progress)
|
|
927
|
+
|
|
928
|
+
if paths.DATA.joinpath("exploitdb/files_exploits.csv").exists():
|
|
929
|
+
load_exploitdb(paths.DATA / "exploitdb/files_exploits.csv", con)
|
|
930
|
+
load_shellcodes(paths.DATA / "exploitdb/files_shellcodes.csv", con)
|
|
931
|
+
link_exploits(con)
|
|
932
|
+
if deep:
|
|
933
|
+
load_nuclei(paths.DATA / "nuclei/templates", con)
|
|
934
|
+
if not (paths.DATA / "metasploit/source/modules").exists():
|
|
935
|
+
load_msf(paths.DATA / "metasploit/tree.json", con)
|
|
936
|
+
for dom, f in (("enterprise", "enterprise"), ("mobile", "mobile"), ("ics", "ics")):
|
|
937
|
+
load_attack(paths.DATA / f"attack/{f}-attack.json", dom, con)
|
|
938
|
+
load_bulk(con, progress, heavy=False)
|
|
939
|
+
load_models(paths.DATA / "models/models.dev.json", con)
|
|
940
|
+
load_cwe(paths.DATA / "cwe/cwec_latest.xml.zip", con)
|
|
941
|
+
load_capec(paths.DATA / "capec/capec_latest.xml", con)
|
|
942
|
+
if (paths.DATA / "vulnrichment").exists():
|
|
943
|
+
load_ssvc(paths.DATA / "vulnrichment", con)
|
|
944
|
+
|
|
945
|
+
intel.rebuild_cve_meta(con)
|
|
946
|
+
intel.stub_missing_cwes(con)
|
|
947
|
+
intel.rebuild_aliases(con)
|
|
948
|
+
|
|
949
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
950
|
+
con.execute("INSERT OR REPLACE INTO meta VALUES ('updated_at', ?)", (now,))
|
|
951
|
+
con.commit()
|
|
952
|
+
stats = {"updated_at": now, "rows_touched": changed,
|
|
953
|
+
"seconds": round(time.time() - t0, 1),
|
|
954
|
+
"cve": con.execute("SELECT count(*) FROM cve").fetchone()[0]}
|
|
955
|
+
con.close()
|
|
956
|
+
except BaseException:
|
|
957
|
+
con.close()
|
|
958
|
+
tmp.unlink(missing_ok=True)
|
|
959
|
+
raise
|
|
960
|
+
tmp.replace(paths.DB)
|
|
961
|
+
|
|
962
|
+
full = summarize()
|
|
963
|
+
full.update(stats)
|
|
964
|
+
_write_stats(full)
|
|
965
|
+
return stats
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _write_stats(stats: dict) -> None:
|
|
969
|
+
import json as _json
|
|
970
|
+
(paths.INDEX / "stats.json").write_text(_json.dumps(stats, indent=2, default=str) + "\n")
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def summarize() -> dict:
|
|
974
|
+
con = sqlite3.connect(f"file:{paths.DB}?mode=ro", uri=True)
|
|
975
|
+
out = {}
|
|
976
|
+
for (t,) in con.execute("SELECT name FROM sqlite_master WHERE type='table' "
|
|
977
|
+
"AND name NOT LIKE '%_fts%' AND name NOT LIKE 'sqlite_%'"):
|
|
978
|
+
out[t] = con.execute(f"SELECT count(*) FROM {t}").fetchone()[0]
|
|
979
|
+
try:
|
|
980
|
+
out["meta"] = {k: v for k, v in con.execute("SELECT key, value FROM meta")}
|
|
981
|
+
except sqlite3.DatabaseError:
|
|
982
|
+
out["meta"] = {}
|
|
983
|
+
con.close()
|
|
984
|
+
out["db_bytes"] = paths.DB.stat().st_size
|
|
985
|
+
return out
|