Files
rengine/shared/models/target_summary.py
T
Yogesh OjhaandClaude Opus 5 37b3f8bacd feat(targets): the target page becomes a summary
`/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
2026-09-05 14:04:35 +05:30

61 lines
1.6 KiB
Python

import uuid
from datetime import datetime
from pydantic import BaseModel, Field
from shared.models.scan import ScanRead
from shared.models.vulnerability import SeverityCount
class SurfaceMetric(BaseModel):
"""What the most recent scan that actually ran this dimension found."""
key: str
label: str
covered: bool = False
value: int | None = None
previous: int | None = None
delta: int | None = None
added: int | None = None
gone: int | None = None
scan_id: uuid.UUID | None = None
scan_status: str | None = None
observed_at: datetime | None = None
current: bool = False
class TargetRisk(BaseModel):
scan_id: uuid.UUID | None = None
observed_at: datetime | None = None
total: int = 0
actionable: int = 0
kev: int = 0
suppressed: int = 0
by_severity: list[SeverityCount] = Field(default_factory=list)
class TargetMonitoring(BaseModel):
schedule_id: uuid.UUID
name: str
cadence: str
status: str
next_run_at: datetime | None = None
last_run_at: datetime | None = None
class TargetSummaryRead(BaseModel):
target_id: uuid.UUID
scans_total: int = 0
scans_running: int = 0
scans_failed: int = 0
first_scan_at: datetime | None = None
last_scan_at: datetime | None = None
last_completed_at: datetime | None = None
latest_scan: ScanRead | None = None
surface: list[SurfaceMetric] = Field(default_factory=list)
risk: TargetRisk = Field(default_factory=TargetRisk)
sensitive_services: int | None = None
inventory_total: int = 0
inventory_first_seen: datetime | None = None
monitoring: TargetMonitoring | None = None