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/loaders.py ADDED
@@ -0,0 +1,382 @@
1
+ """Loaders for the bulk OSV / vendor / toolkit corpora."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import pathlib
6
+ import re
7
+ import sqlite3
8
+ import zipfile
9
+
10
+ CVE_RE = re.compile(r"CVE-\d{4}-\d{4,7}", re.I)
11
+
12
+ MSRC_IMPACT = {
13
+ "Elevation of Privilege": "EoP",
14
+ "Remote Code Execution": "RCE",
15
+ "Information Disclosure": "InfoDisclosure",
16
+ "Denial of Service": "DoS",
17
+ "Spoofing": "Spoofing",
18
+ "Security Feature Bypass": "Bypass",
19
+ "Tampering": "Tampering",
20
+ "Defense in Depth": "DefenseInDepth",
21
+ "Not a Vulnerability": "",
22
+ }
23
+
24
+ MSRC_SEVERITY = {100000000: "Critical", 100000001: "Important",
25
+ 100000002: "Moderate", 100000003: "Low"}
26
+
27
+
28
+ def _norm(s: str | None, limit: int = 4000) -> str:
29
+ if not s:
30
+ return ""
31
+ return re.sub(r"\s+", " ", s).strip()[:limit]
32
+
33
+
34
+ # ---------------------------------------------------------------- OSV
35
+ def load_osv_zip(path: pathlib.Path, con: sqlite3.Connection) -> int:
36
+ """One OSV ecosystem zip -> advisory + package_range rows."""
37
+ if not path.exists():
38
+ return 0
39
+ eco_default = path.stem
40
+ adv: dict[tuple[str, str], dict] = {}
41
+ ranges: list[tuple] = []
42
+ with zipfile.ZipFile(path) as zf:
43
+ for name in zf.namelist():
44
+ if not name.endswith(".json"):
45
+ continue
46
+ try:
47
+ doc = json.loads(zf.read(name))
48
+ except (json.JSONDecodeError, KeyError):
49
+ continue
50
+ aid = doc.get("id")
51
+ if not aid:
52
+ continue
53
+ aliases = [str(a) for a in doc.get("aliases") or []]
54
+ cve = next((a.upper() for a in aliases if CVE_RE.fullmatch(a.upper())), "")
55
+ if not cve:
56
+ m = CVE_RE.search(doc.get("summary") or "")
57
+ cve = m.group(0).upper() if m else ""
58
+ sev = ""
59
+ for s in doc.get("severity") or []:
60
+ if s.get("score"):
61
+ sev = s["score"]
62
+ sev = sev or (doc.get("database_specific") or {}).get("severity") or ""
63
+ affected = doc.get("affected") or []
64
+ first_eco, first_pkg = "", ""
65
+ for a in affected:
66
+ pkg = a.get("package") or {}
67
+ eco = pkg.get("ecosystem") or eco_default
68
+ pname = (pkg.get("name") or "").lower()
69
+ if not pname:
70
+ continue
71
+ first_eco = first_eco or eco
72
+ first_pkg = first_pkg or pname
73
+ for rng in a.get("ranges") or []:
74
+ intro = fixed = last = ""
75
+ for ev in rng.get("events") or []:
76
+ if "introduced" in ev and not intro:
77
+ intro = str(ev["introduced"])
78
+ if "fixed" in ev and not fixed:
79
+ fixed = str(ev["fixed"])
80
+ if "last_affected" in ev and not last:
81
+ last = str(ev["last_affected"])
82
+ ranges.append((aid, eco, pname, intro, fixed, last,
83
+ ",".join(a.get("versions") or [])[:1000]))
84
+ if not a.get("ranges") and a.get("versions"):
85
+ ranges.append((aid, eco, pname, "", "", "",
86
+ ",".join(a["versions"])[:1000]))
87
+ adv[(aid, eco_default)] = {
88
+ "id": aid, "ecosystem": first_eco, "package": first_pkg, "cve": cve,
89
+ "summary": _norm(doc.get("summary") or doc.get("details"), 800),
90
+ "severity": sev, "published": doc.get("published"),
91
+ "modified": doc.get("modified"),
92
+ "url": f"https://osv.dev/vulnerability/{aid}",
93
+ "aliases": ",".join(aliases)[:500],
94
+ "affected": len(affected),
95
+ }
96
+ rows = [(r["id"], "osv", r["ecosystem"], r["package"], r["cve"], r["summary"],
97
+ r["severity"], r["published"], r["modified"], r["url"], r["aliases"],
98
+ r["affected"]) for r in adv.values()]
99
+ con.executemany(
100
+ "INSERT OR REPLACE INTO advisory(source,id,ecosystem,package,cve,summary,"
101
+ "severity,published,modified,url,aliases,affected_count) VALUES ('osv',?,?,?,?,?,?,?,?,?,?,?)",
102
+ [(r[0], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11]) for r in rows])
103
+ # Reloading a zip re-REPLACEs advisory rows; clear their old ranges too so
104
+ # package_range stays in sync instead of accumulating duplicate rows.
105
+ con.executemany("DELETE FROM package_range WHERE advisory_id=?",
106
+ [(r[0],) for r in rows])
107
+ con.executemany(
108
+ "INSERT INTO package_range(advisory_id,ecosystem,package,introduced,fixed,"
109
+ "last_affected,versions) VALUES (?,?,?,?,?,?,?)", ranges)
110
+ con.commit()
111
+ return len(rows)
112
+
113
+
114
+ # ---------------------------------------------------------------- Debian
115
+ def load_debian(path: pathlib.Path, con: sqlite3.Connection) -> int:
116
+ """Debian tracker: package -> CVE -> per-release status/fixed version."""
117
+ if not path.exists():
118
+ return 0
119
+ doc = json.loads(path.read_text())
120
+ rows = []
121
+ for pkg, cves in doc.items():
122
+ if not isinstance(cves, dict):
123
+ continue
124
+ for cve, info in cves.items():
125
+ if not CVE_RE.fullmatch(cve):
126
+ continue
127
+ desc = _norm((info or {}).get("description"), 500)
128
+ for rel, rinfo in ((info or {}).get("releases") or {}).items():
129
+ rows.append((pkg.lower(), rel, cve, (rinfo or {}).get("status", ""),
130
+ (rinfo or {}).get("fixed_version", ""),
131
+ (rinfo or {}).get("urgency", ""), desc))
132
+ con.execute("DELETE FROM debian")
133
+ con.executemany("INSERT INTO debian(package,release,cve,status,fixed,urgency,"
134
+ "description) VALUES (?,?,?,?,?,?,?)", rows)
135
+ con.commit()
136
+ return len(rows)
137
+
138
+
139
+ # ---------------------------------------------------------------- Microsoft
140
+ def _sev_from_cvss(score: float) -> str:
141
+ """Microsoft severity label derived from a CVSS base score (used only when
142
+ MSRC omits the label but ships a score)."""
143
+ return ("Critical" if score >= 9 else "Important" if score >= 7
144
+ else "Moderate" if score >= 4 else "Low")
145
+
146
+
147
+ def load_msrc(path: pathlib.Path, con: sqlite3.Connection) -> int:
148
+ """MSRC Security Update Guide: CVE, title, severity, impact, product tag."""
149
+ if not path.exists():
150
+ return 0
151
+ rows = []
152
+ for v in json.loads(path.read_text()):
153
+ cid = v.get("cveNumber")
154
+ if not cid:
155
+ continue
156
+ title = v.get("cveTitle") or ""
157
+ impact = MSRC_IMPACT.get(v.get("impact") or "", "")
158
+ if not impact:
159
+ low = title.lower()
160
+ for kw, val in (("remote code execution", "RCE"),
161
+ ("elevation of privilege", "EoP"),
162
+ ("information disclosure", "InfoDisclosure"),
163
+ ("denial of service", "DoS"),
164
+ ("security feature bypass", "Bypass"),
165
+ ("spoofing", "Spoofing"),
166
+ ("tampering", "Tampering")):
167
+ if kw in low:
168
+ impact = val
169
+ break
170
+ severity = MSRC_SEVERITY.get(v.get("severityId"), "")
171
+ raw_sev = (v.get("severity") or "").strip()
172
+ if raw_sev and raw_sev.lower() != "none":
173
+ severity = raw_sev
174
+ if not severity: # recover from a CVSS score if present
175
+ score = v.get("baseScore")
176
+ if not score:
177
+ for sset in (v.get("cvssScoreSets") or []):
178
+ if sset.get("baseScore"):
179
+ score = sset["baseScore"]
180
+ break
181
+ if score:
182
+ try:
183
+ severity = _sev_from_cvss(float(score))
184
+ except (TypeError, ValueError):
185
+ pass
186
+ # MSRC sends "Yes"/"No"; the old truthiness test marked 13.4k "No" rows.
187
+ exploited = "exploited" if str(v.get("exploited") or "").lower() == "yes" else ""
188
+ rows.append((
189
+ cid, "windows", (v.get("tag") or "Microsoft").lower(),
190
+ f"{(v.get('releaseNumber') or '')}",
191
+ title, severity, impact, exploited,
192
+ v.get("releaseDate") or "",
193
+ v.get("mitreUrl") or f"https://msrc.microsoft.com/update-guide/vulnerability/{cid}",
194
+ ))
195
+ con.execute("DELETE FROM os_advisory WHERE source='msrc'")
196
+ con.executemany(
197
+ "INSERT OR REPLACE INTO os_advisory(source,cve,platform,product,version,title,"
198
+ "severity,impact,exploited,released,url) VALUES ('msrc',?,?,?,?,?,?,?,?,?,?)", rows)
199
+ con.commit()
200
+ return len(rows)
201
+
202
+
203
+ # ---------------------------------------------------------------- Apple
204
+ APPLE_ROWS = re.compile(
205
+ r'<a href="(https://support\.apple\.com/en-us/\d+)"[^>]*>([^<]+)</a>.*?'
206
+ r'<td[^>]*><p[^>]*>(.*?)</p></td>\s*<td[^>]*><p[^>]*>([^<]*)</p></td>',
207
+ re.S)
208
+
209
+
210
+ def load_apple(root: pathlib.Path, con: sqlite3.Connection) -> int:
211
+ """Apple security releases: product/version -> CVE list scraped from advisories."""
212
+ index = root / "index.html"
213
+ if not index.exists():
214
+ return 0
215
+ html = index.read_text(encoding="utf-8", errors="replace")
216
+ rows = []
217
+ for url, title, _devices, date in APPLE_ROWS.findall(html):
218
+ title = re.sub(r"\s+", " ", title).strip()
219
+ page = root / "pages" / (url.rsplit("/", 1)[-1] + ".html")
220
+ cves: list[str] = []
221
+ exploited_map: dict[str, str] = {}
222
+ if page.exists():
223
+ txt = page.read_text(encoding="utf-8", errors="replace")
224
+ cves = sorted({c.upper() for c in CVE_RE.findall(txt)})
225
+ # Apple notes exploitation in each CVE's block, just before its id:
226
+ # attribute "exploited" to a CVE if the phrase is in its preceding segment.
227
+ last = 0
228
+ for m in CVE_RE.finditer(txt):
229
+ seg = txt[last:m.start()]
230
+ if re.search(r"(actively )?(been )?exploited", seg, re.I) and \
231
+ re.search(r"aware|report|exploited in", seg, re.I):
232
+ exploited_map[m.group(0).upper()] = "exploited"
233
+ last = m.start()
234
+ platform = "macos" if title.lower().startswith("macos") else \
235
+ "ios" if any(k in title.lower() for k in ("ios", "ipados")) else "apple"
236
+ version = ""
237
+ m = re.search(r"(\d+(?:\.\d+)*)", title)
238
+ if m:
239
+ version = m.group(1)
240
+ if not cves:
241
+ rows.append(("APPLE:" + url.rsplit("/", 1)[-1], "", platform, title.lower(),
242
+ version, title, "", "", "", date.strip(), url))
243
+ continue
244
+ for c in cves:
245
+ rows.append(("APPLE:" + url.rsplit("/", 1)[-1], c, platform, title.lower(),
246
+ version, title, "", "", exploited_map.get(c, ""), date.strip(), url))
247
+ con.execute("DELETE FROM os_advisory WHERE source='apple'")
248
+ con.executemany(
249
+ "INSERT OR REPLACE INTO os_advisory(source,cve,platform,product,version,title,"
250
+ "severity,impact,exploited,released,url) VALUES ('apple',?,?,?,?,?,?,?,?,?,?)",
251
+ [(r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10]) for r in rows])
252
+ con.commit()
253
+ return len(rows)
254
+
255
+
256
+ # ---------------------------------------------------------------- Metasploit
257
+ NAME_RE = re.compile(r"'Name'\s*=>\s*['\"](.+?)['\"]")
258
+ PLAT_RE = re.compile(r"'Platform'\s*=>\s*(.+)$", re.M)
259
+ PRIV_RE = re.compile(r"'Privileged'\s*=>\s*(true|false)")
260
+ DATE_RE = re.compile(r"'DisclosureDate'\s*=>\s*['\"](.+?)['\"]")
261
+ DESC_RE = re.compile(r"'Description'\s*=>\s*%q\{(.*?)\}\s*,?\s*$", re.S | re.M)
262
+
263
+
264
+ def load_msf_source(root: pathlib.Path, con: sqlite3.Connection) -> int:
265
+ """Parse module sources for metadata + CVE references (fixes cve_msf)."""
266
+ mods = root / "modules"
267
+ if not mods.exists():
268
+ return 0
269
+ rows, pairs = [], []
270
+ for p in mods.rglob("*.rb"):
271
+ rel = p.relative_to(root).as_posix()
272
+ parts = rel.split("/")
273
+ if len(parts) < 3:
274
+ continue
275
+ text = p.read_text(encoding="utf-8", errors="replace")
276
+ name_m = NAME_RE.search(text)
277
+ platform = ""
278
+ pm = PLAT_RE.search(text)
279
+ if pm:
280
+ platform = re.sub(r"[\s%w\[\]'\"]", "", pm.group(1))[:60]
281
+ dm = DESC_RE.search(text)
282
+ date_m = DATE_RE.search(text)
283
+ priv = PRIV_RE.search(text)
284
+ cves = sorted({c.upper() for c in CVE_RE.findall(text)})
285
+ rows.append((rel, parts[1],
286
+ name_m.group(1) if name_m else "/".join(parts[2:]).removesuffix(".rb"),
287
+ platform, 1 if (priv and priv.group(1) == "true") else 0,
288
+ date_m.group(1) if date_m else "",
289
+ _norm(dm.group(1) if dm else "", 1200), ",".join(cves)))
290
+ for c in cves:
291
+ pairs.append((c, rel))
292
+ con.execute("DELETE FROM msf")
293
+ con.executemany("INSERT OR REPLACE INTO msf(path,type,name,platform,privileged,"
294
+ "disclosure_date,description,cves) VALUES (?,?,?,?,?,?,?,?)", rows)
295
+ con.execute("DELETE FROM cve_msf")
296
+ con.executemany("INSERT OR IGNORE INTO cve_msf VALUES (?,?)", pairs)
297
+ con.execute("UPDATE cve SET has_msf=0 WHERE has_msf=1")
298
+ con.execute("UPDATE cve SET has_msf=1 WHERE id IN (SELECT cve_id FROM cve_msf)")
299
+ con.commit()
300
+ return len(rows)
301
+
302
+
303
+ # ---------------------------------------------------------------- Exploit-DB code
304
+ CODE_EXT = {".c", ".cc", ".cpp", ".py", ".rb", ".pl", ".sh", ".php", ".js", ".java",
305
+ ".go", ".rs", ".txt", ".md", ".html", ".xml", ".ps1", ".asm", ".s"}
306
+ CODE_CAP = 60_000
307
+
308
+
309
+ def load_exploit_code(root: pathlib.Path, con: sqlite3.Connection) -> int:
310
+ """Full Exploit-DB sources -> FTS over the code itself."""
311
+ if not root.exists():
312
+ return 0
313
+ con.execute("DELETE FROM exploit_code_fts")
314
+ n = 0
315
+ batch = []
316
+ for p in root.rglob("*"):
317
+ if not p.is_file() or p.suffix.lower() not in CODE_EXT:
318
+ continue
319
+ try:
320
+ if p.stat().st_size > 400_000:
321
+ continue
322
+ text = p.read_text(encoding="utf-8", errors="replace")
323
+ except OSError:
324
+ continue
325
+ rel = p.relative_to(root).as_posix()
326
+ eid = p.stem if p.stem.isdigit() else ""
327
+ batch.append((eid, rel, text[:CODE_CAP]))
328
+ n += 1
329
+ if len(batch) >= 500:
330
+ con.executemany("INSERT INTO exploit_code_fts(exploit_id,path,content) VALUES (?,?,?)", batch)
331
+ batch = []
332
+ if batch:
333
+ con.executemany("INSERT INTO exploit_code_fts(exploit_id,path,content) VALUES (?,?,?)", batch)
334
+ con.commit()
335
+ return n
336
+
337
+
338
+ # ---------------------------------------------------------------- toolkits
339
+ def load_toolkit(root: pathlib.Path, source: str, con: sqlite3.Connection) -> int:
340
+ if not root.exists():
341
+ return 0
342
+ if source == "gtfobins":
343
+ # Jekyll collection: files have no extension (e.g. _gtfobins/awk)
344
+ files = [p for p in (root / "_gtfobins").rglob("*") if p.is_file()]
345
+ else:
346
+ files = (list(root.rglob("*.md")) + list(root.rglob("*.yml"))
347
+ + list(root.rglob("*.yaml")))
348
+ rows = []
349
+ for p in sorted(files)[:8000]:
350
+ if ".git" in p.parts:
351
+ continue
352
+ try:
353
+ text = p.read_text(encoding="utf-8", errors="replace")
354
+ except OSError:
355
+ continue
356
+ name = p.stem
357
+ rel = p.relative_to(root).as_posix()
358
+ # frontmatter: functions/tags block, else the first heading or description
359
+ tags = ""
360
+ m = re.search(r"^functions:\s*\n((?:[ \t]+\w+:.*\n?)+)", text, re.M)
361
+ if m:
362
+ tags = ",".join(sorted(set(re.findall(r"^[ \t]{2}(\w+):", m.group(1), re.M))))
363
+ if not tags:
364
+ m = re.search(r"^(?:functions|tags):\s*(.+)$", text, re.M)
365
+ if m:
366
+ tags = _norm(m.group(1), 200)
367
+ desc = ""
368
+ dm = re.search(r"^description:\s*(.+)$", text, re.M)
369
+ if dm:
370
+ desc = _norm(dm.group(1), 400)
371
+ body = re.sub(r"^---.*?---", "", text, flags=re.S)
372
+ if not desc:
373
+ heading = re.search(r"^#\s+(.+)$", body, re.M)
374
+ desc = _norm(heading.group(1) if heading else body, 400)
375
+ rows.append((source, name, rel, desc, tags))
376
+ con.execute("DELETE FROM toolkit WHERE source=?", (source,))
377
+ con.executemany("INSERT INTO toolkit(source,name,path,description,tags) VALUES (?,?,?,?,?)", rows)
378
+ con.execute("DELETE FROM toolkit_fts WHERE source=?", (source,))
379
+ con.executemany("INSERT INTO toolkit_fts(source,name,description,tags) VALUES (?,?,?,?)",
380
+ [(r[0], r[1], r[3], r[4]) for r in rows])
381
+ con.commit()
382
+ return len(rows)