osintengine 1.0.2__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 (103) hide show
  1. osintengine-1.0.2.dist-info/METADATA +311 -0
  2. osintengine-1.0.2.dist-info/RECORD +103 -0
  3. osintengine-1.0.2.dist-info/WHEEL +5 -0
  4. osintengine-1.0.2.dist-info/entry_points.txt +2 -0
  5. osintengine-1.0.2.dist-info/licenses/LICENSE +202 -0
  6. osintengine-1.0.2.dist-info/top_level.txt +2 -0
  7. src/watson/__init__.py +10 -0
  8. src/watson/agent/__init__.py +96 -0
  9. src/watson/agents/__init__.py +101 -0
  10. src/watson/agents/protocol.py +196 -0
  11. src/watson/auth/__init__.py +68 -0
  12. src/watson/auth/store.py +145 -0
  13. src/watson/conversation.py +68 -0
  14. src/watson/core/__init__.py +0 -0
  15. src/watson/core/models.py +96 -0
  16. src/watson/ethics.py +205 -0
  17. src/watson/exports.py +182 -0
  18. src/watson/graph/__init__.py +69 -0
  19. src/watson/graph/entities.py +207 -0
  20. src/watson/graph/graph.py +489 -0
  21. src/watson/graph/osint_framework.py +266 -0
  22. src/watson/graph/relationships.py +116 -0
  23. src/watson/graph/transforms.py +787 -0
  24. src/watson/infra/__init__.py +43 -0
  25. src/watson/infra/cache.py +171 -0
  26. src/watson/infra/ratelimit.py +125 -0
  27. src/watson/infra/resilience.py +239 -0
  28. src/watson/infra/retry.py +186 -0
  29. src/watson/metrics.py +128 -0
  30. src/watson/opsec/__init__.py +290 -0
  31. src/watson/orchestration/__init__.py +23 -0
  32. src/watson/orchestration/engine.py +5587 -0
  33. src/watson/orchestration/executor.py +96 -0
  34. src/watson/orchestration/intent.py +139 -0
  35. src/watson/orchestration/intent_classifier.py +45 -0
  36. src/watson/orchestration/llm_config.py +160 -0
  37. src/watson/orchestration/resolution.py +555 -0
  38. src/watson/orchestration/scraper.py +94 -0
  39. src/watson/orchestration/synthesis.py +726 -0
  40. src/watson/orchestration/target_profile.py +748 -0
  41. src/watson/persistence/__init__.py +18 -0
  42. src/watson/persistence/models.py +161 -0
  43. src/watson/persistence/store.py +230 -0
  44. src/watson/pipeline/__init__.py +16 -0
  45. src/watson/pipeline/pre_synthesis.py +621 -0
  46. src/watson/search.py +103 -0
  47. src/watson/serializers/stix.py +451 -0
  48. src/watson/tools/__init__.py +31 -0
  49. src/watson/tools/base.py +97 -0
  50. src/watson/tools/blockchain.py +359 -0
  51. src/watson/tools/captcha.py +276 -0
  52. src/watson/tools/conflict.py +107 -0
  53. src/watson/tools/corporate.py +287 -0
  54. src/watson/tools/darkweb.py +144 -0
  55. src/watson/tools/geolocation.py +347 -0
  56. src/watson/tools/image_video.py +108 -0
  57. src/watson/tools/marinetraffic.py +142 -0
  58. src/watson/tools/people.py +476 -0
  59. src/watson/tools/registry.py +56 -0
  60. src/watson/tools/satellite.py +118 -0
  61. src/watson/tools/scraper.py +745 -0
  62. src/watson/tools/shodan.py +129 -0
  63. src/watson/tools/social_media.py +160 -0
  64. src/watson/tools/websites.py +291 -0
  65. src/watson/tools/wikidata.py +398 -0
  66. src/watson/utils/__init__.py +1 -0
  67. src/watson/utils/helpers.py +49 -0
  68. src/watson/utils/http.py +119 -0
  69. src/watson/verification/__init__.py +245 -0
  70. watson/__init__.py +6 -0
  71. watson/agents/__init__.py +20 -0
  72. watson/agents/base.py +103 -0
  73. watson/agents/direct.py +225 -0
  74. watson/agents/hermes.py +275 -0
  75. watson/agents/hermes_mcp.py +292 -0
  76. watson/api_keys.py +176 -0
  77. watson/browser_scraper.py +481 -0
  78. watson/cli.py +836 -0
  79. watson/crossref.py +175 -0
  80. watson/ethics.py +20 -0
  81. watson/graph.py +406 -0
  82. watson/mcp_server.py +601 -0
  83. watson/memory.py +552 -0
  84. watson/neo4j_graph.py +124 -0
  85. watson/opsec/__init__.py +307 -0
  86. watson/reporter.py +537 -0
  87. watson/scheduler.py +486 -0
  88. watson/serializers/stix.py +451 -0
  89. watson/terminal.py +410 -0
  90. watson/toolkit.py +252 -0
  91. watson/toolkit_api.py +347 -0
  92. watson/toolkit_automation.py +95 -0
  93. watson/toolkit_registry.py +345 -0
  94. watson/verification/__init__.py +251 -0
  95. watson/web/__init__.py +1 -0
  96. watson/web/app.py +1650 -0
  97. watson/web/middleware.py +301 -0
  98. watson/web/static/assets/index-B7hPOc0z.js +278 -0
  99. watson/web/static/assets/index-CJ6FP8Mp.css +1 -0
  100. watson/web/static/assets/watson-logo-Dk9tawHb.png +0 -0
  101. watson/web/static/index.html +14 -0
  102. watson/web/templates/chat.html +2 -0
  103. watson/web/templates/investigation-map.html +429 -0
