darkwatch 0.5.1__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.
Files changed (46) hide show
  1. darkwatch/__init__.py +3 -0
  2. darkwatch/__main__.py +5 -0
  3. darkwatch/cache.py +100 -0
  4. darkwatch/cli.py +896 -0
  5. darkwatch/config.py +558 -0
  6. darkwatch/data/__init__.py +0 -0
  7. darkwatch/data/telegram_channels.json +5400 -0
  8. darkwatch/desktop.py +125 -0
  9. darkwatch/fetch.py +196 -0
  10. darkwatch/matcher.py +347 -0
  11. darkwatch/notify.py +246 -0
  12. darkwatch/report.py +425 -0
  13. darkwatch/scanner.py +222 -0
  14. darkwatch/schedule.py +232 -0
  15. darkwatch/search.py +359 -0
  16. darkwatch/sources/__init__.py +161 -0
  17. darkwatch/sources/ahmia.py +351 -0
  18. darkwatch/sources/hibp.py +140 -0
  19. darkwatch/sources/leakcheck.py +110 -0
  20. darkwatch/sources/leaksites.py +196 -0
  21. darkwatch/sources/recentattacks.py +88 -0
  22. darkwatch/sources/seeds.py +92 -0
  23. darkwatch/sources/sites.py +225 -0
  24. darkwatch/sources/stealers.py +151 -0
  25. darkwatch/sources/telegram.py +145 -0
  26. darkwatch/sources/xposedornot.py +195 -0
  27. darkwatch/storage.py +382 -0
  28. darkwatch/tor.py +227 -0
  29. darkwatch/web/__init__.py +7 -0
  30. darkwatch/web/jobs.py +93 -0
  31. darkwatch/web/server.py +375 -0
  32. darkwatch/web/static/app.css +323 -0
  33. darkwatch/web/static/app.js +730 -0
  34. darkwatch/web/static/favicon.svg +6 -0
  35. darkwatch/web/static/index.html +176 -0
  36. darkwatch/web/static/shader.js +205 -0
  37. darkwatch/web/static/vendor/DrawSVGPlugin.min.js +11 -0
  38. darkwatch/web/static/vendor/Flip.min.js +11 -0
  39. darkwatch/web/static/vendor/ScrambleTextPlugin.min.js +11 -0
  40. darkwatch/web/static/vendor/ScrollTrigger.min.js +11 -0
  41. darkwatch/web/static/vendor/gsap.min.js +11 -0
  42. darkwatch-0.5.1.dist-info/METADATA +351 -0
  43. darkwatch-0.5.1.dist-info/RECORD +46 -0
  44. darkwatch-0.5.1.dist-info/WHEEL +4 -0
  45. darkwatch-0.5.1.dist-info/entry_points.txt +2 -0
  46. darkwatch-0.5.1.dist-info/licenses/LICENSE +21 -0
darkwatch/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Darkwatch — dark web exposure monitor for people and companies you are authorised to protect."""
2
+
3
+ __version__ = "0.5.1"
darkwatch/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """`python -m darkwatch` (used by the scheduled task through pythonw.exe)."""
2
+
3
+ from .cli import main
4
+
5
+ main()
darkwatch/cache.py ADDED
@@ -0,0 +1,100 @@
1
+ """On-disk cache for bulk feeds, with conditional GET (ETag / Last-Modified) and a max age.
2
+
3
+ A feed younger than `max_age_hours` is served from disk with no network call. An older one is
4
+ revalidated; a 304 just refreshes the timestamp. If the network fails and a cached copy exists,
5
+ the stale copy is used and the caller is told so, which keeps a run useful offline.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import time
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ import requests
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class CachedFeed:
24
+ body: bytes
25
+ fetched_at: float # epoch seconds of the last successful download or revalidation
26
+ from_network: bool # True if bytes were downloaded this call
27
+ stale: bool # True if the network failed and an old copy was used
28
+ note: str = ""
29
+
30
+
31
+ class FeedCache:
32
+ def __init__(self, directory: str | Path):
33
+ self.dir = Path(directory)
34
+ self.dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def _paths(self, name: str) -> tuple[Path, Path]:
37
+ return self.dir / f"{name}.body", self.dir / f"{name}.meta.json"
38
+
39
+ def get(
40
+ self,
41
+ name: str,
42
+ url: str,
43
+ session: requests.Session,
44
+ *,
45
+ max_age_hours: float,
46
+ timeout: int = 120,
47
+ max_bytes: int = 200_000_000,
48
+ validate: Callable[[bytes], None] | None = None,
49
+ ) -> CachedFeed:
50
+ """`validate(body)` raises ValueError for a body that must not replace the cached copy
51
+ (an HTML maintenance page, an empty body, a JSON error object)."""
52
+ body_p, meta_p = self._paths(name)
53
+ meta: dict = {}
54
+ if meta_p.exists() and body_p.exists():
55
+ try:
56
+ meta = json.loads(meta_p.read_text(encoding="utf-8"))
57
+ except ValueError:
58
+ meta = {}
59
+ now = time.time()
60
+ if meta and now - meta.get("fetched_at", 0) < max_age_hours * 3600:
61
+ return CachedFeed(body_p.read_bytes(), meta["fetched_at"], False, False, "fresh cache")
62
+
63
+ headers = {}
64
+ if meta.get("etag"):
65
+ headers["If-None-Match"] = meta["etag"]
66
+ if meta.get("last_modified"):
67
+ headers["If-Modified-Since"] = meta["last_modified"]
68
+ try:
69
+ with session.get(url, headers=headers, timeout=timeout, stream=True) as r:
70
+ if r.status_code == 304 and meta:
71
+ meta["fetched_at"] = now
72
+ meta_p.write_text(json.dumps(meta), encoding="utf-8")
73
+ return CachedFeed(body_p.read_bytes(), now, False, False, "not modified")
74
+ r.raise_for_status()
75
+ buf = bytearray()
76
+ for chunk in r.iter_content(chunk_size=256 * 1024):
77
+ buf.extend(chunk)
78
+ if len(buf) > max_bytes:
79
+ raise ValueError(f"feed {name} exceeds {max_bytes} bytes")
80
+ if validate is not None:
81
+ validate(bytes(buf))
82
+ tmp = body_p.with_suffix(".part")
83
+ tmp.write_bytes(bytes(buf))
84
+ tmp.replace(body_p)
85
+ meta = {
86
+ "url": url,
87
+ "fetched_at": now,
88
+ "etag": r.headers.get("ETag", ""),
89
+ "last_modified": r.headers.get("Last-Modified", ""),
90
+ "bytes": len(buf),
91
+ }
92
+ meta_p.write_text(json.dumps(meta), encoding="utf-8")
93
+ return CachedFeed(bytes(buf), now, True, False, f"downloaded {len(buf)} bytes")
94
+ except (requests.RequestException, ValueError) as exc:
95
+ if body_p.exists():
96
+ log.warning("feed %s refresh failed (%s); using cached copy", name, exc)
97
+ return CachedFeed(
98
+ body_p.read_bytes(), meta.get("fetched_at", 0), False, True, f"stale: {exc}"
99
+ )
100
+ raise