mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 15:21:30 +02:00
`/targets/[id]` was a header plus eight tabs, five of which rendered empty
states written before the scan pipeline existed. It is now four real tabs —
Summary, Web Assets, Intelligence, Scans — wearing the scan pages' clothes:
the same sticky counted tab strip, the same Card + PanelHead hairline-cell
panels, and a Scans tab that is the scan history page scoped to the target.
The five result dimensions become one vocabulary. shared/definitions/surface.py
and frontend config/surface.ts name what a scan produces the way the UI names
it — Web Assets, Endpoints, Services, IPs, Vulnerabilities. The scan page's tab
strip, scanCountPills, targetAssetNoun and the activity chart now read from that
one definition, so no screen says "subdomains" or "open ports" any more.
A target's surface is per-dimension "last observed", not "the latest scan".
GET /targets/{id}/summary reports what the most recent scan that actually ran
each dimension found. A dimension nothing has run reads "Not scanned", never 0 —
"we looked and found nothing" and "we never looked" are different facts. Rows
are proof a stage ran, because a cancelled run writes what it found before it is
stopped without recording success, and counts come from the tables rather than
the scans.*_found rollup, which is wrong for those runs. Every figure equals the
row count you land on: 1,379 / 7,982 / 656 / 182 / 626 against the result tabs,
and 37 sensitive, 20 critical, 19 KEV against their filters.
POST /targets/{id}/assets is a SQL rollup of the target's web assets across
every scan — first seen, last seen, scans seen in, and All/Current/Gone tabs.
It is the one asset view a scan page structurally cannot give. A running scan
can never retire an asset, and is_new needs a baseline, the same rule the
query grammars use. A 24k-asset target pages in ~90ms.
Also fixed: the headline read "No change since the previous scan" above a cell
reading "11,436" because two different comparisons shared a screen; formatSeconds
never rolled minutes into hours, so a 12-hour scan read "720m 55s" here and on
the scan history page; and truncate on an inline span in a block table cell does
not clip, so a long page title spilled across three columns.
Removes target-subdomains, subdomain-table and scan-tab-empty. Promotes
panel-head.svelte to a shared primitive.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9Gw9RvaR2m6wkZZUdfb4w
48 lines
1.6 KiB
Python
48 lines
1.6 KiB
Python
"""The five result dimensions a scan produces, named as the UI names them."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import StrEnum
|
|
|
|
from shared.enums.scan import AssetKind
|
|
|
|
|
|
class SurfaceDimension(StrEnum):
|
|
WEB_ASSETS = "web_assets"
|
|
ENDPOINTS = "endpoints"
|
|
SERVICES = "services"
|
|
IPS = "ips"
|
|
VULNERABILITIES = "vulnerabilities"
|
|
|
|
|
|
SURFACE_ORDER: tuple[str, ...] = tuple(d.value for d in SurfaceDimension)
|
|
|
|
SURFACE_LABELS: dict[str, str] = {
|
|
SurfaceDimension.WEB_ASSETS.value: "Web Assets",
|
|
SurfaceDimension.ENDPOINTS.value: "Endpoints",
|
|
SurfaceDimension.SERVICES.value: "Services",
|
|
SurfaceDimension.IPS.value: "IPs",
|
|
SurfaceDimension.VULNERABILITIES.value: "Vulnerabilities",
|
|
}
|
|
|
|
SURFACE_NOUN: dict[str, tuple[str, str]] = {
|
|
SurfaceDimension.WEB_ASSETS.value: ("web asset", "web assets"),
|
|
SurfaceDimension.ENDPOINTS.value: ("endpoint", "endpoints"),
|
|
SurfaceDimension.SERVICES.value: ("service", "services"),
|
|
SurfaceDimension.IPS.value: ("address", "addresses"),
|
|
SurfaceDimension.VULNERABILITIES.value: ("finding", "findings"),
|
|
}
|
|
|
|
# a stage covers a dimension when it produces one of these kinds
|
|
SURFACE_KINDS: dict[str, frozenset[str]] = {
|
|
SurfaceDimension.WEB_ASSETS.value: frozenset(
|
|
{AssetKind.HOSTS.value, AssetKind.HTTP_ASSETS.value}
|
|
),
|
|
SurfaceDimension.ENDPOINTS.value: frozenset({AssetKind.ENDPOINTS.value}),
|
|
SurfaceDimension.SERVICES.value: frozenset({AssetKind.PORTS.value}),
|
|
SurfaceDimension.IPS.value: frozenset({AssetKind.ADDRESSES.value}),
|
|
SurfaceDimension.VULNERABILITIES.value: frozenset(
|
|
{AssetKind.VULNERABILITIES.value}
|
|
),
|
|
}
|