from __future__ import annotations
import string
from typing import Annotated, Literal
from pydantic import StringConstraints, field_validator
from octopoes.models import OOI, Reference
from octopoes.models.ooi.network import IPAddress, Network
from octopoes.models.persistence import ReferenceField
VALID_HOSTNAME_CHARACTERS = string.ascii_letters + string.digits + "-."
[docs]
class DNSZone(OOI):
object_type: Literal["DNSZone"] = "DNSZone"
hostname: Reference = ReferenceField("Hostname", max_issue_scan_level=2, max_inherit_scan_level=1)
parent: Reference | None = ReferenceField("DNSZone", max_issue_scan_level=0, max_inherit_scan_level=1, default=None)
_natural_key_attrs = ["hostname"]
_reverse_relation_names = {"parent": "child_dns_zones"}
[docs]
class Hostname(OOI):
object_type: Literal["Hostname"] = "Hostname"
network: Reference = ReferenceField(Network)
name: Annotated[str, StringConstraints(to_lower=True)]
dns_zone: Reference | None = ReferenceField(DNSZone, max_issue_scan_level=1, max_inherit_scan_level=2, default=None)
registered_domain: Reference | None = ReferenceField(
"Hostname", max_issue_scan_level=1, max_inherit_scan_level=2, default=None
)
_natural_key_attrs = ["network", "name"]
_reverse_relation_names = {"network": "hostnames", "dns_zone": "hostnames", "registered_domain": "subdomains"}
[docs]
@field_validator("name")
@classmethod
def hostname_valid(cls, v: str) -> str:
v = v.encode("idna").decode()
for c in v:
if c not in VALID_HOSTNAME_CHARACTERS:
raise ValueError(f"Invalid hostname character: {c}")
if v.endswith("-"):
raise ValueError("Hostname must not end with a hyphen")
return v
[docs]
class ResolvedHostname(OOI):
object_type: Literal["ResolvedHostname"] = "ResolvedHostname"
hostname: Reference = ReferenceField(Hostname, max_issue_scan_level=0, max_inherit_scan_level=4)
address: Reference = ReferenceField(IPAddress, max_issue_scan_level=4, max_inherit_scan_level=0)
_natural_key_attrs = ["hostname", "address"]