| 1 | """Uniform enricher contract. Every OSINT module returns the same envelope so the platform |
| 2 | can store entities, relationships, evidence, source metadata, confidence and the raw response |
| 3 | reference identically regardless of source.""" |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | from abc import ABC, abstractmethod |
| 7 | from dataclasses import asdict, dataclass, field |
| 8 | from datetime import datetime, timezone |
| 9 | |
| 10 | |
| 11 | @dataclass |
| 12 | class EnrichEntity: |
| 13 | kind: str |
| 14 | value: str |
| 15 | attributes: dict = field(default_factory=dict) |
| 16 | |
| 17 | |
| 18 | @dataclass |
| 19 | class EnrichRelationship: |
| 20 | src_kind: str |
| 21 | src_value: str |
| 22 | dst_kind: str |
| 23 | dst_value: str |
| 24 | relation: str |
| 25 | confidence: float = 0.7 |
| 26 | |
| 27 | |
| 28 | @dataclass |
| 29 | class EnrichResult: |
| 30 | enricher: str |
| 31 | target: str |
| 32 | entities: list[EnrichEntity] = field(default_factory=list) |
| 33 | relationships: list[EnrichRelationship] = field(default_factory=list) |
| 34 | evidence: list[dict] = field(default_factory=list) |
| 35 | source_metadata: dict = field(default_factory=dict) |
| 36 | confidence: float = 0.5 |
| 37 | raw_response: object | None = None |
| 38 | error: str | None = None |
| 39 | fetched_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) |
| 40 | |
| 41 | def to_dict(self) -> dict: |
| 42 | d = asdict(self) |
| 43 | d.pop("raw_response", None) |
| 44 | return d |
| 45 | |
| 46 | |
| 47 | class BaseEnricher(ABC): |
| 48 | name: str = "base" |
| 49 | kind: str = "generic" |
| 50 | requires_network: bool = True |
| 51 | |
| 52 | @abstractmethod |
| 53 | def run(self, target: str) -> EnrichResult: |
| 54 | ... |
| 55 | |
| 56 | def _empty(self, target: str, error: str | None = None) -> EnrichResult: |
| 57 | return EnrichResult(enricher=self.name, target=target, error=error, confidence=0.0) |