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.
- fluidattacks_core/resolves/__init__.py +51 -0
- fluidattacks_core/resolves/_entries.py +129 -0
- fluidattacks_core/resolves/_envelope.py +73 -0
- fluidattacks_core/resolves/_hosts.py +57 -0
- fluidattacks_core/resolves/_instants.py +9 -0
- fluidattacks_core/resolves/_schema.py +35 -0
- fluidattacks_core/resolves/py.typed +0 -0
- fluidattacks_core/resolves/schemas/resolves_output.schema.json +445 -0
- fluidattacks_core_resolves-1.0.0.dist-info/METADATA +14 -0
- fluidattacks_core_resolves-1.0.0.dist-info/RECORD +11 -0
- fluidattacks_core_resolves-1.0.0.dist-info/WHEEL +4 -0
|
@@ -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,,
|