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.
- osintengine-1.0.2.dist-info/METADATA +311 -0
- osintengine-1.0.2.dist-info/RECORD +103 -0
- osintengine-1.0.2.dist-info/WHEEL +5 -0
- osintengine-1.0.2.dist-info/entry_points.txt +2 -0
- osintengine-1.0.2.dist-info/licenses/LICENSE +202 -0
- osintengine-1.0.2.dist-info/top_level.txt +2 -0
- src/watson/__init__.py +10 -0
- src/watson/agent/__init__.py +96 -0
- src/watson/agents/__init__.py +101 -0
- src/watson/agents/protocol.py +196 -0
- src/watson/auth/__init__.py +68 -0
- src/watson/auth/store.py +145 -0
- src/watson/conversation.py +68 -0
- src/watson/core/__init__.py +0 -0
- src/watson/core/models.py +96 -0
- src/watson/ethics.py +205 -0
- src/watson/exports.py +182 -0
- src/watson/graph/__init__.py +69 -0
- src/watson/graph/entities.py +207 -0
- src/watson/graph/graph.py +489 -0
- src/watson/graph/osint_framework.py +266 -0
- src/watson/graph/relationships.py +116 -0
- src/watson/graph/transforms.py +787 -0
- src/watson/infra/__init__.py +43 -0
- src/watson/infra/cache.py +171 -0
- src/watson/infra/ratelimit.py +125 -0
- src/watson/infra/resilience.py +239 -0
- src/watson/infra/retry.py +186 -0
- src/watson/metrics.py +128 -0
- src/watson/opsec/__init__.py +290 -0
- src/watson/orchestration/__init__.py +23 -0
- src/watson/orchestration/engine.py +5587 -0
- src/watson/orchestration/executor.py +96 -0
- src/watson/orchestration/intent.py +139 -0
- src/watson/orchestration/intent_classifier.py +45 -0
- src/watson/orchestration/llm_config.py +160 -0
- src/watson/orchestration/resolution.py +555 -0
- src/watson/orchestration/scraper.py +94 -0
- src/watson/orchestration/synthesis.py +726 -0
- src/watson/orchestration/target_profile.py +748 -0
- src/watson/persistence/__init__.py +18 -0
- src/watson/persistence/models.py +161 -0
- src/watson/persistence/store.py +230 -0
- src/watson/pipeline/__init__.py +16 -0
- src/watson/pipeline/pre_synthesis.py +621 -0
- src/watson/search.py +103 -0
- src/watson/serializers/stix.py +451 -0
- src/watson/tools/__init__.py +31 -0
- src/watson/tools/base.py +97 -0
- src/watson/tools/blockchain.py +359 -0
- src/watson/tools/captcha.py +276 -0
- src/watson/tools/conflict.py +107 -0
- src/watson/tools/corporate.py +287 -0
- src/watson/tools/darkweb.py +144 -0
- src/watson/tools/geolocation.py +347 -0
- src/watson/tools/image_video.py +108 -0
- src/watson/tools/marinetraffic.py +142 -0
- src/watson/tools/people.py +476 -0
- src/watson/tools/registry.py +56 -0
- src/watson/tools/satellite.py +118 -0
- src/watson/tools/scraper.py +745 -0
- src/watson/tools/shodan.py +129 -0
- src/watson/tools/social_media.py +160 -0
- src/watson/tools/websites.py +291 -0
- src/watson/tools/wikidata.py +398 -0
- src/watson/utils/__init__.py +1 -0
- src/watson/utils/helpers.py +49 -0
- src/watson/utils/http.py +119 -0
- src/watson/verification/__init__.py +245 -0
- watson/__init__.py +6 -0
- watson/agents/__init__.py +20 -0
- watson/agents/base.py +103 -0
- watson/agents/direct.py +225 -0
- watson/agents/hermes.py +275 -0
- watson/agents/hermes_mcp.py +292 -0
- watson/api_keys.py +176 -0
- watson/browser_scraper.py +481 -0
- watson/cli.py +836 -0
- watson/crossref.py +175 -0
- watson/ethics.py +20 -0
- watson/graph.py +406 -0
- watson/mcp_server.py +601 -0
- watson/memory.py +552 -0
- watson/neo4j_graph.py +124 -0
- watson/opsec/__init__.py +307 -0
- watson/reporter.py +537 -0
- watson/scheduler.py +486 -0
- watson/serializers/stix.py +451 -0
- watson/terminal.py +410 -0
- watson/toolkit.py +252 -0
- watson/toolkit_api.py +347 -0
- watson/toolkit_automation.py +95 -0
- watson/toolkit_registry.py +345 -0
- watson/verification/__init__.py +251 -0
- watson/web/__init__.py +1 -0
- watson/web/app.py +1650 -0
- watson/web/middleware.py +301 -0
- watson/web/static/assets/index-B7hPOc0z.js +278 -0
- watson/web/static/assets/index-CJ6FP8Mp.css +1 -0
- watson/web/static/assets/watson-logo-Dk9tawHb.png +0 -0
- watson/web/static/index.html +14 -0
- watson/web/templates/chat.html +2 -0
- watson/web/templates/investigation-map.html +429 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Typed OSINT entities for the Watson entity graph.
|
|
2
|
+
|
|
3
|
+
Each entity type corresponds to a real-world OSINT artifact that can be
|
|
4
|
+
discovered, enriched, and pivoted from.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, ClassVar, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EntityType(str, Enum):
|
|
17
|
+
"""Core entity types — maps to Maltego's entity palette."""
|
|
18
|
+
DOMAIN = "domain"
|
|
19
|
+
IP_ADDRESS = "ip_address"
|
|
20
|
+
EMAIL = "email"
|
|
21
|
+
PERSON = "person"
|
|
22
|
+
ORGANIZATION = "organization"
|
|
23
|
+
WEBSITE = "website"
|
|
24
|
+
LOCATION = "location"
|
|
25
|
+
DOCUMENT = "document"
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Entity:
|
|
31
|
+
"""Base entity — all entities have a unique ID derived from their value + type."""
|
|
32
|
+
|
|
33
|
+
entity_type: EntityType = EntityType.UNKNOWN
|
|
34
|
+
value: str = "" # Canonical string representation (lowercased, normalized)
|
|
35
|
+
display_name: str = "" # Human-readable label
|
|
36
|
+
confidence: float = field(default=0.8, metadata={"ge": 0.0, "le": 1.0})
|
|
37
|
+
source: str = "" # Transform name or tool that discovered this
|
|
38
|
+
discovered_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
39
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
# Subclasses override this to set their entity_type
|
|
42
|
+
_entity_type: ClassVar[EntityType] = EntityType.UNKNOWN
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
if not self.display_name:
|
|
46
|
+
self.display_name = self.value
|
|
47
|
+
# Set entity_type from class variable if not explicitly passed
|
|
48
|
+
if self.entity_type == EntityType.UNKNOWN and self._entity_type != EntityType.UNKNOWN:
|
|
49
|
+
self.entity_type = self._entity_type
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def id(self) -> str:
|
|
53
|
+
"""Deterministic entity ID — same value+type always produces same ID."""
|
|
54
|
+
raw = f"{self.entity_type.value}:{self.value}"
|
|
55
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def label(self) -> str:
|
|
59
|
+
"""Short label for graph display."""
|
|
60
|
+
return f"{self.entity_type.value}:{self.display_name[:40]}"
|
|
61
|
+
|
|
62
|
+
def __hash__(self) -> int:
|
|
63
|
+
return hash(self.id)
|
|
64
|
+
|
|
65
|
+
def __eq__(self, other: object) -> bool:
|
|
66
|
+
if not isinstance(other, Entity):
|
|
67
|
+
return False
|
|
68
|
+
return self.id == other.id
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict:
|
|
71
|
+
return {
|
|
72
|
+
"id": self.id,
|
|
73
|
+
"entity_type": self.entity_type.value,
|
|
74
|
+
"value": self.value,
|
|
75
|
+
"display_name": self.display_name,
|
|
76
|
+
"confidence": self.confidence,
|
|
77
|
+
"source": self.source,
|
|
78
|
+
"properties": self.properties,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
return f"{self.__class__.__name__}(value={self.value!r}, conf={self.confidence:.2f})"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Concrete entity types ─────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(eq=False)
|
|
89
|
+
class Domain(Entity):
|
|
90
|
+
"""A DNS domain name."""
|
|
91
|
+
_entity_type: ClassVar[EntityType] = EntityType.DOMAIN
|
|
92
|
+
tld: str = ""
|
|
93
|
+
registrar: str = ""
|
|
94
|
+
|
|
95
|
+
def __post_init__(self):
|
|
96
|
+
super().__post_init__()
|
|
97
|
+
self.value = self.value.lower().rstrip(".")
|
|
98
|
+
if not self.tld and "." in self.value:
|
|
99
|
+
self.tld = self.value.rsplit(".", 1)[-1]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(eq=False)
|
|
103
|
+
class IPAddress(Entity):
|
|
104
|
+
"""An IPv4 or IPv6 address."""
|
|
105
|
+
_entity_type: ClassVar[EntityType] = EntityType.IP_ADDRESS
|
|
106
|
+
is_ipv6: bool = False
|
|
107
|
+
asn: str = ""
|
|
108
|
+
isp: str = ""
|
|
109
|
+
|
|
110
|
+
def __post_init__(self):
|
|
111
|
+
super().__post_init__()
|
|
112
|
+
self.is_ipv6 = ":" in self.value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(eq=False)
|
|
116
|
+
class Email(Entity):
|
|
117
|
+
"""An email address."""
|
|
118
|
+
_entity_type: ClassVar[EntityType] = EntityType.EMAIL
|
|
119
|
+
local_part: str = ""
|
|
120
|
+
domain: str = ""
|
|
121
|
+
|
|
122
|
+
def __post_init__(self):
|
|
123
|
+
super().__post_init__()
|
|
124
|
+
self.value = self.value.lower()
|
|
125
|
+
if "@" in self.value and not self.local_part:
|
|
126
|
+
self.local_part, self.domain = self.value.split("@", 1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(eq=False)
|
|
130
|
+
class Person(Entity):
|
|
131
|
+
"""A person (name or identity)."""
|
|
132
|
+
_entity_type: ClassVar[EntityType] = EntityType.PERSON
|
|
133
|
+
given_name: str = ""
|
|
134
|
+
surname: str = ""
|
|
135
|
+
|
|
136
|
+
def __post_init__(self):
|
|
137
|
+
super().__post_init__()
|
|
138
|
+
if not self.given_name and not self.surname:
|
|
139
|
+
parts = self.value.split()
|
|
140
|
+
if len(parts) >= 2:
|
|
141
|
+
self.given_name = parts[-1] # Western order assumption
|
|
142
|
+
self.surname = parts[0]
|
|
143
|
+
elif len(parts) == 1:
|
|
144
|
+
self.surname = parts[0]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(eq=False)
|
|
148
|
+
class Organization(Entity):
|
|
149
|
+
"""A company, NGO, government body, etc."""
|
|
150
|
+
_entity_type: ClassVar[EntityType] = EntityType.ORGANIZATION
|
|
151
|
+
industry: str = ""
|
|
152
|
+
country: str = ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(eq=False)
|
|
156
|
+
class Website(Entity):
|
|
157
|
+
"""A specific web page or site URL."""
|
|
158
|
+
_entity_type: ClassVar[EntityType] = EntityType.WEBSITE
|
|
159
|
+
url: str = ""
|
|
160
|
+
|
|
161
|
+
def __post_init__(self):
|
|
162
|
+
super().__post_init__()
|
|
163
|
+
if not self.url:
|
|
164
|
+
self.url = self.value
|
|
165
|
+
self.value = self.url.lower().rstrip("/")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(eq=False)
|
|
169
|
+
class Location(Entity):
|
|
170
|
+
"""A physical location (city, country, coordinates)."""
|
|
171
|
+
_entity_type: ClassVar[EntityType] = EntityType.LOCATION
|
|
172
|
+
latitude: float = 0.0
|
|
173
|
+
longitude: float = 0.0
|
|
174
|
+
country_code: str = ""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(eq=False)
|
|
178
|
+
class Document(Entity):
|
|
179
|
+
"""A document, file, or extracted text snippet."""
|
|
180
|
+
_entity_type: ClassVar[EntityType] = EntityType.DOCUMENT
|
|
181
|
+
snippet: str = ""
|
|
182
|
+
source_url: str = ""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── Entity factory ────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def make_entity(
|
|
189
|
+
entity_type: EntityType,
|
|
190
|
+
value: str,
|
|
191
|
+
**kwargs,
|
|
192
|
+
) -> Entity:
|
|
193
|
+
"""Create an entity of the appropriate type."""
|
|
194
|
+
_type_map: dict[EntityType, type[Entity]] = {
|
|
195
|
+
EntityType.DOMAIN: Domain,
|
|
196
|
+
EntityType.IP_ADDRESS: IPAddress,
|
|
197
|
+
EntityType.EMAIL: Email,
|
|
198
|
+
EntityType.PERSON: Person,
|
|
199
|
+
EntityType.ORGANIZATION: Organization,
|
|
200
|
+
EntityType.WEBSITE: Website,
|
|
201
|
+
EntityType.LOCATION: Location,
|
|
202
|
+
EntityType.DOCUMENT: Document,
|
|
203
|
+
}
|
|
204
|
+
cls = _type_map.get(entity_type, Entity)
|
|
205
|
+
if cls is Entity:
|
|
206
|
+
kwargs["entity_type"] = entity_type
|
|
207
|
+
return cls(value=value, **kwargs)
|