Files
rengine/shared/services/surface_query/__init__.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

172 lines
4.5 KiB
Python

"""One definition of each dimension's filtered set, for the api and the worker alike."""
from __future__ import annotations
from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING, Any
from shared.definitions.surface import SurfaceDimension
from shared.services.surface_query import (
endpoints,
ips,
secrets,
services,
software,
vulnerabilities,
web_assets,
)
if TYPE_CHECKING:
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel
from sqlalchemy import Select
from shared.services.asset_query import QueryScope
@dataclass(frozen=True)
class Built:
"""A filtered statement and the source its columns hang off."""
statement: Select
source: Any
@dataclass(frozen=True)
class SurfaceQuery:
dimension: str
module: Any
filter_path: str
needs_project: bool = False
takes_source: bool = False
order_needs_scope: bool = False
@property
def filter_model(self) -> type[BaseModel]:
module_name, _, attribute = self.filter_path.rpartition(".")
import importlib # noqa: PLC0415
return getattr(importlib.import_module(module_name), attribute)
def build(
self,
scope: QueryScope,
f: BaseModel,
now: datetime,
*,
project_id: UUID | None = None,
columns=None,
) -> Built:
if self.needs_project:
return Built(
self.module.scoped(project_id, scope, f, now, columns=columns), None
)
if self.takes_source:
source, statement = self.module.scoped(scope, f, columns=columns)
return Built(statement, source)
return Built(self.module.scoped(scope, f, columns=columns), None)
def order(self, built: Built, f: BaseModel, scope: QueryScope) -> Select:
if self.takes_source:
return self.module.order(built.statement, built.source, f)
if self.order_needs_scope:
return self.module.order(built.statement, f, scope)
return self.module.order(built.statement, f)
def compiled(self, built: Built, scope: QueryScope, f: BaseModel, now: datetime):
"""The query language predicate, or None when the query is empty."""
if self.takes_source:
return self.module.compiled(scope, f, now, built.source)
return self.module.compiled(scope, f, now)
def filtered(
self,
scope: QueryScope,
f: BaseModel,
now: datetime,
*,
project_id: UUID | None = None,
columns=None,
) -> Built:
"""The statement the table shows: scope, facets and the typed query."""
built = self.build(scope, f, now, project_id=project_id, columns=columns)
predicate = self.compiled(built, scope, f, now)
if predicate is None:
return built
return Built(built.statement.where(predicate), built.source)
QUERIES: tuple[SurfaceQuery, ...] = (
SurfaceQuery(
SurfaceDimension.WEB_ASSETS.value,
web_assets,
"shared.models.subdomain.SubdomainFilter",
needs_project=True,
),
SurfaceQuery(
SurfaceDimension.ENDPOINTS.value,
endpoints,
"shared.models.endpoint.EndpointFilter",
),
SurfaceQuery(
SurfaceDimension.SERVICES.value,
services,
"shared.models.scan_correlation.ServiceFilter",
takes_source=True,
),
SurfaceQuery(
SurfaceDimension.IPS.value,
ips,
"shared.models.scan_correlation.IpGroupFilter",
takes_source=True,
),
SurfaceQuery(
SurfaceDimension.VULNERABILITIES.value,
vulnerabilities,
"shared.models.vulnerability.VulnerabilityFilter",
order_needs_scope=True,
),
SurfaceQuery(
SurfaceDimension.SOFTWARE.value,
software,
"shared.models.software.SoftwareFilter",
),
SurfaceQuery(
SurfaceDimension.SECRETS.value,
secrets,
"shared.models.secret.SecretFilter",
),
)
@lru_cache(maxsize=1)
def by_dimension() -> dict[str, SurfaceQuery]:
return {q.dimension: q for q in QUERIES}
def for_dimension(dimension: str) -> SurfaceQuery:
query = by_dimension().get(dimension)
if query is None:
msg = f"unknown dimension {dimension!r}"
raise KeyError(msg)
return query
__all__ = [
"QUERIES",
"Built",
"SurfaceQuery",
"by_dimension",
"endpoints",
"for_dimension",
"ips",
"secrets",
"services",
"software",
"vulnerabilities",
"web_assets",
]