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,301 @@
1
+ """
2
+ Enterprise middleware — auth, rate limiting, tracing, structured logging.
3
+
4
+ Loaded by watson.web.app on startup.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import hmac
11
+ import json
12
+ import logging
13
+ import os
14
+ import sys
15
+ import time
16
+ import uuid
17
+ from collections import defaultdict
18
+ from contextvars import ContextVar
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Callable
22
+
23
+ from starlette.middleware.base import BaseHTTPMiddleware
24
+ from starlette.requests import Request
25
+ from starlette.responses import JSONResponse, Response
26
+
27
+ # ── Structured JSON logging ─────────────────────────────────────
28
+
29
+ class JSONFormatter(logging.Formatter):
30
+ def format(self, record: logging.LogRecord) -> str:
31
+ log_entry = {
32
+ "timestamp": datetime.now(timezone.utc).isoformat(),
33
+ "level": record.levelname,
34
+ "logger": record.name,
35
+ "message": record.getMessage(),
36
+ "correlation_id": _correlation_id.get(""),
37
+ "module": record.module,
38
+ }
39
+ if record.exc_info and record.exc_info[1]:
40
+ log_entry["exception"] = str(record.exc_info[1])
41
+ return json.dumps(log_entry)
42
+
43
+
44
+ def setup_logging(level: str = "INFO"):
45
+ handler = logging.StreamHandler(sys.stdout)
46
+ handler.setFormatter(JSONFormatter())
47
+ root = logging.getLogger()
48
+ root.handlers.clear()
49
+ root.addHandler(handler)
50
+ root.setLevel(getattr(logging, level.upper(), logging.INFO))
51
+ # Silence noisy libs
52
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
53
+ logging.getLogger("httpx").setLevel(logging.WARNING)
54
+
55
+
56
+ # ── Correlation IDs ─────────────────────────────────────────────
57
+
58
+ _correlation_id: ContextVar[str] = ContextVar("correlation_id", default="")
59
+
60
+ def get_correlation_id() -> str:
61
+ return _correlation_id.get() or ""
62
+
63
+ # ── API Key Auth ─────────────────────────────────────────────────
64
+
65
+ # Load valid keys from env: WATSON_API_KEYS=key1,key2
66
+ _VALID_KEYS: set[str] = set()
67
+ _raw = os.environ.get("WATSON_API_KEYS", "")
68
+ if _raw:
69
+ _VALID_KEYS = {k.strip() for k in _raw.split(",") if k.strip()}
70
+
71
+ # Also accept the Hermes API key if configured
72
+ _hermes_key = os.environ.get("HERMES_API_KEY", "")
73
+ if _hermes_key:
74
+ _VALID_KEYS.add(_hermes_key)
75
+
76
+ # Public endpoints — no auth required
77
+ _PUBLIC_PATHS = {"/", "/health", "/docs", "/openapi.json", "/redoc", "/static", "/api/settings"}
78
+
79
+ # Terminal endpoints — require auth AND elevated permission
80
+ _TERMINAL_PATHS = {"/api/agent/terminal"}
81
+
82
+ # Admin keys can access terminal
83
+ _ADMIN_KEYS: set[str] = set()
84
+ _raw_admin = os.environ.get("WATSON_ADMIN_KEYS", "")
85
+ if _raw_admin:
86
+ _ADMIN_KEYS = {k.strip() for k in _raw_admin.split(",") if k.strip()}
87
+
88
+
89
+ def validate_api_key(key: str) -> bool:
90
+ if not _VALID_KEYS:
91
+ return True # No keys configured — allow all (dev mode)
92
+ return key in _VALID_KEYS
93
+
94
+
95
+ def validate_admin_key(key: str) -> bool:
96
+ if not _ADMIN_KEYS:
97
+ return False # No admin keys — deny terminal
98
+ return key in _ADMIN_KEYS
99
+
100
+
101
+ # ── Rate Limiting (token bucket per IP) ─────────────────────────
102
+
103
+ class TokenBucket:
104
+ def __init__(self, rate: float = 10.0, burst: int = 20):
105
+ self.rate = rate # tokens per second
106
+ self.burst = burst # max bucket size
107
+ self.tokens = float(burst)
108
+ self.last_refill = time.monotonic()
109
+
110
+ def consume(self, n: int = 1) -> bool:
111
+ now = time.monotonic()
112
+ elapsed = now - self.last_refill
113
+ self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
114
+ self.last_refill = now
115
+ if self.tokens >= n:
116
+ self.tokens -= n
117
+ return True
118
+ return False
119
+
120
+
121
+ _buckets: dict[str, TokenBucket] = {}
122
+ _bucket_lock = __import__('threading').Lock()
123
+
124
+ # Higher limits for investigation endpoints
125
+ _INVESTIGATE_RATE = float(os.environ.get("RATE_LIMIT_INVESTIGATE", "3")) # 3/sec
126
+ _CHAT_RATE = float(os.environ.get("RATE_LIMIT_CHAT", "10")) # 10/sec
127
+ _DEFAULT_RATE = float(os.environ.get("RATE_LIMIT_DEFAULT", "30")) # 30/sec
128
+
129
+
130
+ def _get_rate_for_path(path: str) -> float:
131
+ if "investigate" in path:
132
+ return _INVESTIGATE_RATE
133
+ if "chat" in path:
134
+ return _CHAT_RATE
135
+ return _DEFAULT_RATE
136
+
137
+
138
+ def check_rate_limit(ip: str, path: str) -> tuple[bool, dict]:
139
+ """Returns (allowed, headers_dict)."""
140
+ with _bucket_lock:
141
+ if ip not in _buckets:
142
+ _buckets[ip] = TokenBucket(rate=_DEFAULT_RATE)
143
+ bucket = _buckets[ip]
144
+ # Adjust rate if needed
145
+ target_rate = _get_rate_for_path(path)
146
+ if bucket.rate != target_rate:
147
+ bucket.rate = target_rate
148
+
149
+ allowed = bucket.consume()
150
+ remaining = int(bucket.tokens)
151
+ headers = {
152
+ "X-RateLimit-Limit": str(int(bucket.burst)),
153
+ "X-RateLimit-Remaining": str(remaining),
154
+ "X-RateLimit-Reset": str(int(time.time() + remaining / bucket.rate)),
155
+ }
156
+ return allowed, headers
157
+
158
+ # Cleanup old buckets periodically
159
+ _last_cleanup = time.monotonic()
160
+
161
+ def _cleanup_buckets():
162
+ global _last_cleanup
163
+ now = time.monotonic()
164
+ if now - _last_cleanup < 300: # Every 5 minutes
165
+ return
166
+ _last_cleanup = now
167
+ with _bucket_lock:
168
+ stale = [ip for ip, b in _buckets.items() if now - b.last_refill > 600]
169
+ for ip in stale:
170
+ del _buckets[ip]
171
+
172
+
173
+ # ── Combined Middleware ──────────────────────────────────────────
174
+
175
+ class EnterpriseMiddleware(BaseHTTPMiddleware):
176
+ """Auth + rate limiting + tracing + logging in one pass."""
177
+
178
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
179
+ corr_id = request.headers.get("X-Correlation-ID") or uuid.uuid4().hex[:12]
180
+ _correlation_id.set(corr_id)
181
+
182
+ start_time = time.monotonic()
183
+ ip = request.client.host if request.client else "unknown"
184
+ path = request.url.path
185
+
186
+ _cleanup_buckets()
187
+
188
+ # 1. Rate limit
189
+ allowed, rl_headers = check_rate_limit(ip, path)
190
+ if not allowed:
191
+ return JSONResponse(
192
+ {"error": "Rate limit exceeded", "retry_after": rl_headers["X-RateLimit-Reset"]},
193
+ status_code=429,
194
+ headers=rl_headers,
195
+ )
196
+
197
+ # 2. Auth (skip public paths)
198
+ if path not in _PUBLIC_PATHS and not path.startswith("/static"):
199
+ auth_header = request.headers.get("Authorization", "")
200
+ api_key = ""
201
+ if auth_header.startswith("Bearer "):
202
+ api_key = auth_header[7:]
203
+ elif auth_header.startswith("ApiKey "):
204
+ api_key = auth_header[7:]
205
+
206
+ if not validate_api_key(api_key):
207
+ logging.getLogger("watson").warning(
208
+ "auth_denied", extra={"ip": ip, "path": path}
209
+ )
210
+ return JSONResponse(
211
+ {"error": "Unauthorized — provide API key via Bearer token"},
212
+ status_code=401,
213
+ headers={"WWW-Authenticate": "Bearer"},
214
+ )
215
+
216
+ # Elevated: terminal requires admin key
217
+ if path in _TERMINAL_PATHS and not validate_admin_key(api_key):
218
+ return JSONResponse(
219
+ {"error": "Forbidden — admin key required for terminal access"},
220
+ status_code=403,
221
+ )
222
+
223
+ # 3. Process request
224
+ try:
225
+ response = await call_next(request)
226
+ except Exception as e:
227
+ logging.getLogger("watson").error(
228
+ "request_failed",
229
+ extra={"path": path, "method": request.method, "error": str(e)},
230
+ exc_info=True,
231
+ )
232
+ return JSONResponse(
233
+ {"error": "Internal server error", "correlation_id": corr_id},
234
+ status_code=500,
235
+ )
236
+
237
+ # 4. Add response headers
238
+ duration_ms = int((time.monotonic() - start_time) * 1000)
239
+ response.headers["X-Correlation-ID"] = corr_id
240
+ response.headers["X-Response-Time-Ms"] = str(duration_ms)
241
+ for k, v in rl_headers.items():
242
+ response.headers[k] = v
243
+
244
+ # 5. Structured access log
245
+ logging.getLogger("watson").info(
246
+ "request",
247
+ extra={
248
+ "method": request.method,
249
+ "path": path,
250
+ "status": response.status_code,
251
+ "duration_ms": duration_ms,
252
+ "ip": ip,
253
+ },
254
+ )
255
+
256
+ return response
257
+
258
+
259
+ # ── Graceful shutdown ────────────────────────────────────────────
260
+
261
+ import asyncio
262
+
263
+ _active_tasks: set[asyncio.Task] = set()
264
+
265
+ def register_task(task: asyncio.Task):
266
+ """Track a background task for graceful shutdown."""
267
+ _active_tasks.add(task)
268
+ task.add_done_callback(_active_tasks.discard)
269
+
270
+ async def shutdown_tasks(timeout: float = 10.0):
271
+ """Cancel all tracked tasks on shutdown."""
272
+ logger = logging.getLogger("watson")
273
+ logger.info("shutting_down", extra={"pending_tasks": len(_active_tasks)})
274
+ for task in list(_active_tasks):
275
+ task.cancel()
276
+ if _active_tasks:
277
+ done, pending = await asyncio.wait(_active_tasks, timeout=timeout)
278
+ if pending:
279
+ logger.warning("tasks_did_not_finish", extra={"count": len(pending)})
280
+
281
+
282
+ # ── Startup ──────────────────────────────────────────────────────
283
+
284
+ def init_app(app):
285
+ """Wire enterprise middleware into FastAPI app."""
286
+ setup_logging(os.environ.get("LOG_LEVEL", "INFO"))
287
+ app.add_middleware(EnterpriseMiddleware)
288
+
289
+ # Register shutdown handler
290
+ @app.on_event("shutdown")
291
+ async def on_shutdown():
292
+ await shutdown_tasks()
293
+
294
+ logger = logging.getLogger("watson")
295
+ logger.info("enterprise_middleware_initialized", extra={
296
+ "auth_enabled": bool(_VALID_KEYS),
297
+ "admin_keys_configured": bool(_ADMIN_KEYS),
298
+ "rate_limit_investigate": _INVESTIGATE_RATE,
299
+ "rate_limit_chat": _CHAT_RATE,
300
+ "rate_limit_default": _DEFAULT_RATE,
301
+ })