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,266 @@
1
+ """OSINT Framework integration — maps 1,417 OSINT tools to Watson entity types.
2
+
3
+ The OSINT Framework (https://osintframework.com) is a curated tree of OSINT
4
+ tools organized by investigation target (username, email, domain, IP, etc.).
5
+ Maltego's competitive advantage is its Transform Hub — a marketplace of paid
6
+ data connectors. OSINT Framework is the open-source equivalent.
7
+
8
+ This module:
9
+ 1. Loads the framework tree from cache (downloads on first use)
10
+ 2. Maps framework categories to Watson entity types
11
+ 3. Provides tool lookup by entity type (e.g., "what tools investigate domains?")
12
+ 4. Generates search URLs for discovered entities (enrichment transforms)
13
+
14
+ Integration: when the graph engine discovers a new entity (e.g., a domain),
15
+ it queries OSINT Framework for relevant investigation tools, then enriches
16
+ findings with direct links to those tools — compensating for Watson's lack
17
+ of direct database access.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
+ from .entities import EntityType
28
+
29
+ logger = logging.getLogger("watson.graph.osint_framework")
30
+
31
+ # Cache location
32
+ _CACHE_DIR = Path.home() / ".watson" / "cache"
33
+ _CACHE_FILE = _CACHE_DIR / "osint_framework.json"
34
+ _FRAMEWORK_URL = (
35
+ "https://raw.githubusercontent.com/lockfale/osint-framework/"
36
+ "master/public/arf.json"
37
+ )
38
+
39
+ # ── Category → EntityType mapping ─────────────────────────────────
40
+ # Maps OSINT Framework top-level categories to Watson entity types.
41
+ # When Watson discovers a Domain, it looks up the Domain category
42
+ # in the framework and gets all subcategory tools.
43
+
44
+ CATEGORY_ENTITY_MAP: dict[str, EntityType] = {
45
+ "Username": EntityType.PERSON,
46
+ "Email Address": EntityType.EMAIL,
47
+ "Domain Name": EntityType.DOMAIN,
48
+ "IP & MAC Address": EntityType.IP_ADDRESS,
49
+ "Images / Videos / Docs": EntityType.DOCUMENT,
50
+ "Social Networks": EntityType.PERSON,
51
+ "People Search Engines": EntityType.PERSON,
52
+ "Public Records": EntityType.PERSON,
53
+ "Business Records": EntityType.ORGANIZATION,
54
+ "Telephone Numbers": EntityType.PERSON,
55
+ "Geolocation Tools / Maps": EntityType.LOCATION,
56
+ "Cloud Infrastructure": EntityType.DOMAIN,
57
+ "Blockchain & Cryptocurrency": EntityType.ORGANIZATION,
58
+ "Dark Web": EntityType.DOCUMENT,
59
+ "Archives": EntityType.WEBSITE,
60
+ "Search Engines": EntityType.WEBSITE,
61
+ "Online Communities": EntityType.WEBSITE,
62
+ }
63
+
64
+
65
+ class OSINTFramework:
66
+ """Query interface to the OSINT Framework tree."""
67
+
68
+ def __init__(self):
69
+ self._tree: dict = {}
70
+ self._loaded = False
71
+ self._by_entity_type: dict[EntityType, list[dict]] = {}
72
+
73
+ # ── Loading ──────────────────────────────────────────────────
74
+
75
+ def _ensure_loaded(self) -> None:
76
+ """Load the framework tree from cache or download it."""
77
+ if self._loaded:
78
+ return
79
+
80
+ if _CACHE_FILE.exists():
81
+ try:
82
+ self._tree = json.loads(_CACHE_FILE.read_text())
83
+ self._loaded = True
84
+ self._build_index()
85
+ logger.info(
86
+ "osint_framework_loaded_from_cache: %s",
87
+ _CACHE_FILE,
88
+ )
89
+ return
90
+ except (json.JSONDecodeError, OSError):
91
+ logger.warning("osint_framework_cache_corrupt, re-downloading")
92
+
93
+ self._download()
94
+ self._loaded = True
95
+ self._build_index()
96
+
97
+ def _download(self) -> None:
98
+ """Download the framework data from GitHub."""
99
+ import urllib.request
100
+
101
+ logger.info("osint_framework_downloading: %s", _FRAMEWORK_URL)
102
+ try:
103
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
104
+ urllib.request.urlretrieve(_FRAMEWORK_URL, str(_CACHE_FILE))
105
+ self._tree = json.loads(_CACHE_FILE.read_text())
106
+ logger.info(
107
+ "osint_framework_downloaded: %d top-level nodes",
108
+ len(self._tree.get("children", [])),
109
+ )
110
+ except Exception as e:
111
+ logger.error("osint_framework_download_failed: %s", e)
112
+ self._tree = {"name": "OSINT Framework", "type": "folder", "children": []}
113
+
114
+ def _build_index(self) -> None:
115
+ """Build entity-type lookup index from the tree."""
116
+ self._by_entity_type = {}
117
+
118
+ root_children = self._tree.get("children", [])
119
+ for category in root_children:
120
+ cat_name = category.get("name", "")
121
+ entity_type = CATEGORY_ENTITY_MAP.get(cat_name)
122
+ if entity_type is None:
123
+ continue
124
+
125
+ tools = self._extract_tools(category)
126
+ if tools:
127
+ existing = self._by_entity_type.setdefault(entity_type, [])
128
+ existing.extend(tools)
129
+
130
+ # Log what we mapped
131
+ for et, tools in self._by_entity_type.items():
132
+ logger.info(
133
+ "osint_framework_mapped: %s → %d tools",
134
+ et.value,
135
+ len(tools),
136
+ )
137
+
138
+ def _extract_tools(self, node: dict, parent_category: str = "") -> list[dict]:
139
+ """Recursively extract tool entries from a framework node."""
140
+ tools: list[dict] = []
141
+
142
+ node_type = node.get("type", "")
143
+ name = node.get("name", "")
144
+ url = node.get("url", "")
145
+
146
+ if node_type != "folder" and url:
147
+ # This is a leaf tool entry
148
+ tools.append({
149
+ "name": name,
150
+ "url": url,
151
+ "category": parent_category or name,
152
+ })
153
+
154
+ for child in node.get("children", []):
155
+ child_category = name if node_type == "folder" else parent_category
156
+ tools.extend(self._extract_tools(child, child_category))
157
+
158
+ return tools
159
+
160
+ # ── Query API ────────────────────────────────────────────────
161
+
162
+ def get_tools_for_entity(self, entity_type: EntityType) -> list[dict]:
163
+ """Return all OSINT Framework tools relevant to an entity type.
164
+
165
+ Example:
166
+ >>> framework.get_tools_for_entity(EntityType.DOMAIN)
167
+ [
168
+ {"name": "Domain Dossier", "url": "https://centralops.net/...",
169
+ "category": "Whois Records"},
170
+ {"name": "Shodan", "url": "https://www.shodan.io/",
171
+ "category": "Discovery"},
172
+ ...
173
+ ]
174
+ """
175
+ self._ensure_loaded()
176
+ return self._by_entity_type.get(entity_type, [])
177
+
178
+ def get_search_urls(
179
+ self,
180
+ entity_type: EntityType,
181
+ value: str,
182
+ max_results: int = 10,
183
+ ) -> list[dict]:
184
+ """Generate ready-to-use search URLs for an entity.
185
+
186
+ Takes OSINT Framework tools and interpolates the entity value
187
+ into their search URL templates (where the tool supports it).
188
+ For tools that don't have search URL templates, returns the
189
+ tool homepage as a reference link.
190
+
191
+ Returns list of {"tool": str, "url": str, "category": str}.
192
+ """
193
+ self._ensure_loaded()
194
+ tools = self.get_tools_for_entity(entity_type)
195
+ results: list[dict] = []
196
+
197
+ for tool in tools[:max_results]:
198
+ url = tool["url"]
199
+ name = tool["name"]
200
+
201
+ # Interpolate search parameters where supported
202
+ if "<%3C" in url or "%3C" in url:
203
+ # Has a template parameter — interpolate the entity value
204
+ url = url.replace("<%3Cusername%3E>", value)
205
+ url = url.replace("<%3Cdomain%3E>", value)
206
+ url = url.replace("<%3Cemail%3E>", value)
207
+ url = url.replace("<%3Cip%3E>", value)
208
+
209
+ results.append({
210
+ "tool": name,
211
+ "url": url,
212
+ "category": tool.get("category", ""),
213
+ })
214
+
215
+ return results
216
+
217
+ def get_categories(self) -> list[str]:
218
+ """Return all top-level OSINT Framework categories."""
219
+ self._ensure_loaded()
220
+ return [
221
+ c.get("name", "")
222
+ for c in self._tree.get("children", [])
223
+ ]
224
+
225
+ def search_tools(self, keyword: str, limit: int = 20) -> list[dict]:
226
+ """Search all tools by keyword (case-insensitive)."""
227
+ self._ensure_loaded()
228
+ keyword_lower = keyword.lower()
229
+ results: list[dict] = []
230
+
231
+ def _search(node):
232
+ name = node.get("name", "")
233
+ if keyword_lower in name.lower():
234
+ url = node.get("url", "")
235
+ if url:
236
+ results.append({"name": name, "url": url})
237
+ for child in node.get("children", []):
238
+ _search(child)
239
+
240
+ _search(self._tree)
241
+ return results[:limit]
242
+
243
+ @property
244
+ def total_tools(self) -> int:
245
+ """Total number of tool entries in the framework."""
246
+ self._ensure_loaded()
247
+
248
+ def _count(node):
249
+ if node.get("type") != "folder" and node.get("url"):
250
+ return 1
251
+ return sum(_count(c) for c in node.get("children", []))
252
+
253
+ return _count(self._tree)
254
+
255
+
256
+ # ── Singleton ─────────────────────────────────────────────────────
257
+
258
+ _framework_instance: Optional[OSINTFramework] = None
259
+
260
+
261
+ def get_framework() -> OSINTFramework:
262
+ """Get or create the singleton OSINT Framework instance."""
263
+ global _framework_instance
264
+ if _framework_instance is None:
265
+ _framework_instance = OSINTFramework()
266
+ return _framework_instance
@@ -0,0 +1,116 @@
1
+ """Typed relationships for the Watson entity graph.
2
+
3
+ Each relationship connects two entities and carries metadata about how
4
+ the connection was discovered (source transform, confidence, timestamp).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from enum import Enum
12
+ from typing import ClassVar
13
+
14
+
15
+ class RelationshipType(str, Enum):
16
+ """Standard OSINT relationship types — mirrors Maltego's link labels."""
17
+ # Infrastructure
18
+ RESOLVES_TO = "resolves_to" # Domain → IPAddress
19
+ HAS_SUBDOMAIN = "has_subdomain" # Domain → Domain
20
+ HOSTED_ON = "hosted_on" # Website → IPAddress
21
+ USES_NAMESERVER = "uses_nameserver" # Domain → Domain (NS record)
22
+
23
+ # People & Organizations
24
+ HAS_EMAIL = "has_email" # Domain/Person → Email
25
+ WORKS_AT = "works_at" # Person → Organization
26
+ OWNS = "owns" # Person → Domain/Website
27
+ CONTACT_FOR = "contact_for" # Email → Organization (role-based)
28
+
29
+ # Location
30
+ LOCATED_IN = "located_in" # IPAddress → Location
31
+ BASED_IN = "based_in" # Organization → Location
32
+ REGISTERED_IN = "registered_in" # Domain → Location
33
+
34
+ # Content
35
+ MENTIONS = "mentions" # Website/Document → Entity
36
+ CONTAINS = "contains" # Document → Entity (extracted)
37
+ REFERENCES = "references" # Website → Website/Document
38
+
39
+ # Generic
40
+ RELATED_TO = "related_to" # Any → Any (fallback)
41
+
42
+
43
+ @dataclass
44
+ class Relationship:
45
+ """A directed, typed link between two entities."""
46
+
47
+ source_id: str # Entity ID of the source node
48
+ target_id: str # Entity ID of the target node
49
+ rel_type: RelationshipType
50
+ confidence: float = field(default=0.7, metadata={"ge": 0.0, "le": 1.0})
51
+ source_transform: str = "" # Which transform created this
52
+ evidence: list[str] = field(default_factory=list) # Source URLs/proof
53
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
54
+ properties: dict = field(default_factory=dict)
55
+
56
+ @property
57
+ def id(self) -> str:
58
+ """Unique relationship ID."""
59
+ raw = f"{self.source_id}:{self.rel_type.value}:{self.target_id}"
60
+ import hashlib
61
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
62
+
63
+ def __hash__(self) -> int:
64
+ return hash(self.id)
65
+
66
+ def __eq__(self, other: object) -> bool:
67
+ if not isinstance(other, Relationship):
68
+ return False
69
+ return self.id == other.id
70
+
71
+ def to_dict(self) -> dict:
72
+ return {
73
+ "id": self.id,
74
+ "source_id": self.source_id,
75
+ "target_id": self.target_id,
76
+ "rel_type": self.rel_type.value,
77
+ "confidence": self.confidence,
78
+ "source_transform": self.source_transform,
79
+ "evidence": self.evidence,
80
+ "properties": self.properties,
81
+ }
82
+
83
+ def __repr__(self) -> str:
84
+ return (
85
+ f"Relationship({self.source_id[:8]} "
86
+ f"-{self.rel_type.value}→ "
87
+ f"{self.target_id[:8]})"
88
+ )
89
+
90
+
91
+ # ── Shorthand aliases for readability ─────────────────────────────
92
+
93
+ # Infrastructure
94
+ RESOLVES_TO = RelationshipType.RESOLVES_TO
95
+ HAS_SUBDOMAIN = RelationshipType.HAS_SUBDOMAIN
96
+ HOSTED_ON = RelationshipType.HOSTED_ON
97
+ USES_NAMESERVER = RelationshipType.USES_NAMESERVER
98
+
99
+ # People & Organizations
100
+ HAS_EMAIL = RelationshipType.HAS_EMAIL
101
+ WORKS_AT = RelationshipType.WORKS_AT
102
+ OWNS = RelationshipType.OWNS
103
+ CONTACT_FOR = RelationshipType.CONTACT_FOR
104
+
105
+ # Location
106
+ LOCATED_IN = RelationshipType.LOCATED_IN
107
+ BASED_IN = RelationshipType.BASED_IN
108
+ REGISTERED_IN = RelationshipType.REGISTERED_IN
109
+
110
+ # Content
111
+ MENTIONS = RelationshipType.MENTIONS
112
+ CONTAINS = RelationshipType.CONTAINS
113
+ REFERENCES = RelationshipType.REFERENCES
114
+
115
+ # Generic
116
+ RELATED_TO = RelationshipType.RELATED_TO