Files
rengine/shared/models/hosting.py
T
Yogesh OjhaandClaude Opus 5 de98b75947 refactor(hosting): replace the sankey with a fronting split over ranked networks
The three-column sankey's first column was always a single node, so two thirds
of it was a stacked bar drawn as ribbons. On badssl.com — 15,477 web assets, one
ASN — it degenerated to a 250px slab of one colour carrying one sentence, and
486 lines of layout, a linearGradient per link, an IntersectionObserver draw-in
and a recursive hover walk paid for three numbers.

GET /subdomains/hosting returns a HostingComposition: the fronting split, then
the top MAX_NETWORKS AS orgs plus "Other networks" as ranked rows, each carrying
its own fronting mix. hosting-body.svelte draws a stacked hairline with an
inline legend and a ranked list whose meter is segmented by that mix, so "19
hosts in Salesforce AS14340, all direct to origin" is one glance. One body, used
by hosting-panel.svelte on the scan Overview and hosting-section.svelte on the
target Overview; the host passes its own padding. Rows reuse ranked-list.svelte,
which gains segments, hint and a rendered meta, and the strip reuses
composition-bar.svelte, which gains inline.

Two count-is-a-promise breaks went with it. The service grouped on
coalesce(Subdomain.asn, HttpAsset.asn) where the grammar's asn reads
Subdomain.asn alone, and it read cloud off a DISTINCT ON (host) latest row where
is:cloud is an EXISTS over every asset of the host, now bool_or. All 23 numbers
on affirm.com equal their search/counts, checked across five scans.

Unattributed hosts are a row, not a column: they appear as "Network not
attributed" when some networks resolve, and when none do the list is dropped and
the panel head says it instead of the network count.

The dashboard widget showed two segments both reading "10,000 100%" — COUNT_CAP,
with the denominator capped too. It now reads 10,000+ and drops the percentages
when any count is capped. Its fronting colours come from config/hosting.ts,
which the panel shares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6DocJiHaZQdMhgQcWddsb
2026-09-13 19:09:42 +05:30

27 lines
600 B
Python

from pydantic import BaseModel, Field
class HostingSlice(BaseModel):
kind: str
label: str
count: int
query: str | None = None
class HostingNetwork(BaseModel):
id: str
label: str
detail: str | None = None
count: int
query: str | None = None
fronting: list[HostingSlice] = Field(default_factory=list)
class HostingComposition(BaseModel):
hosts: int = 0
resolving: int = 0
attributed: int = 0
networks: int = 0
fronting: list[HostingSlice] = Field(default_factory=list)
by_network: list[HostingNetwork] = Field(default_factory=list)