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 +47 -0
- validin/annotation_record.py +92 -0
- validin/certificate_record.py +217 -0
- validin/client.py +1586 -0
- validin/content.py +140 -0
- validin/dns_record.py +119 -0
- validin/enriched_indicator.py +77 -0
- validin/errors.py +15 -0
- validin/host_response_record.py +348 -0
- validin/indicator.py +115 -0
- validin/lookalike_record.py +99 -0
- validin/registration.py +181 -0
- validin/registration_record.py +116 -0
- validin/result_set.py +181 -0
- validin/scan.py +121 -0
- validin/scan_record.py +159 -0
- validin/yara_match_record.py +189 -0
- validin_sdk-0.1.0.dist-info/METADATA +516 -0
- validin_sdk-0.1.0.dist-info/RECORD +22 -0
- validin_sdk-0.1.0.dist-info/WHEEL +5 -0
- validin_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- validin_sdk-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
}
|