Files
rengine/shared/models/whois.py
T
Yogesh OjhaandClaude Fable 5.1 fb8b6b5df7 feat(targets): the target page becomes an entity page
The summary rebuild was a card grid: a lot of space to see nothing, the same
fact three times, and a scan table pasted in from the history page. This is the
page rebuilt from an approved mockup as an entity page — Overview · Web Assets ·
DNS · WHOIS · BGP — where the rail is the fact sheet, the main column is the
story, and every fact has one home.

Overview: a five-pill surface strip (count plus one note — added/gone,
sensitive, critical, "so far" while live, dashed "Not scanned" — with a
sparkline over the last six completed runs once two carry a value); Needs
attention tiles ranked critical → warning from the summary's risk figures,
sensitive services and the derived checks; an activity timeline of runs on a
dotted rail whose latest chips read the summary's surface values, so the
timeline never contradicts the strip; and related targets from WHOIS
correlation and certificate names. The sticky rail opens with the globe (the
IPs-covering scan's countries, every dot a pivot), then Scan · Resolution ·
Registration · Routing, with NS, MX and registrar shown as provider name plus
vendored brand logo and a failed lookup's error carried in the group note next
to Refresh.

There is no Scans tab: "All runs" opens /scans?target=<id>, which the scan
history page now reads. DNS, WHOIS and BGP are evidence tabs sharing one
grammar (record-shell + record-group): DNS grouped by type with provider logos,
TXT purpose and private-range warnings; WHOIS as registration facts → EPP status
→ nameservers → contacts; BGP as routing facts → prefixes (family filter,
search, paging) → peers → related prefixes → abuse contacts. A tab renders only
when it has data or its lookup is still pending, so a deep link is not yanked
to Overview mid-load.

WhoisRecordSummary carries target_id so a correlated registrant or nameserver
links to its target. copy-button takes a class that overrides its size and the
org/tag popovers report their patch through onChange. Removes the enrichment/,
summary/ and scan-history components of the previous version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YK61xuAJGMhq1uniLhHX5
2026-09-05 18:39:10 +05:30

133 lines
3.7 KiB
Python

import uuid
from datetime import datetime
from pydantic import BaseModel
from sqlalchemy import Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Field, SQLModel
from shared.enums.whois import WhoisLookupType
from shared.utils.datetime import utc_now
class WhoisRecord(SQLModel, table=True):
__tablename__ = "whois_records"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True, index=True)
query_value: str = Field(max_length=500, index=True, unique=True)
lookup_type: WhoisLookupType
queried_at: datetime = Field(
default_factory=utc_now,
description="When RDAP was last queried for this record",
)
handle: str = Field(default="", max_length=200)
name: str = Field(default="", max_length=500, index=True)
whois_server: str = Field(default="", max_length=200)
object_class: str = Field(default="", max_length=100)
rir: str = Field(default="", max_length=50)
description: str = Field(default="", sa_type=Text)
registration_date: datetime | None = None
last_changed_date: datetime | None = None
expiration_date: datetime | None = None
registrant_name: str = Field(default="", max_length=500, index=True)
registrant_email: str = Field(default="", max_length=500, index=True)
registrar_name: str = Field(default="", max_length=500, index=True)
abuse_email: str = Field(default="", max_length=500)
nameservers: list | None = Field(default=None, sa_type=JSONB)
domain_status: list | None = Field(default=None, sa_type=JSONB)
dnssec: bool | None = None
country: str = Field(default="", max_length=10, index=True)
ip_version: int | None = None
assignment_type: str = Field(default="", max_length=100)
network_cidr: str = Field(default="", max_length=100, index=True)
asn_range_start: int | None = None
asn_range_end: int | None = None
parsed_data: dict | None = Field(default=None, sa_type=JSONB)
created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)
class WhoisNameserver(SQLModel, table=True):
__tablename__ = "whois_nameservers"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
whois_record_id: uuid.UUID = Field(
foreign_key="whois_records.id",
index=True,
ondelete="CASCADE",
)
nameserver: str = Field(max_length=500, index=True)
class WhoisRecordRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
query_value: str
lookup_type: str
queried_at: datetime
handle: str
name: str
whois_server: str
object_class: str = Field(default="")
rir: str
description: str
registration_date: datetime | None
last_changed_date: datetime | None
expiration_date: datetime | None
registrant_name: str
registrant_email: str
registrar_name: str
abuse_email: str
nameservers: list | None
domain_status: list | None
dnssec: bool | None
country: str
ip_version: int | None
assignment_type: str
network_cidr: str
asn_range_start: int | None
asn_range_end: int | None
parsed_data: dict | None
created_at: datetime
updated_at: datetime
class WhoisRecordSummary(BaseModel):
id: uuid.UUID
target_id: uuid.UUID | None = None
query_value: str
lookup_type: str
name: str
registrant_name: str
registrar_name: str
country: str
network_cidr: str
registration_date: datetime | None
expiration_date: datetime | None
queried_at: datetime
class WhoisCorrelationResult(BaseModel):
correlation_type: str
correlation_value: str
records: list[WhoisRecordSummary]
count: int