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,451 @@
1
+ """STIX 2.1 Serializer — convert Watson findings to structured threat intelligence.
2
+
3
+ Produces valid STIX 2.1 bundles with:
4
+ - Report → SDO with object_refs
5
+ - Finding → Indicator (pattern-based) + ObservedData
6
+ - Entity → Identity (individual/organization), Location, DomainName, IPv4Addr
7
+ - Relationships between all objects
8
+ - External references for source URLs
9
+ - Confidence mapping: CONFIRMED→85, PROBABLE→65, POSSIBLE→40, UNLIKELY→15, UNVERIFIED→0
10
+
11
+ Reference: https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import re
18
+ import uuid
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ import logging
24
+
25
+ logger = logging.getLogger("watson.stix")
26
+
27
+ # ── Confidence mapping ────────────────────────────────────────
28
+
29
+ _TIER_CONFIDENCE: dict[str, int] = {
30
+ "CONFIRMED": 85,
31
+ "PROBABLE": 65,
32
+ "POSSIBLE": 40,
33
+ "UNLIKELY": 15,
34
+ "UNVERIFIED": 0,
35
+ "UNSUBSTANTIATED": 0,
36
+ }
37
+
38
+ # ── Entity type → STIX identity_class ─────────────────────────
39
+
40
+ _ENTITY_STIX_CLASS: dict[str, str] = {
41
+ "person": "individual",
42
+ "organization": "organization",
43
+ "company": "organization",
44
+ "government": "organization",
45
+ "unknown": "unknown",
46
+ "": "unknown",
47
+ }
48
+
49
+ # ── STIX ID generation ────────────────────────────────────────
50
+
51
+ def _stix_id(stix_type: str, value: str = "") -> str:
52
+ """Generate deterministic STIX ID from type + value, or random UUID."""
53
+ if value:
54
+ # Namespace-based UUID for reproducibility
55
+ ns = uuid.uuid5(uuid.NAMESPACE_DNS, f"watson-osint:{stix_type}")
56
+ return f"{stix_type}--{uuid.uuid5(ns, value)}"
57
+ return f"{stix_type}--{uuid.uuid4()}"
58
+
59
+
60
+ # ── IP regex ──────────────────────────────────────────────────
61
+
62
+ _IPV4_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
63
+ _IPV6_RE = re.compile(r"^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$")
64
+
65
+
66
+ def _is_ip(value: str) -> str | None:
67
+ """Return 'ipv4', 'ipv6', or None."""
68
+ v = value.strip()
69
+ if _IPV4_RE.match(v):
70
+ return "ipv4-addr"
71
+ if _IPV6_RE.match(v):
72
+ return "ipv6-addr"
73
+ return None
74
+
75
+
76
+ def _is_domain(value: str) -> bool:
77
+ """Rough domain detection."""
78
+ v = value.strip().lower()
79
+ if not v or " " in v or len(v) > 253:
80
+ return False
81
+ return "." in v and not v.startswith(("http://", "https://"))
82
+
83
+
84
+ def _is_email(value: str) -> bool:
85
+ return bool(re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", value.strip()))
86
+
87
+
88
+ def _is_url(value: str) -> bool:
89
+ return value.strip().startswith(("http://", "https://"))
90
+
91
+
92
+ # ── Main serializer ───────────────────────────────────────────
93
+
94
+ class STIXSerializer:
95
+ """Convert Watson investigation results to STIX 2.1 bundles."""
96
+
97
+ def __init__(self):
98
+ self._objects: list[dict[str, Any]] = []
99
+ self._seen_ids: set[str] = set()
100
+ self._report_refs: list[str] = []
101
+ self._entity_cache: dict[str, str] = {} # value → stix_id
102
+
103
+ def serialize(
104
+ self,
105
+ query: str,
106
+ case_id: str,
107
+ target_type: str,
108
+ findings: list[Any],
109
+ entities: list[dict] | None = None,
110
+ brief: dict | None = None,
111
+ ) -> dict[str, Any]:
112
+ """Produce a STIX 2.1 bundle from investigation results."""
113
+ self._objects = []
114
+ self._seen_ids = set()
115
+ self._report_refs = []
116
+ self._entity_cache = {}
117
+
118
+ now = datetime.now(timezone.utc).isoformat()
119
+
120
+ # ── 1. Report SDO ──
121
+ report_id = _stix_id("report", case_id)
122
+ report = {
123
+ "type": "report",
124
+ "id": report_id,
125
+ "spec_version": "2.1",
126
+ "created": now,
127
+ "modified": now,
128
+ "name": f"Watson Investigation: {query[:80]}",
129
+ "description": brief.get("executive_summary", "") if brief else f"OSINT investigation of {query}",
130
+ "report_types": ["investigation"],
131
+ "published": now,
132
+ "object_refs": self._report_refs, # will be filled
133
+ "labels": ["osint", target_type],
134
+ "external_references": [],
135
+ }
136
+ self._add_object(report)
137
+
138
+ # ── 2. Target identity ──
139
+ target_id = self._entity_for(query, target_type, brief)
140
+ self._report_refs.append(target_id)
141
+ self._relate(report_id, "investigates", target_id)
142
+
143
+ # ── 3. Findings → Indicators & ObservedData ──
144
+ for f in findings:
145
+ self._serialize_finding(f, report_id, target_id)
146
+
147
+ # ── 4. Entities → Identities, Locations, SCOs ──
148
+ if entities:
149
+ for ent in entities:
150
+ ent_id = self._serialize_entity(ent)
151
+ if ent_id:
152
+ self._report_refs.append(ent_id)
153
+ self._relate(report_id, "references", ent_id)
154
+
155
+ # ── 5. Fix up report object_refs ──
156
+ report["object_refs"] = self._report_refs
157
+
158
+ bundle = {
159
+ "type": "bundle",
160
+ "id": _stix_id("bundle", case_id),
161
+ "spec_version": "2.1",
162
+ "objects": self._objects,
163
+ }
164
+ return bundle
165
+
166
+ def to_json(self, **kwargs) -> str:
167
+ bundle = self.serialize(**kwargs)
168
+ return json.dumps(bundle, indent=2, default=str)
169
+
170
+ def to_file(self, path: Path | str, **kwargs) -> Path:
171
+ path = Path(path)
172
+ path.write_text(self.to_json(**kwargs))
173
+ logger.info("stix_exported: %s", path)
174
+ return path
175
+
176
+ # ── Internal ──────────────────────────────────────────────
177
+
178
+ def _add_object(self, obj: dict) -> None:
179
+ oid = obj.get("id", "")
180
+ if oid and oid not in self._seen_ids:
181
+ self._seen_ids.add(oid)
182
+ self._objects.append(obj)
183
+
184
+ def _relate(self, source_id: str, rel_type: str, target_id: str) -> str | None:
185
+ if not source_id or not target_id:
186
+ return None
187
+ rid = _stix_id("relationship", f"{source_id}|{rel_type}|{target_id}")
188
+ rel = {
189
+ "type": "relationship",
190
+ "id": rid,
191
+ "spec_version": "2.1",
192
+ "created": datetime.now(timezone.utc).isoformat(),
193
+ "modified": datetime.now(timezone.utc).isoformat(),
194
+ "relationship_type": rel_type,
195
+ "source_ref": source_id,
196
+ "target_ref": target_id,
197
+ }
198
+ self._add_object(rel)
199
+ return rid
200
+
201
+ def _confidence(self, tier: str) -> int:
202
+ return _TIER_CONFIDENCE.get(tier, 30)
203
+
204
+ def _entity_for(self, value: str, etype: str = "unknown", brief: dict | None = None) -> str:
205
+ """Get or create a STIX identity for an entity value."""
206
+ key = f"{value}|{etype}"
207
+ if key in self._entity_cache:
208
+ return self._entity_cache[key]
209
+
210
+ identity_class = _ENTITY_STIX_CLASS.get(etype, "unknown")
211
+ eid = _stix_id("identity", value)
212
+
213
+ identity = {
214
+ "type": "identity",
215
+ "id": eid,
216
+ "spec_version": "2.1",
217
+ "created": datetime.now(timezone.utc).isoformat(),
218
+ "modified": datetime.now(timezone.utc).isoformat(),
219
+ "name": value[:200],
220
+ "identity_class": identity_class,
221
+ "description": f"Target entity of type: {etype}",
222
+ }
223
+
224
+ # Add sectors/labels from brief if available
225
+ if brief and identity_class == "organization":
226
+ risk_themes = brief.get("risk_themes", [])
227
+ if risk_themes:
228
+ identity["sectors"] = [t.get("theme", "")[:80] for t in risk_themes[:3]]
229
+ identity["labels"] = ["risk-associated"] + [
230
+ t.get("severity", "").lower() for t in risk_themes if t.get("severity")
231
+ ]
232
+
233
+ self._add_object(identity)
234
+ self._entity_cache[key] = eid
235
+ return eid
236
+
237
+ def _serialize_finding(self, finding: Any, report_id: str, target_id: str) -> None:
238
+ """Convert a Watson Finding to STIX Indicator + ObservedData."""
239
+ title = getattr(finding, "title", str(finding))[:200]
240
+ desc = getattr(finding, "description", "")[:500]
241
+ tier = getattr(finding, "tier", "UNVERIFIED")
242
+ source_url = getattr(finding, "source_url", "")
243
+ src_type = getattr(finding, "source_type", "osint")
244
+ confidence_val = self._confidence(tier) if tier else 30
245
+ fid = _stix_id("indicator", getattr(finding, "id", str(uuid.uuid4())))
246
+
247
+ # ── Indicator (pattern-based) ──
248
+ # Build a STIX pattern from finding text
249
+ pattern = self._build_pattern(title, desc)
250
+ indicator = {
251
+ "type": "indicator",
252
+ "id": fid,
253
+ "spec_version": "2.1",
254
+ "created": datetime.now(timezone.utc).isoformat(),
255
+ "modified": datetime.now(timezone.utc).isoformat(),
256
+ "name": title,
257
+ "description": desc or title,
258
+ "indicator_types": ["osint-report"],
259
+ "pattern": pattern,
260
+ "pattern_type": "stix",
261
+ "valid_from": datetime.now(timezone.utc).isoformat(),
262
+ "confidence": confidence_val,
263
+ "labels": [src_type, tier.lower() if tier else "unverified"],
264
+ }
265
+ if source_url:
266
+ indicator["external_references"] = [{
267
+ "source_name": "OSINT Source",
268
+ "url": source_url,
269
+ "description": "Original investigation source",
270
+ }]
271
+ self._add_object(indicator)
272
+ self._report_refs.append(fid)
273
+ self._relate(fid, "derived-from", target_id)
274
+
275
+ # ── ObservedData (structured facts) ──
276
+ # Extract IPs, domains, emails from the finding text
277
+ observables = self._extract_observables(desc)
278
+ if observables:
279
+ odata_id = _stix_id("observed-data", fid)
280
+ observed = {
281
+ "type": "observed-data",
282
+ "id": odata_id,
283
+ "spec_version": "2.1",
284
+ "created": datetime.now(timezone.utc).isoformat(),
285
+ "modified": datetime.now(timezone.utc).isoformat(),
286
+ "first_observed": datetime.now(timezone.utc).isoformat(),
287
+ "last_observed": datetime.now(timezone.utc).isoformat(),
288
+ "number_observed": 1,
289
+ "objects": observables,
290
+ }
291
+ self._add_object(observed)
292
+ self._report_refs.append(odata_id)
293
+ self._relate(fid, "based-on", odata_id)
294
+
295
+ def _serialize_entity(self, entity: dict | Any) -> str | None:
296
+ """Serialize a Watson entity to STIX."""
297
+ if isinstance(entity, dict):
298
+ val = entity.get("value", entity.get("name", entity.get("canonical", "")))
299
+ etype = entity.get("type", "unknown")
300
+ elif hasattr(entity, "value"):
301
+ val = getattr(entity, "value", "") or ""
302
+ etype = getattr(entity, "type", "unknown")
303
+ else:
304
+ val = str(entity)
305
+ etype = "unknown"
306
+
307
+ if not val or not val.strip():
308
+ return None
309
+
310
+ v = val.strip()
311
+
312
+ # IP address → IPv4Addr / IPv6Addr SCO
313
+ ip_type = _is_ip(v)
314
+ if ip_type:
315
+ ip_id = _stix_id(ip_type, v)
316
+ ip_obj = {
317
+ "type": ip_type,
318
+ "id": ip_id,
319
+ "spec_version": "2.1",
320
+ "value": v,
321
+ }
322
+ self._add_object(ip_obj)
323
+ return ip_id
324
+
325
+ # Domain → DomainName SCO
326
+ if _is_domain(v):
327
+ dom_id = _stix_id("domain-name", v)
328
+ domain = {
329
+ "type": "domain-name",
330
+ "id": dom_id,
331
+ "spec_version": "2.1",
332
+ "value": v,
333
+ }
334
+ self._add_object(domain)
335
+ return dom_id
336
+
337
+ # Email → EmailAddr SCO
338
+ if _is_email(v):
339
+ email_id = _stix_id("email-addr", v)
340
+ email_obj = {
341
+ "type": "email-addr",
342
+ "id": email_id,
343
+ "spec_version": "2.1",
344
+ "value": v,
345
+ }
346
+ self._add_object(email_obj)
347
+ return email_id
348
+
349
+ # URL → URL SCO
350
+ if _is_url(v):
351
+ url_id = _stix_id("url", v)
352
+ url_obj = {
353
+ "type": "url",
354
+ "id": url_id,
355
+ "spec_version": "2.1",
356
+ "value": v,
357
+ }
358
+ self._add_object(url_obj)
359
+ return url_id
360
+
361
+ # Default → Identity
362
+ return self._entity_for(v, etype)
363
+
364
+ def _build_pattern(self, title: str, desc: str) -> str:
365
+ """Build a STIX pattern string from finding text."""
366
+ # Extract first significant noun phrase
367
+ text = f"{title} {desc}"[:500]
368
+ # Simple: mark as osint-report indicator with description
369
+ return f"[indicator:pattern_type = 'osint-report' AND indicator:description MATCHES '{self._escape_pattern(desc[:100])}']"
370
+
371
+ @staticmethod
372
+ def _escape_pattern(text: str) -> str:
373
+ """Escape single quotes for STIX pattern."""
374
+ return text.replace("'", "\\'").replace("\n", " ")[:200]
375
+
376
+ def _extract_observables(self, text: str) -> dict[str, dict]:
377
+ """Extract IPs, domains, emails from text as STIX Cyber Observable objects."""
378
+ objects: dict[str, dict] = {}
379
+ if not text:
380
+ return objects
381
+
382
+ # Extract IPs
383
+ for ip_match in re.finditer(_IPV4_RE, text):
384
+ ip = ip_match.group(0)
385
+ if ip not in objects:
386
+ objects[f"0"] = {
387
+ "type": "ipv4-addr",
388
+ "value": ip,
389
+ }
390
+ break # just one per finding
391
+
392
+ # Extract domains (rough)
393
+ domain_match = re.search(r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}', text)
394
+ if domain_match:
395
+ dom = domain_match.group(0).lower()
396
+ if dom not in objects:
397
+ key = f"{len(objects)}"
398
+ objects[key] = {
399
+ "type": "domain-name",
400
+ "value": dom,
401
+ }
402
+
403
+ # Extract emails
404
+ email_match = re.search(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
405
+ if email_match:
406
+ email = email_match.group(0)
407
+ key = f"{len(objects)}"
408
+ objects[key] = {
409
+ "type": "email-addr",
410
+ "value": email,
411
+ }
412
+
413
+ return objects
414
+
415
+
416
+ # ── Convenience export function ───────────────────────────────
417
+
418
+ def export_stix(
419
+ query: str,
420
+ case_id: str,
421
+ target_type: str,
422
+ findings: list[Any],
423
+ entities: list[dict] | None = None,
424
+ brief: dict | None = None,
425
+ output_path: Path | str | None = None,
426
+ ) -> tuple[dict, Path | None]:
427
+ """Export investigation results as STIX 2.1.
428
+
429
+ Returns (bundle_dict, output_path_or_None).
430
+ """
431
+ serializer = STIXSerializer()
432
+ bundle = serializer.serialize(
433
+ query=query,
434
+ case_id=case_id,
435
+ target_type=target_type,
436
+ findings=findings,
437
+ entities=entities,
438
+ brief=brief,
439
+ )
440
+ path = None
441
+ if output_path:
442
+ path = serializer.to_file(
443
+ output_path,
444
+ query=query,
445
+ case_id=case_id,
446
+ target_type=target_type,
447
+ findings=findings,
448
+ entities=entities,
449
+ brief=brief,
450
+ )
451
+ return bundle, path