fluidattacks_core_resolves 1.0.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.
@@ -0,0 +1,51 @@
1
+ from fluidattacks_core.resolves._entries import (
2
+ AddressRecord,
3
+ Entry,
4
+ EntryKind,
5
+ NameRecord,
6
+ Outcome,
7
+ RecordType,
8
+ Refusal,
9
+ Resolution,
10
+ SeenBy,
11
+ Sighting,
12
+ )
13
+ from fluidattacks_core.resolves._envelope import (
14
+ SCHEMA_VERSION,
15
+ ResolvesOutput,
16
+ SeedStatus,
17
+ )
18
+ from fluidattacks_core.resolves._hosts import HOST_PATTERN, MAX_HOST_LENGTH, Host, ensure_host
19
+ from fluidattacks_core.resolves._instants import EVIDENCE_DATE_PATTERN, EvidenceDate
20
+ from fluidattacks_core.resolves._schema import (
21
+ SCHEMA_PATH,
22
+ build_schema,
23
+ load_schema,
24
+ render_schema,
25
+ )
26
+
27
+ __all__ = [
28
+ "EVIDENCE_DATE_PATTERN",
29
+ "HOST_PATTERN",
30
+ "MAX_HOST_LENGTH",
31
+ "SCHEMA_PATH",
32
+ "SCHEMA_VERSION",
33
+ "AddressRecord",
34
+ "Entry",
35
+ "EntryKind",
36
+ "EvidenceDate",
37
+ "Host",
38
+ "NameRecord",
39
+ "Outcome",
40
+ "RecordType",
41
+ "Refusal",
42
+ "Resolution",
43
+ "ResolvesOutput",
44
+ "SeedStatus",
45
+ "SeenBy",
46
+ "Sighting",
47
+ "build_schema",
48
+ "ensure_host",
49
+ "load_schema",
50
+ "render_schema",
51
+ ]
@@ -0,0 +1,129 @@
1
+ from enum import StrEnum
2
+ from typing import Annotated, ClassVar, Literal
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
5
+
6
+ from fluidattacks_core.resolves._hosts import Host
7
+ from fluidattacks_core.resolves._instants import EvidenceDate
8
+
9
+
10
+ class EntryKind(StrEnum):
11
+ """What an entry is: a name seen, an answer it gave, or the lack of one."""
12
+
13
+ RECORD = "record"
14
+ RESOLUTION = "resolution"
15
+ SIGHTING = "sighting"
16
+
17
+
18
+ class RecordType(StrEnum):
19
+ """The DNS record types a scan reports, spelled as the protocol does."""
20
+
21
+ A = "A"
22
+ AAAA = "AAAA"
23
+ CNAME = "CNAME"
24
+ MX = "MX"
25
+ NS = "NS"
26
+ TXT = "TXT"
27
+
28
+
29
+ class SeenBy(StrEnum):
30
+ """Which part of the run saw a name, not what it later found about it.
31
+
32
+ Named as the contextualizer contract names it, and deliberately not
33
+ `source`: the platform spells an organization's source entity that way,
34
+ and this is a module of the scanner, not one of those.
35
+ """
36
+
37
+ AXFR = "axfr"
38
+ CRT_NAME = "crt.name"
39
+ CRT_SH = "crt.sh"
40
+ RECORDS = "records"
41
+ SEED = "seed"
42
+ URLSCAN = "urlscan.io"
43
+
44
+
45
+ class Outcome(StrEnum):
46
+ """What resolving a name amounted to when it yielded no usable record.
47
+
48
+ Typed rather than described, so a consumer can tell a name that does not
49
+ exist from one whose address the scan refused to dial.
50
+ """
51
+
52
+ ABSENT = "absent"
53
+ DELEGATED = "delegated"
54
+ NONEXISTENT = "nonexistent"
55
+ REFUSED = "refused"
56
+ UNKNOWN = "unknown"
57
+
58
+
59
+ RecordValue = Annotated[str, StringConstraints(min_length=1, max_length=4096)]
60
+ Port = Annotated[int, Field(ge=1, le=65535)]
61
+
62
+ # An address literal is at most 45 characters in its longest IPv6 spelling.
63
+ RefusedAddress = Annotated[str, StringConstraints(min_length=1, max_length=45)]
64
+
65
+
66
+ class _Observed(BaseModel):
67
+ """What every entry says: which host, under which seed, seen by whom and when."""
68
+
69
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", strict=True)
70
+
71
+ seed: Host
72
+ host: Host
73
+ seen_by: SeenBy
74
+ evidence_date: EvidenceDate
75
+
76
+
77
+ class Sighting(_Observed):
78
+ """A name a module saw, before anything was asked of it."""
79
+
80
+ kind: Literal[EntryKind.SIGHTING]
81
+
82
+
83
+ class AddressRecord(_Observed):
84
+ """An A or AAAA answer, the only kind of record a port probe can follow.
85
+
86
+ `open_ports` absent means the round did not probe; empty means it probed
87
+ and found none. The envelope's seed status says whether it got to ask.
88
+ """
89
+
90
+ kind: Literal[EntryKind.RECORD]
91
+ record_type: Literal[RecordType.A, RecordType.AAAA]
92
+ record_value: RecordValue
93
+ open_ports: tuple[Port, ...] | None = None
94
+
95
+
96
+ class NameRecord(_Observed):
97
+ """An answer that names something else, so there is nothing to probe."""
98
+
99
+ kind: Literal[EntryKind.RECORD]
100
+ record_type: Literal[RecordType.CNAME, RecordType.MX, RecordType.NS, RecordType.TXT]
101
+ record_value: RecordValue
102
+
103
+
104
+ class Resolution(_Observed):
105
+ """A name that was asked and yielded no usable record, and why."""
106
+
107
+ kind: Literal[EntryKind.RESOLUTION]
108
+ outcome: Literal[Outcome.ABSENT, Outcome.DELEGATED, Outcome.NONEXISTENT, Outcome.UNKNOWN]
109
+
110
+
111
+ class Refusal(_Observed):
112
+ """A name whose address the scan declined to dial.
113
+
114
+ The address travels apart from any `record_value`, which only ever holds
115
+ an answer a scan may act on: a reader walking entries can never mistake an
116
+ address the screen withheld for one it may publish.
117
+ """
118
+
119
+ kind: Literal[EntryKind.RESOLUTION]
120
+ outcome: Literal[Outcome.REFUSED]
121
+ refused_address: RefusedAddress
122
+
123
+
124
+ Record = Annotated[AddressRecord | NameRecord, Field(discriminator="record_type")]
125
+ Resolved = Annotated[Resolution | Refusal, Field(discriminator="outcome")]
126
+
127
+ # Which type an entry is follows from `kind`, then from `record_type` or
128
+ # `outcome`, so a field foreign to the kind is an unknown field, not a rule.
129
+ Entry = Annotated[Sighting | Record | Resolved, Field(discriminator="kind")]
@@ -0,0 +1,73 @@
1
+ from collections.abc import Mapping
2
+ from enum import StrEnum
3
+ from typing import Annotated, ClassVar
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
+
7
+ from fluidattacks_core.resolves._entries import Entry
8
+ from fluidattacks_core.resolves._hosts import Host
9
+
10
+ SCHEMA_VERSION = 1
11
+
12
+
13
+ class SeedStatus(StrEnum):
14
+ """How much of one seed's surface the run covered.
15
+
16
+ A consumer that treats absence as a removal must be able to refuse
17
+ anything but `complete`. It is per seed because a run takes several and
18
+ gives up on the ones it cannot enumerate, so one status for the file would
19
+ make a whole run unusable when a single seed failed.
20
+ """
21
+
22
+ COMPLETE = "complete"
23
+ FAILED = "failed"
24
+ PARTIAL = "partial"
25
+
26
+
27
+ class ResolvesOutput(BaseModel):
28
+ """One scan's report over the seeds it was given.
29
+
30
+ Scanner vocabulary only: hosts, records, addresses and ports. Nothing
31
+ about assets, scope or groups, none of which is observable from outside
32
+ the surface being scanned.
33
+ """
34
+
35
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", strict=True)
36
+
37
+ schema_version: int = Field(ge=SCHEMA_VERSION, le=SCHEMA_VERSION)
38
+ # Keyed by seed, so one seed cannot be declared twice with two statuses.
39
+ # The key grammar reaches the schema as `patternProperties`, which alone
40
+ # lets a non-matching key through; closing the object makes it refuse.
41
+ seeds: Annotated[
42
+ Mapping[Host, SeedStatus],
43
+ Field(min_length=1, json_schema_extra={"additionalProperties": False}),
44
+ ]
45
+ entries: tuple[Entry, ...] = ()
46
+
47
+ @model_validator(mode="after")
48
+ def _reject_an_entry_of_an_undeclared_seed(self) -> "ResolvesOutput":
49
+ # An entry is read against its seed's status, so a seed the file does
50
+ # not declare leaves its entries with none.
51
+ stray = sorted({entry.seed for entry in self.entries} - self.seeds.keys())
52
+ if stray:
53
+ msg = f"entries name seeds the run does not declare: {', '.join(stray)}"
54
+ raise ValueError(msg)
55
+
56
+ return self
57
+
58
+ @model_validator(mode="after")
59
+ def _reject_repeated_entries(self) -> "ResolvesOutput":
60
+ # Two modules seeing one name are two facts (68 of 2320 rows on a
61
+ # real run), while the same fact with different ports or dates is one
62
+ # fact stated twice, and a run may state each fact once.
63
+ asserted = [_asserted(entry) for entry in self.entries]
64
+ if len(set(asserted)) != len(asserted):
65
+ msg = "entries must not repeat what they report about one host"
66
+ raise ValueError(msg)
67
+
68
+ return self
69
+
70
+
71
+ def _asserted(entry: Entry) -> frozenset[tuple[str, object]]:
72
+ # What an entry claims about its host, not how or when it was learned.
73
+ return frozenset(entry.model_dump(exclude={"evidence_date", "open_ports"}).items())
@@ -0,0 +1,57 @@
1
+ import re
2
+ from typing import Annotated
3
+
4
+ from pydantic import AfterValidator, StringConstraints
5
+
6
+ # A name is at most 253 octets on the wire.
7
+ MAX_HOST_LENGTH = 253
8
+
9
+ # The canonical spelling, so a validator reading only the schema refuses what
10
+ # `ensure_host` refuses: lowercase ascii labels of letters, digits and inner
11
+ # hyphens, at least two of them, and no root label.
12
+ HOST_PATTERN = r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$"
13
+
14
+ _FORBIDDEN = re.compile("[\\x00-\\x1f\\x7f\\u202a-\\u202e\\u2066-\\u2069]")
15
+
16
+
17
+ def ensure_host(value: str) -> str:
18
+ """Reject, never normalize, anything but the canonical spelling of a host.
19
+
20
+ One spelling per host, so a consumer that folds `A.example.com`,
21
+ `a.example.com.` and `a.example.com` into a single identity can never be
22
+ handed three of them as separate entries. A producer resolving names has
23
+ the canonical form already; anything else is a bug upstream, and repairing
24
+ it here would hide it.
25
+ """
26
+ if not value:
27
+ msg = "host must not be empty"
28
+ raise ValueError(msg)
29
+
30
+ if _FORBIDDEN.search(value):
31
+ msg = f"host must not hold control or bidi characters: {value!r}"
32
+ raise ValueError(msg)
33
+
34
+ if not value.isascii():
35
+ msg = f"host must be in its ascii form: {value!r}"
36
+ raise ValueError(msg)
37
+
38
+ if value != value.lower():
39
+ msg = f"host must be lowercase: {value!r}"
40
+ raise ValueError(msg)
41
+
42
+ if value.endswith("."):
43
+ msg = f"host must not carry the root label: {value!r}"
44
+ raise ValueError(msg)
45
+
46
+ if ".." in value or value.startswith("."):
47
+ msg = f"host must not hold an empty label: {value!r}"
48
+ raise ValueError(msg)
49
+
50
+ return value
51
+
52
+
53
+ Host = Annotated[
54
+ str,
55
+ StringConstraints(min_length=1, max_length=MAX_HOST_LENGTH, pattern=HOST_PATTERN),
56
+ AfterValidator(ensure_host),
57
+ ]
@@ -0,0 +1,9 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import StringConstraints
4
+
5
+ # The one shape the producer's own type emits, and a strict subset of the
6
+ # platform's `#isoDate`, so nothing downstream has to guess a spelling.
7
+ EVIDENCE_DATE_PATTERN = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$"
8
+
9
+ EvidenceDate = Annotated[str, StringConstraints(pattern=EVIDENCE_DATE_PATTERN)]
@@ -0,0 +1,35 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from fluidattacks_core.resolves._envelope import SCHEMA_VERSION, ResolvesOutput
5
+
6
+ SCHEMA_PATH = Path(__file__).parent / "schemas" / "resolves_output.schema.json"
7
+
8
+ # Versioned: a validator caches by $id, so v2 must not answer to v1's uri.
9
+ _SCHEMA_ID = f"https://fluidattacks.com/schemas/resolves_output/v{SCHEMA_VERSION}.json"
10
+
11
+ # Declared, or a validator defaulting to draft-07 reads `$defs` differently.
12
+ _SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"
13
+
14
+
15
+ def build_schema() -> dict[str, object]:
16
+ """Render the canonical JSONSchema the Rust producer validates against.
17
+
18
+ Structural only. The host grammar and the rules conditional on `kind` are
19
+ not expressed here, so the producer implements them and the fixtures are
20
+ what prove the implementations agree.
21
+ """
22
+ schema = ResolvesOutput.model_json_schema()
23
+
24
+ # Spread first, or a pydantic-emitted `$id` would silently win.
25
+ return {**schema, "$schema": _SCHEMA_DIALECT, "$id": _SCHEMA_ID}
26
+
27
+
28
+ def render_schema() -> str:
29
+ return json.dumps(build_schema(), indent=2, sort_keys=True) + "\n"
30
+
31
+
32
+ def load_schema() -> dict[str, object]:
33
+ content: dict[str, object] = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
34
+
35
+ return content
File without changes
@@ -0,0 +1,445 @@
1
+ {
2
+ "$defs": {
3
+ "AddressRecord": {
4
+ "additionalProperties": false,
5
+ "description": "An A or AAAA answer, the only kind of record a port probe can follow.\n\n`open_ports` absent means the round did not probe; empty means it probed\nand found none. The envelope's seed status says whether it got to ask.",
6
+ "properties": {
7
+ "evidence_date": {
8
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
9
+ "title": "Evidence Date",
10
+ "type": "string"
11
+ },
12
+ "host": {
13
+ "maxLength": 253,
14
+ "minLength": 1,
15
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
16
+ "title": "Host",
17
+ "type": "string"
18
+ },
19
+ "kind": {
20
+ "const": "record",
21
+ "title": "Kind",
22
+ "type": "string"
23
+ },
24
+ "open_ports": {
25
+ "anyOf": [
26
+ {
27
+ "items": {
28
+ "maximum": 65535,
29
+ "minimum": 1,
30
+ "type": "integer"
31
+ },
32
+ "type": "array"
33
+ },
34
+ {
35
+ "type": "null"
36
+ }
37
+ ],
38
+ "default": null,
39
+ "title": "Open Ports"
40
+ },
41
+ "record_type": {
42
+ "enum": [
43
+ "A",
44
+ "AAAA"
45
+ ],
46
+ "title": "Record Type",
47
+ "type": "string"
48
+ },
49
+ "record_value": {
50
+ "maxLength": 4096,
51
+ "minLength": 1,
52
+ "title": "Record Value",
53
+ "type": "string"
54
+ },
55
+ "seed": {
56
+ "maxLength": 253,
57
+ "minLength": 1,
58
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
59
+ "title": "Seed",
60
+ "type": "string"
61
+ },
62
+ "seen_by": {
63
+ "$ref": "#/$defs/SeenBy"
64
+ }
65
+ },
66
+ "required": [
67
+ "seed",
68
+ "host",
69
+ "seen_by",
70
+ "evidence_date",
71
+ "kind",
72
+ "record_type",
73
+ "record_value"
74
+ ],
75
+ "title": "AddressRecord",
76
+ "type": "object"
77
+ },
78
+ "NameRecord": {
79
+ "additionalProperties": false,
80
+ "description": "An answer that names something else, so there is nothing to probe.",
81
+ "properties": {
82
+ "evidence_date": {
83
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
84
+ "title": "Evidence Date",
85
+ "type": "string"
86
+ },
87
+ "host": {
88
+ "maxLength": 253,
89
+ "minLength": 1,
90
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
91
+ "title": "Host",
92
+ "type": "string"
93
+ },
94
+ "kind": {
95
+ "const": "record",
96
+ "title": "Kind",
97
+ "type": "string"
98
+ },
99
+ "record_type": {
100
+ "enum": [
101
+ "CNAME",
102
+ "MX",
103
+ "NS",
104
+ "TXT"
105
+ ],
106
+ "title": "Record Type",
107
+ "type": "string"
108
+ },
109
+ "record_value": {
110
+ "maxLength": 4096,
111
+ "minLength": 1,
112
+ "title": "Record Value",
113
+ "type": "string"
114
+ },
115
+ "seed": {
116
+ "maxLength": 253,
117
+ "minLength": 1,
118
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
119
+ "title": "Seed",
120
+ "type": "string"
121
+ },
122
+ "seen_by": {
123
+ "$ref": "#/$defs/SeenBy"
124
+ }
125
+ },
126
+ "required": [
127
+ "seed",
128
+ "host",
129
+ "seen_by",
130
+ "evidence_date",
131
+ "kind",
132
+ "record_type",
133
+ "record_value"
134
+ ],
135
+ "title": "NameRecord",
136
+ "type": "object"
137
+ },
138
+ "Refusal": {
139
+ "additionalProperties": false,
140
+ "description": "A name whose address the scan declined to dial.\n\nThe address travels apart from any `record_value`, which only ever holds\nan answer a scan may act on: a reader walking entries can never mistake an\naddress the screen withheld for one it may publish.",
141
+ "properties": {
142
+ "evidence_date": {
143
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
144
+ "title": "Evidence Date",
145
+ "type": "string"
146
+ },
147
+ "host": {
148
+ "maxLength": 253,
149
+ "minLength": 1,
150
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
151
+ "title": "Host",
152
+ "type": "string"
153
+ },
154
+ "kind": {
155
+ "const": "resolution",
156
+ "title": "Kind",
157
+ "type": "string"
158
+ },
159
+ "outcome": {
160
+ "const": "refused",
161
+ "title": "Outcome",
162
+ "type": "string"
163
+ },
164
+ "refused_address": {
165
+ "maxLength": 45,
166
+ "minLength": 1,
167
+ "title": "Refused Address",
168
+ "type": "string"
169
+ },
170
+ "seed": {
171
+ "maxLength": 253,
172
+ "minLength": 1,
173
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
174
+ "title": "Seed",
175
+ "type": "string"
176
+ },
177
+ "seen_by": {
178
+ "$ref": "#/$defs/SeenBy"
179
+ }
180
+ },
181
+ "required": [
182
+ "seed",
183
+ "host",
184
+ "seen_by",
185
+ "evidence_date",
186
+ "kind",
187
+ "outcome",
188
+ "refused_address"
189
+ ],
190
+ "title": "Refusal",
191
+ "type": "object"
192
+ },
193
+ "Resolution": {
194
+ "additionalProperties": false,
195
+ "description": "A name that was asked and yielded no usable record, and why.",
196
+ "properties": {
197
+ "evidence_date": {
198
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
199
+ "title": "Evidence Date",
200
+ "type": "string"
201
+ },
202
+ "host": {
203
+ "maxLength": 253,
204
+ "minLength": 1,
205
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
206
+ "title": "Host",
207
+ "type": "string"
208
+ },
209
+ "kind": {
210
+ "const": "resolution",
211
+ "title": "Kind",
212
+ "type": "string"
213
+ },
214
+ "outcome": {
215
+ "enum": [
216
+ "absent",
217
+ "delegated",
218
+ "nonexistent",
219
+ "unknown"
220
+ ],
221
+ "title": "Outcome",
222
+ "type": "string"
223
+ },
224
+ "seed": {
225
+ "maxLength": 253,
226
+ "minLength": 1,
227
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
228
+ "title": "Seed",
229
+ "type": "string"
230
+ },
231
+ "seen_by": {
232
+ "$ref": "#/$defs/SeenBy"
233
+ }
234
+ },
235
+ "required": [
236
+ "seed",
237
+ "host",
238
+ "seen_by",
239
+ "evidence_date",
240
+ "kind",
241
+ "outcome"
242
+ ],
243
+ "title": "Resolution",
244
+ "type": "object"
245
+ },
246
+ "SeedStatus": {
247
+ "description": "How much of one seed's surface the run covered.\n\nA consumer that treats absence as a removal must be able to refuse\nanything but `complete`. It is per seed because a run takes several and\ngives up on the ones it cannot enumerate, so one status for the file would\nmake a whole run unusable when a single seed failed.",
248
+ "enum": [
249
+ "complete",
250
+ "failed",
251
+ "partial"
252
+ ],
253
+ "title": "SeedStatus",
254
+ "type": "string"
255
+ },
256
+ "SeenBy": {
257
+ "description": "Which part of the run saw a name, not what it later found about it.\n\nNamed as the contextualizer contract names it, and deliberately not\n`source`: the platform spells an organization's source entity that way,\nand this is a module of the scanner, not one of those.",
258
+ "enum": [
259
+ "axfr",
260
+ "crt.name",
261
+ "crt.sh",
262
+ "records",
263
+ "seed",
264
+ "urlscan.io"
265
+ ],
266
+ "title": "SeenBy",
267
+ "type": "string"
268
+ },
269
+ "Sighting": {
270
+ "additionalProperties": false,
271
+ "description": "A name a module saw, before anything was asked of it.",
272
+ "properties": {
273
+ "evidence_date": {
274
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
275
+ "title": "Evidence Date",
276
+ "type": "string"
277
+ },
278
+ "host": {
279
+ "maxLength": 253,
280
+ "minLength": 1,
281
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
282
+ "title": "Host",
283
+ "type": "string"
284
+ },
285
+ "kind": {
286
+ "const": "sighting",
287
+ "title": "Kind",
288
+ "type": "string"
289
+ },
290
+ "seed": {
291
+ "maxLength": 253,
292
+ "minLength": 1,
293
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
294
+ "title": "Seed",
295
+ "type": "string"
296
+ },
297
+ "seen_by": {
298
+ "$ref": "#/$defs/SeenBy"
299
+ }
300
+ },
301
+ "required": [
302
+ "seed",
303
+ "host",
304
+ "seen_by",
305
+ "evidence_date",
306
+ "kind"
307
+ ],
308
+ "title": "Sighting",
309
+ "type": "object"
310
+ }
311
+ },
312
+ "$id": "https://fluidattacks.com/schemas/resolves_output/v1.json",
313
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
314
+ "additionalProperties": false,
315
+ "description": "One scan's report over the seeds it was given.\n\nScanner vocabulary only: hosts, records, addresses and ports. Nothing\nabout assets, scope or groups, none of which is observable from outside\nthe surface being scanned.",
316
+ "properties": {
317
+ "entries": {
318
+ "default": [],
319
+ "items": {
320
+ "discriminator": {
321
+ "mapping": {
322
+ "record": {
323
+ "discriminator": {
324
+ "mapping": {
325
+ "A": "#/$defs/AddressRecord",
326
+ "AAAA": "#/$defs/AddressRecord",
327
+ "CNAME": "#/$defs/NameRecord",
328
+ "MX": "#/$defs/NameRecord",
329
+ "NS": "#/$defs/NameRecord",
330
+ "TXT": "#/$defs/NameRecord"
331
+ },
332
+ "propertyName": "record_type"
333
+ },
334
+ "oneOf": [
335
+ {
336
+ "$ref": "#/$defs/AddressRecord"
337
+ },
338
+ {
339
+ "$ref": "#/$defs/NameRecord"
340
+ }
341
+ ]
342
+ },
343
+ "resolution": {
344
+ "discriminator": {
345
+ "mapping": {
346
+ "absent": "#/$defs/Resolution",
347
+ "delegated": "#/$defs/Resolution",
348
+ "nonexistent": "#/$defs/Resolution",
349
+ "refused": "#/$defs/Refusal",
350
+ "unknown": "#/$defs/Resolution"
351
+ },
352
+ "propertyName": "outcome"
353
+ },
354
+ "oneOf": [
355
+ {
356
+ "$ref": "#/$defs/Resolution"
357
+ },
358
+ {
359
+ "$ref": "#/$defs/Refusal"
360
+ }
361
+ ]
362
+ },
363
+ "sighting": "#/$defs/Sighting"
364
+ },
365
+ "propertyName": "kind"
366
+ },
367
+ "oneOf": [
368
+ {
369
+ "$ref": "#/$defs/Sighting"
370
+ },
371
+ {
372
+ "discriminator": {
373
+ "mapping": {
374
+ "A": "#/$defs/AddressRecord",
375
+ "AAAA": "#/$defs/AddressRecord",
376
+ "CNAME": "#/$defs/NameRecord",
377
+ "MX": "#/$defs/NameRecord",
378
+ "NS": "#/$defs/NameRecord",
379
+ "TXT": "#/$defs/NameRecord"
380
+ },
381
+ "propertyName": "record_type"
382
+ },
383
+ "oneOf": [
384
+ {
385
+ "$ref": "#/$defs/AddressRecord"
386
+ },
387
+ {
388
+ "$ref": "#/$defs/NameRecord"
389
+ }
390
+ ]
391
+ },
392
+ {
393
+ "discriminator": {
394
+ "mapping": {
395
+ "absent": "#/$defs/Resolution",
396
+ "delegated": "#/$defs/Resolution",
397
+ "nonexistent": "#/$defs/Resolution",
398
+ "refused": "#/$defs/Refusal",
399
+ "unknown": "#/$defs/Resolution"
400
+ },
401
+ "propertyName": "outcome"
402
+ },
403
+ "oneOf": [
404
+ {
405
+ "$ref": "#/$defs/Resolution"
406
+ },
407
+ {
408
+ "$ref": "#/$defs/Refusal"
409
+ }
410
+ ]
411
+ }
412
+ ]
413
+ },
414
+ "title": "Entries",
415
+ "type": "array"
416
+ },
417
+ "schema_version": {
418
+ "maximum": 1,
419
+ "minimum": 1,
420
+ "title": "Schema Version",
421
+ "type": "integer"
422
+ },
423
+ "seeds": {
424
+ "additionalProperties": false,
425
+ "minProperties": 1,
426
+ "patternProperties": {
427
+ "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$": {
428
+ "$ref": "#/$defs/SeedStatus"
429
+ }
430
+ },
431
+ "propertyNames": {
432
+ "maxLength": 253,
433
+ "minLength": 1
434
+ },
435
+ "title": "Seeds",
436
+ "type": "object"
437
+ }
438
+ },
439
+ "required": [
440
+ "schema_version",
441
+ "seeds"
442
+ ],
443
+ "title": "ResolvesOutput",
444
+ "type": "object"
445
+ }
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.5
2
+ Name: fluidattacks_core_resolves
3
+ Version: 1.0.0
4
+ Summary: Fluid Attacks Core Resolves Library
5
+ Author-email: Development <development@fluidattacks.com>
6
+ License: MPL-2.0
7
+ Classifier: Development Status :: 1 - Planning
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: pydantic<3,>=2.12.3
@@ -0,0 +1,11 @@
1
+ fluidattacks_core/resolves/__init__.py,sha256=qjwArhHSwEAXsth3aaYyrhQgpIzuf_J6Y2K61G2IuNE,1052
2
+ fluidattacks_core/resolves/_entries.py,sha256=8R9L8KSLT5SJU3bBlNsnA_3U-jXeQtjjVC7AS794OoM,3920
3
+ fluidattacks_core/resolves/_envelope.py,sha256=BXov1PdNelLe-z12GRI2-dekVKJxozH_X5L5-d6RKm0,2847
4
+ fluidattacks_core/resolves/_hosts.py,sha256=XTvPc82EntY5ep_H5KkfQtuC1mUisOdVgZTj_gfmfVo,1889
5
+ fluidattacks_core/resolves/_instants.py,sha256=C9igJ6Yq7b1MgLqoyzWjI2uMe1cc5rEngrb1zKOrJUs,370
6
+ fluidattacks_core/resolves/_schema.py,sha256=C4mA8G-9fzTuxu_7rOp3-f2cDb4vrpY9_8yziOPzg00,1265
7
+ fluidattacks_core/resolves/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ fluidattacks_core/resolves/schemas/resolves_output.schema.json,sha256=CjLaj0ej0G6AJS_0sRwXqiNO5OkmnvkdO4fvxP_Vozk,13038
9
+ fluidattacks_core_resolves-1.0.0.dist-info/METADATA,sha256=vWWXE3ck83N_zGRszYPFh7KWK2EsZ4QFzW_6ZnCOoQY,569
10
+ fluidattacks_core_resolves-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
11
+ fluidattacks_core_resolves-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any