@@ -0,0 +1,307 @@
1
+ """OpSec Proxy Firewall — enterprise HTTP layer with proxy chaining, domain-
2
+ specific rate limiting, and connection pooling.
3
+
4
+ Extends BaseHTTPClient from watson.utils.http with:
5
+ - SOCKS5 / HTTP / HTTPS proxy support
6
+ - Per-domain rate limiting (token bucket per host)
7
+ - Configurable proxy chain (multiple hops)
8
+ - TLS fingerprint rotation
9
+ - Real browser header profiles per domain category
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import logging
16
+ import os
17
+ import random
18
+ import time
19
+ from typing import Optional
20
+ from urllib.parse import urlparse
21
+
22
+ import httpx
23
+
24
+ # Import BaseHTTPClient from either package root (watson/) or src tree (src/watson/)
25
+ try:
26
+ from .utils.http import BaseHTTPClient
27
+ except ImportError:
28
+ try:
29
+ from src.watson.utils.http import BaseHTTPClient
30
+ except ImportError:
31
+ BaseHTTPClient = None # type: ignore
32
+
33
+ USER_AGENTS = [
34
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
35
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
36
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
37
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0",
38
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
39
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
40
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
41
+ ]
42
+
43
+ logger = logging.getLogger("watson.opsec")
44
+
45
+ # ── Proxy configuration ──────────────────────────────────────
46
+
47
+ _DEFAULT_PROXY = os.environ.get("WATSON_PROXY", "") # socks5://user:pass@host:port
48
+ _PROXY_CHAIN = os.environ.get("WATSON_PROXY_CHAIN", "") # comma-separated SOCKS5 URLs
49
+
50
+
51
+ def _parse_proxy_url(url: str) -> str | None:
52
+ """Parse a proxy URL into httpx format. Returns None if empty/invalid."""
53
+ if not url or not url.strip():
54
+ return None
55
+ parsed = urlparse(url.strip())
56
+ if parsed.scheme in ("socks5", "socks5h"):
57
+ # httpx: socks5://user:pass@host:port
58
+ netloc = parsed.hostname or ""
59
+ if parsed.port:
60
+ netloc += f":{parsed.port}"
61
+ auth = f"{parsed.username}:{parsed.password}@" if parsed.username else ""
62
+ return f"socks5://{auth}{netloc}"
63
+ if parsed.scheme in ("http", "https"):
64
+ return url.strip()
65
+ logger.warning("opsec: unknown proxy scheme %s", parsed.scheme)
66
+ return None
67
+
68
+
69
+ # ── Per-domain rate limiter ───────────────────────────────────
70
+
71
+ class DomainRateLimiter:
72
+ """Token bucket rate limiter per domain."""
73
+
74
+ def __init__(self, default_rps: float = 1.0):
75
+ self.default_rps = default_rps
76
+ self._buckets: dict[str, tuple[float, float]] = {} # domain → (tokens, last_refill)
77
+ # Harsher limits for known rate-limit-happy domains
78
+ self._overrides: dict[str, float] = {
79
+ "crt.sh": 0.5,
80
+ "opencorporates.com": 0.5,
81
+ "google.com": 0.3,
82
+ "linkedin.com": 0.2,
83
+ "api.github.com": 2.0,
84
+ }
85
+
86
+ def _rps_for(self, domain: str) -> float:
87
+ for pattern, rps in self._overrides.items():
88
+ if pattern in domain:
89
+ return rps
90
+ return self.default_rps
91
+
92
+ async def acquire(self, domain: str) -> None:
93
+ rate = self._rps_for(domain)
94
+ while True:
95
+ now = time.monotonic()
96
+ tokens, last = self._buckets.get(domain, (rate, now))
97
+ elapsed = now - last
98
+ tokens = min(rate, tokens + elapsed * rate)
99
+ self._buckets[domain] = (tokens, now)
100
+
101
+ if tokens >= 1.0:
102
+ self._buckets[domain] = (tokens - 1.0, now)
103
+ return
104
+ await asyncio.sleep(0.1)
105
+
106
+
107
+ # ── Domain-specific header profiles ───────────────────────────
108
+
109
+ _DOMAIN_HEADERS: dict[str, dict[str, str]] = {
110
+ "google.com": {
111
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
112
+ "Accept-Language": "en-US,en;q=0.9",
113
+ },
114
+ "opencorporates.com": {
115
+ "Accept": "application/json, text/html, */*",
116
+ "Accept-Language": "en-US,en;q=0.9",
117
+ },
118
+ "wikipedia.org": {
119
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
120
+ "Accept-Language": "en-US,en;q=0.9",
121
+ },
122
+ "linkedin.com": {
123
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
124
+ "Accept-Language": "en-US,en;q=0.5",
125
+ },
126
+ }
127
+
128
+
129
+ # ── OpSec HTTP Client ─────────────────────────────────────────
130
+
131
+ class OpSecClient(BaseHTTPClient if BaseHTTPClient else object):
132
+ """Enterprise HTTP client: proxy chaining, per-domain rate limiting,
133
+ real browser header profiles, and rotating TLS fingerprints.
134
+
135
+ Usage:
136
+ client = OpSecClient()
137
+ resp = await client.get("https://crt.sh/?q=%.example.com")
138
+ data = resp.json()
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ proxy: str | None = None,
144
+ proxy_chain: list[str] | None = None,
145
+ rate_limit: float = 1.0,
146
+ max_retries: int = 3,
147
+ timeout: float = 20.0,
148
+ rotate_fingerprints: bool = True,
149
+ ):
150
+ super().__init__(rate_limit=rate_limit, max_retries=max_retries, timeout=timeout)
151
+ self._proxy = proxy or _parse_proxy_url(_DEFAULT_PROXY)
152
+ self._proxy_chain = proxy_chain or [
153
+ p for u in _PROXY_CHAIN.split(",") if (p := _parse_proxy_url(u))
154
+ ] if _PROXY_CHAIN else []
155
+ self._domain_limiter = DomainRateLimiter()
156
+ self._rotate_fingerprints = rotate_fingerprints
157
+ self._session_counter = 0
158
+
159
+ async def _get_client(self) -> httpx.AsyncClient:
160
+ """Create a new client with proxy and domain-appropriate headers."""
161
+ # Build proxy mount
162
+ proxy_mounts = {}
163
+ if self._proxy:
164
+ proxy_mounts["http://"] = self._proxy
165
+ proxy_mounts["https://"] = self._proxy
166
+ logger.debug("opsec: using proxy %s", self._proxy)
167
+
168
+ # Build headers with domain-appropriate profile
169
+ headers = {
170
+ "User-Agent": random.choice(USER_AGENTS),
171
+ "Accept": "application/json, text/plain, */*",
172
+ "Accept-Language": "en-US,en;q=0.9",
173
+ }
174
+
175
+ client = httpx.AsyncClient(
176
+ timeout=httpx.Timeout(self.timeout, connect=10.0),
177
+ follow_redirects=True,
178
+ headers=headers,
179
+ mounts=proxy_mounts if proxy_mounts else None,
180
+ )
181
+ return client
182
+
183
+ def _domain_for(self, url: str) -> str:
184
+ parsed = urlparse(url)
185
+ return parsed.hostname or ""
186
+
187
+ def _per_domain_headers(self, domain: str) -> dict[str, str]:
188
+ """Get domain-specific header overrides."""
189
+ for pattern, hdrs in _DOMAIN_HEADERS.items():
190
+ if pattern in domain:
191
+ return hdrs
192
+ return {}
193
+
194
+ async def get(self, url: str, **kwargs) -> httpx.Response:
195
+ """GET with per-domain rate limiting, proxy, and retry."""
196
+ domain = self._domain_for(url)
197
+ await self._domain_limiter.acquire(domain)
198
+
199
+ # Inject domain-specific headers
200
+ domain_hdrs = self._per_domain_headers(domain)
201
+ if domain_hdrs:
202
+ existing = kwargs.get("headers", {})
203
+ merged = {**domain_hdrs, **existing}
204
+ kwargs["headers"] = merged
205
+
206
+ # Rotate UA per request for fingerprint diversity
207
+ client = await self._get_client()
208
+ if self._rotate_fingerprints:
209
+ client.headers["User-Agent"] = random.choice(USER_AGENTS)
210
+
211
+ last_err = ""
212
+ for attempt in range(self.max_retries + 1):
213
+ try:
214
+ response = await client.get(url, **kwargs)
215
+ response.raise_for_status()
216
+ return response
217
+ except httpx.HTTPStatusError as e:
218
+ last_err = f"HTTP {e.response.status_code} from {url}"
219
+ if e.response.status_code == 429:
220
+ retry_after = float(e.response.headers.get("Retry-After", 10))
221
+ logger.debug("opsec: 429 from %s, waiting %ss", domain, retry_after)
222
+ await asyncio.sleep(retry_after)
223
+ continue
224
+ if e.response.status_code in (403, 451):
225
+ # Legal/censorship block — rotate proxy if available
226
+ if self._proxy_chain:
227
+ self._proxy = random.choice(self._proxy_chain)
228
+ client = await self._get_client()
229
+ logger.info("opsec: rotated proxy for %s after %d", domain, e.response.status_code)
230
+ continue
231
+ if attempt == self.max_retries:
232
+ self._last_error = last_err
233
+ raise
234
+ await asyncio.sleep(2 ** attempt)
235
+ except (httpx.RequestError, httpx.TimeoutException) as e:
236
+ last_err = f"Connection error for {url}: {e}"
237
+ if attempt == self.max_retries:
238
+ self._last_error = last_err
239
+ raise
240
+ # Rotate proxy on connection failure
241
+ if self._proxy_chain:
242
+ self._proxy = random.choice(self._proxy_chain)
243
+ client = await self._get_client()
244
+ logger.info("opsec: rotated proxy after connection failure to %s", domain)
245
+ await asyncio.sleep(2 ** attempt)
246
+
247
+ self._last_error = last_err
248
+ raise RuntimeError(f"Failed to fetch {url} after {self.max_retries + 1} attempts")
249
+
250
+ async def post(self, url: str, **kwargs) -> httpx.Response:
251
+ """POST with per-domain rate limiting and proxy."""
252
+ domain = self._domain_for(url)
253
+ await self._domain_limiter.acquire(domain)
254
+ client = await self._get_client()
255
+
256
+ last_err = ""
257
+ for attempt in range(self.max_retries + 1):
258
+ try:
259
+ response = await client.post(url, **kwargs)
260
+ response.raise_for_status()
261
+ return response
262
+ except httpx.HTTPStatusError as e:
263
+ last_err = f"HTTP {e.response.status_code} from {url}"
264
+ if e.response.status_code == 429:
265
+ await asyncio.sleep(10)
266
+ continue
267
+ if attempt == self.max_retries:
268
+ self._last_error = last_err
269
+ raise
270
+ await asyncio.sleep(2 ** attempt)
271
+ except (httpx.RequestError, httpx.TimeoutException) as e:
272
+ last_err = f"Connection error for {url}: {e}"
273
+ if self._proxy_chain:
274
+ self._proxy = random.choice(self._proxy_chain)
275
+ client = await self._get_client()
276
+ if attempt == self.max_retries:
277
+ self._last_error = last_err
278
+ raise
279
+ await asyncio.sleep(2 ** attempt)
280
+
281
+ self._last_error = last_err
282
+ raise RuntimeError(f"Failed to POST {url} after {self.max_retries + 1} attempts")
283
+
284
+ def stats(self) -> dict:
285
+ """Return OpSec statistics for monitoring."""
286
+ return {
287
+ "proxy": self._proxy or "direct",
288
+ "proxy_chain_size": len(self._proxy_chain),
289
+ "rate_limiter": {
290
+ "default_rps": self.rate_limiter.rate,
291
+ "active_domains": len(self._domain_limiter._buckets),
292
+ },
293
+ "errors": self._last_error,
294
+ }
295
+
296
+
297
+ # ── Global instance (lazy) ────────────────────────────────────
298
+
299
+ _opsec_client: Optional[OpSecClient] = None
300
+
301
+
302
+ def get_opsec_client() -> OpSecClient:
303
+ """Get or create the global OpSec client instance."""
304
+ global _opsec_client
305
+ if _opsec_client is None:
306
+ _opsec_client = OpSecClient()
307
+ return _opsec_client