Files
rengine/shared/definitions/surface.py
T
Yogesh OjhaandClaude Opus 5 3644dffe63 feat(tables): select rows and act on them from one action bar
Every result and management table gains multi-row selection and the floating
action bar targets and scans already had: copy, export selection, delete, and
the dimension's own actions. Rescan and triage move off the inline strip, which
now only offers the rescan that acts on the whole query.

Delete is dimension-local. A row takes only what cannot stand without it: a web
asset its stored responses, a secret its sightings. Endpoints, services and
findings on a deleted host are kept, so every count stays equal to its table.
POST /surface/delete is the one route and the scope is a WHERE, so an id from
another scan deletes nothing. Vulnerabilities are addressable by template_id so
the Issues view deletes a whole check. Exposures dismiss rather than delete: an
exposure row is a web asset.

Selection export runs through the export pipeline. Every surface filter gains
ids, capped by MAX_SELECTED_ROWS.

Secrets and software become exportable, which fixes two breaks: a bundle export
raised KeyError on the secrets dimension, and mcp read a SURFACE_COLUMNS entry
for secrets that did not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyDpdVXL7YVsr5D7BVj2zt
2026-09-15 07:38:48 +05:30

229 lines
6.2 KiB
Python

"""The 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"
SOFTWARE = "software"
SECRETS = "secrets"
SURFACE_ORDER: tuple[str, ...] = tuple(d.value for d in SurfaceDimension)
# the most rows one selection may address
MAX_SELECTED_ROWS = 1_000
SURFACE_LABELS: dict[str, str] = {
SurfaceDimension.WEB_ASSETS.value: "Web assets",
SurfaceDimension.ENDPOINTS.value: "Endpoints",
SurfaceDimension.SERVICES.value: "Services",
SurfaceDimension.IPS.value: "IP addresses",
SurfaceDimension.VULNERABILITIES.value: "Vulnerabilities",
SurfaceDimension.SOFTWARE.value: "Software",
SurfaceDimension.SECRETS.value: "Secrets",
}
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"),
SurfaceDimension.SOFTWARE.value: ("software CVE", "software CVEs"),
SurfaceDimension.SECRETS.value: ("secret", "secrets"),
}
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}
),
# inferred from what the probe stages report
SurfaceDimension.SOFTWARE.value: frozenset(
{AssetKind.HTTP_ASSETS.value, AssetKind.PORTS.value}
),
SurfaceDimension.SECRETS.value: frozenset({AssetKind.SECRETS.value}),
}
SURFACE_COUNT_COLUMNS: dict[str, tuple[str, ...]] = {
SurfaceDimension.WEB_ASSETS.value: ("subdomains_found", "http_assets_found"),
SurfaceDimension.ENDPOINTS.value: ("endpoints_found",),
SurfaceDimension.SERVICES.value: ("open_ports_found",),
SurfaceDimension.IPS.value: ("ips_found",),
SurfaceDimension.VULNERABILITIES.value: ("vulnerabilities_found",),
SurfaceDimension.SECRETS.value: ("secrets_found",),
}
# the columns a person reads, in the order an export writes them
SURFACE_COLUMNS: dict[str, tuple[str, ...]] = {
SurfaceDimension.WEB_ASSETS.value: (
"name",
"http_status",
"page_title",
"http_url",
"tech",
"webserver",
"resolved_ips",
"cname",
"is_cdn",
"cdn_name",
"waf",
"asn_org",
"ports",
"tls_expired",
"endpoint_count",
"vuln_count",
"vuln_severity",
"vuln_kev",
),
SurfaceDimension.ENDPOINTS.value: (
"url",
"host",
"path",
"status_code",
"content_type",
"content_length",
"title",
"methods",
"param_count",
"endpoint_class",
"interest",
"primary_source",
"is_probed",
"is_new",
),
SurfaceDimension.SERVICES.value: (
"ip",
"port",
"protocol",
"service_name",
"service_class",
"product",
"version",
"is_http",
"tls",
"status_code",
"title",
"url",
"hosts",
"asn_org",
"country",
"is_cdn",
"is_sensitive",
"source",
"is_new",
),
SurfaceDimension.IPS.value: (
"ip",
"version",
"asn",
"asn_org",
"country",
"prefix",
"is_cdn",
"cdn_name",
"is_alive",
"ports",
"port_count",
"host_count",
"hosts",
"has_sensitive",
),
SurfaceDimension.VULNERABILITIES.value: (
"fingerprint",
"template_id",
"template_name",
"severity",
"scanner",
"matched_at",
"host",
"ip",
"port",
"cve_ids",
"cvss_score",
"epss_score",
"is_kev",
"state",
"is_new",
"tags",
),
SurfaceDimension.SOFTWARE.value: (
"cve",
"name",
"version",
"product",
"vendor",
"cpe",
"severity",
"cvss_score",
"epss_score",
"is_kev",
"exploit_score",
"evidence",
"confidence",
"caveats",
"version_source",
"host",
"ip",
"port",
"url",
),
SurfaceDimension.SECRETS.value: (
"value",
"kind",
"group",
"vendor",
"state",
"subject",
"host",
"url",
"source",
"sightings",
"hosts",
"is_secret",
),
}
# the column a human reads first
SURFACE_IDENTITY: dict[str, str] = {
SurfaceDimension.WEB_ASSETS.value: "name",
SurfaceDimension.ENDPOINTS.value: "url",
SurfaceDimension.SERVICES.value: "ip",
SurfaceDimension.IPS.value: "ip",
SurfaceDimension.VULNERABILITIES.value: "template_name",
SurfaceDimension.SOFTWARE.value: "cve",
SurfaceDimension.SECRETS.value: "value",
}
# the one value a plain text export writes per line, for piping into the next tool
SURFACE_TEXT_VALUE: dict[str, tuple[str, ...]] = {
SurfaceDimension.WEB_ASSETS.value: ("name",),
SurfaceDimension.ENDPOINTS.value: ("url",),
SurfaceDimension.SERVICES.value: ("ip", "port"),
SurfaceDimension.IPS.value: ("ip",),
SurfaceDimension.VULNERABILITIES.value: ("matched_at",),
SurfaceDimension.SOFTWARE.value: ("cve",),
SurfaceDimension.SECRETS.value: ("value",),
}
# a dimension an export can write: every map an export run reads has an entry for it
EXPORTABLE_DIMENSIONS: tuple[str, ...] = tuple(
d
for d in SURFACE_ORDER
if d in SURFACE_COLUMNS and d in SURFACE_IDENTITY and d in SURFACE_TEXT_VALUE
)