mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-19 23:01:29 +02:00
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
172 lines
4.5 KiB
Python
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",
|
|
]
|