validin-sdk 0.1.0__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.
validin/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """Minimal Validin SDK surface for DNS history, host responses, and enrichment."""
2
+
3
+ from .annotation_record import AnnotationRecord
4
+ from .certificate_record import CertificateObservation, CertificateRecord
5
+ from .client import Client, ValidinClient
6
+ from .enriched_indicator import EnrichedIndicator
7
+ from .content import Content, ContentIndicator, ContentResource
8
+ from .dns_record import DNSRecord
9
+ from .errors import ApiError, ValidinError
10
+ from .host_response_record import HostResponse, HostResponseRecord
11
+ from .indicator import Indicator, IndicatorType
12
+ from .lookalike_record import LookalikeRecord
13
+ from .registration import Registration
14
+ from .registration_record import RegistrationRecord
15
+ from .result_set import ResultSet
16
+ from .scan import ScanJob
17
+ from .scan_record import ScanRecord, ScanResponse
18
+ from .yara_match_record import YaraMatchRecord
19
+
20
+ __all__ = [
21
+ "AnnotationRecord",
22
+ "ApiError",
23
+ "CertificateObservation",
24
+ "CertificateRecord",
25
+ "Client",
26
+ "Content",
27
+ "ContentIndicator",
28
+ "ContentResource",
29
+ "DNSRecord",
30
+ "EnrichedIndicator",
31
+ "HostResponse",
32
+ "HostResponseRecord",
33
+ "Indicator",
34
+ "IndicatorType",
35
+ "LookalikeRecord",
36
+ "Registration",
37
+ "RegistrationRecord",
38
+ "ResultSet",
39
+ "ScanJob",
40
+ "ScanRecord",
41
+ "ScanResponse",
42
+ "ValidinClient",
43
+ "ValidinError",
44
+ "YaraMatchRecord",
45
+ ]
46
+
47
+ __version__ = "0.1.0"
@@ -0,0 +1,92 @@
1
+ """Quick reputation annotation record model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Mapping, Optional
7
+
8
+ from .indicator import Indicator, IndicatorType
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class AnnotationRecord:
13
+ """Flattened annotation row returned by quick reputation endpoints."""
14
+
15
+ description: Optional[str]
16
+ key: Optional[Indicator]
17
+ value: Any
18
+ value_type: Optional[str]
19
+ category: Optional[str]
20
+ risk_cat: Optional[str]
21
+ title: Optional[str]
22
+ raw: Mapping[str, Any] = field(default_factory=dict, repr=False)
23
+
24
+ @classmethod
25
+ def from_api(
26
+ cls,
27
+ payload: Mapping[str, Any],
28
+ *,
29
+ query_indicator_type: IndicatorType,
30
+ ) -> "AnnotationRecord":
31
+ key_value = payload.get("key")
32
+ key = None
33
+ if key_value not in (None, ""):
34
+ key = Indicator(str(key_value), query_indicator_type)
35
+
36
+ value = payload.get("value")
37
+ if value is None and "values" in payload:
38
+ values = payload.get("values")
39
+ value = list(values) if isinstance(values, list) else values
40
+
41
+ value_type = payload.get("value_type")
42
+ if value_type in (None, "") and isinstance(value, list):
43
+ value_type = "list"
44
+
45
+ return cls(
46
+ description=str(payload.get("description")) if payload.get("description") is not None else None,
47
+ key=key,
48
+ value=value,
49
+ value_type=str(value_type) if value_type is not None else None,
50
+ category=str(payload.get("category")) if payload.get("category") is not None else None,
51
+ risk_cat=str(payload.get("risk_cat")) if payload.get("risk_cat") is not None else None,
52
+ title=str(payload.get("title")) if payload.get("title") is not None else None,
53
+ raw=dict(payload),
54
+ )
55
+
56
+ @property
57
+ def result_key(self) -> Optional[Indicator]:
58
+ return self.key
59
+
60
+ def dedupe_key(self) -> tuple:
61
+ return (
62
+ self.description,
63
+ self.key.value if self.key else None,
64
+ self.key.type.value if self.key else None,
65
+ self._make_hashable(self.value),
66
+ self.value_type,
67
+ self.category,
68
+ self.risk_cat,
69
+ self.title,
70
+ )
71
+
72
+ def to_dict(self) -> dict:
73
+ return {
74
+ "description": self.description,
75
+ "key": self.key.value if self.key else None,
76
+ "key_type": self.key.type.value if self.key else None,
77
+ "value": self.value,
78
+ "value_type": self.value_type,
79
+ "category": self.category,
80
+ "risk_cat": self.risk_cat,
81
+ "title": self.title,
82
+ }
83
+
84
+ @staticmethod
85
+ def _make_hashable(value: Any) -> Any:
86
+ if isinstance(value, list):
87
+ return tuple(AnnotationRecord._make_hashable(item) for item in value)
88
+ if isinstance(value, dict):
89
+ return tuple(
90
+ sorted((key, AnnotationRecord._make_hashable(item)) for key, item in value.items())
91
+ )
92
+ return value
@@ -0,0 +1,217 @@
1
+ """Certificate transparency record models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Mapping, Optional
8
+
9
+ from .indicator import Indicator, IndicatorType
10
+
11
+
12
+ def _coerce_str(value: Any) -> Optional[str]:
13
+ if value in (None, ""):
14
+ return None
15
+ return str(value)
16
+
17
+
18
+ def _parse_timestamp(value: Any) -> Optional[datetime]:
19
+ if value in (None, ""):
20
+ return None
21
+
22
+ if isinstance(value, datetime):
23
+ return value
24
+
25
+ if isinstance(value, (int, float)):
26
+ return datetime.fromtimestamp(value, tz=timezone.utc)
27
+
28
+ if isinstance(value, str):
29
+ stripped = value.strip()
30
+ if not stripped:
31
+ return None
32
+ if stripped.endswith("Z"):
33
+ stripped = stripped[:-1] + "+00:00"
34
+ try:
35
+ return datetime.fromisoformat(stripped)
36
+ except ValueError:
37
+ try:
38
+ return datetime.fromtimestamp(float(stripped), tz=timezone.utc)
39
+ except ValueError:
40
+ return None
41
+
42
+ return None
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class CertificateObservation:
47
+ """Normalized certificate transparency observation."""
48
+
49
+ timestamp: Optional[datetime] = None
50
+ update_type: Optional[str] = None
51
+ common_name: Optional[str] = None
52
+ cert_issuer: Optional[str] = None
53
+ issuer: Mapping[str, Any] = field(default_factory=dict)
54
+ not_before: Optional[datetime] = None
55
+ not_after: Optional[datetime] = None
56
+ fingerprint: Optional[str] = None
57
+ fingerprint_sha256: Optional[str] = None
58
+ domains: tuple[str, ...] = field(default_factory=tuple)
59
+ links: tuple[str, ...] = field(default_factory=tuple)
60
+ type: Optional[str] = None
61
+ time: Optional[datetime] = None
62
+ raw: Mapping[str, Any] = field(default_factory=dict, repr=False)
63
+
64
+ @classmethod
65
+ def from_payload(cls, payload: Mapping[str, Any]) -> "CertificateObservation":
66
+ if not isinstance(payload, Mapping):
67
+ raise TypeError("payload must be a mapping")
68
+
69
+ details = payload.get("details")
70
+ if isinstance(details, Mapping):
71
+ details_payload = details
72
+ else:
73
+ details_payload = {}
74
+
75
+ issuer = payload.get("issuer")
76
+ if isinstance(issuer, Mapping):
77
+ issuer_payload = dict(issuer)
78
+ else:
79
+ issuer_payload = {}
80
+
81
+ domains = tuple(
82
+ str(item).strip()
83
+ for item in (details_payload.get("domains") or [])
84
+ if str(item).strip()
85
+ )
86
+ links = tuple(
87
+ str(item).strip()
88
+ for item in (payload.get("links") or [])
89
+ if str(item).strip()
90
+ )
91
+
92
+ return cls(
93
+ timestamp=_parse_timestamp(payload.get("timestamp")),
94
+ update_type=_coerce_str(payload.get("update_type")),
95
+ common_name=_coerce_str(payload.get("common_name")),
96
+ cert_issuer=_coerce_str(payload.get("cert_issuer")),
97
+ issuer=issuer_payload,
98
+ not_before=_parse_timestamp(payload.get("not_before")),
99
+ not_after=_parse_timestamp(payload.get("not_after")),
100
+ fingerprint=_coerce_str(details_payload.get("fingerprint")),
101
+ fingerprint_sha256=_coerce_str(details_payload.get("fingerprint_sha256")),
102
+ domains=domains,
103
+ links=links,
104
+ type=_coerce_str(payload.get("type")),
105
+ time=_parse_timestamp(payload.get("time")),
106
+ raw=dict(payload),
107
+ )
108
+
109
+ def to_dict(self) -> dict:
110
+ return {
111
+ "timestamp": self.timestamp.isoformat() if self.timestamp else None,
112
+ "update_type": self.update_type,
113
+ "common_name": self.common_name,
114
+ "cert_issuer": self.cert_issuer,
115
+ "issuer": dict(self.issuer),
116
+ "not_before": self.not_before.isoformat() if self.not_before else None,
117
+ "not_after": self.not_after.isoformat() if self.not_after else None,
118
+ "fingerprint": self.fingerprint,
119
+ "fingerprint_sha256": self.fingerprint_sha256,
120
+ "domains": list(self.domains),
121
+ "links": list(self.links),
122
+ "type": self.type,
123
+ "time": self.time.isoformat() if self.time else None,
124
+ }
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class CertificateRecord:
129
+ """Normalized certificate history row."""
130
+
131
+ key: Optional[Indicator]
132
+ type: str
133
+ value: Optional[CertificateObservation]
134
+ value_type: Optional[str]
135
+ first_seen: Optional[datetime]
136
+ last_seen: Optional[datetime]
137
+ raw: Mapping[str, Any] = field(default_factory=dict, repr=False)
138
+
139
+ @classmethod
140
+ def from_api(
141
+ cls,
142
+ record_type: str,
143
+ payload: Mapping[str, Any],
144
+ *,
145
+ query_indicator_type: IndicatorType,
146
+ ) -> "CertificateRecord":
147
+ key = None
148
+ key_value = payload.get("key")
149
+ if key_value not in (None, ""):
150
+ key = Indicator.from_api(
151
+ str(key_value),
152
+ str(payload.get("key_type") or query_indicator_type.value),
153
+ )
154
+
155
+ value = None
156
+ value_payload = payload.get("value")
157
+ if isinstance(value_payload, Mapping):
158
+ value = CertificateObservation.from_payload(value_payload)
159
+
160
+ return cls(
161
+ key=key,
162
+ type=str(record_type),
163
+ value=value,
164
+ value_type=_coerce_str(payload.get("value_type")),
165
+ first_seen=_parse_timestamp(payload.get("first_seen")),
166
+ last_seen=_parse_timestamp(payload.get("last_seen")),
167
+ raw=dict(payload),
168
+ )
169
+
170
+ @property
171
+ def result_key(self) -> Optional[Indicator]:
172
+ return self.key
173
+
174
+ @property
175
+ def common_name(self) -> Optional[str]:
176
+ return self.value.common_name if self.value else None
177
+
178
+ @property
179
+ def fingerprint(self) -> Optional[str]:
180
+ return self.value.fingerprint if self.value else None
181
+
182
+ @property
183
+ def fingerprint_sha256(self) -> Optional[str]:
184
+ return self.value.fingerprint_sha256 if self.value else None
185
+
186
+ @property
187
+ def domains(self) -> tuple[str, ...]:
188
+ return self.value.domains if self.value else ()
189
+
190
+ @property
191
+ def timestamp(self) -> Optional[datetime]:
192
+ return self.value.timestamp if self.value else None
193
+
194
+ def dedupe_key(self) -> tuple:
195
+ return (
196
+ self.key.value if self.key else None,
197
+ self.key.type.value if self.key else None,
198
+ self.type,
199
+ self.value_type,
200
+ self.common_name,
201
+ self.fingerprint,
202
+ self.fingerprint_sha256,
203
+ self.domains,
204
+ self.first_seen.isoformat() if self.first_seen else None,
205
+ self.last_seen.isoformat() if self.last_seen else None,
206
+ )
207
+
208
+ def to_dict(self) -> dict:
209
+ return {
210
+ "key": self.key.value if self.key else None,
211
+ "key_type": self.key.type.value if self.key else None,
212
+ "type": self.type,
213
+ "value_type": self.value_type,
214
+ "first_seen": self.first_seen.isoformat() if self.first_seen else None,
215
+ "last_seen": self.last_seen.isoformat() if self.last_seen else None,
216
+ "certificate": self.value.to_dict() if self.value else None,
217
+ }