Files
rengine/shared/services/surface_scope_sync.py
T
Yogesh OjhaandClaude Opus 5 7781259c62 feat(exports): run an export as a job, and keep the recipe
POST /exports saves what the table was showing — dimension, scope, the
filter body, the format — and queues a run. The row is the recipe, so
rerun runs it again against today's rows rather than handing back
yesterday's file, which is the point of keeping it.

The scope resolves when the run starts, not when it was saved: a scan
export reads that scan, a target or project export re-picks the newest
covering scan of each target, so a saved export follows the estate.
That needed a sync resolver, since surface_scope is async and the api is
not on the worker's path.

A bundle writes one file per dimension into a zip. Expiry is its own
status rather than a failure wearing the wrong label, and a reaper fails
a run the worker lost, which reports still lack.

Streaming takes a connection of its own. yield_per opens a server side
cursor, and the progress commit — or the per chunk join — executed on
the session would invalidate it mid-file; the first large export failed
with "named cursor isn't valid anymore" and nothing smaller ever would
have. The export therefore reads committed rows, so its promise test
moves to the durable fixture.

Proven end to end: a filtered CSV that matches the table, 7,784 URLs one
per line, a five file zip, a project wide export across 2,184 addresses,
and a rerun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYZfaNjq3Ym8GPvWYuHisS
2026-09-13 08:21:50 +05:30

70 lines
2.2 KiB
Python

"""The scans a dimension reads, resolved from sync code the way the api resolves them."""
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import func, select
from shared.definitions.surface import SurfaceDimension
from shared.models.endpoint import Endpoint
from shared.models.ip_address import IpAddress
from shared.models.port import Port
from shared.models.scan import Scan
from shared.models.software import SoftwareCve
from shared.models.subdomain import Subdomain
from shared.models.vulnerability import Vulnerability
from shared.services.asset_query import QueryScope
from shared.services.scan_scope import census_only, covers
if TYPE_CHECKING:
from uuid import UUID
from sqlalchemy.orm import Session
TABLES = {
SurfaceDimension.WEB_ASSETS.value: Subdomain,
SurfaceDimension.ENDPOINTS.value: Endpoint,
SurfaceDimension.SERVICES.value: Port,
SurfaceDimension.IPS.value: IpAddress,
SurfaceDimension.VULNERABILITIES.value: Vulnerability,
SurfaceDimension.SOFTWARE.value: SoftwareCve,
}
def _started():
return func.coalesce(Scan.started_at, Scan.created_at)
def _picks(session: Session, project_id: UUID, dimension: str, target_id=None):
"""The newest scan of each target that actually ran this dimension."""
statement = (
select(Scan.id, Scan.target_id)
.where(
Scan.project_id == project_id,
census_only(),
covers(TABLES[dimension], dimension),
)
.distinct(Scan.target_id)
.order_by(Scan.target_id, _started().desc())
)
if target_id is not None:
statement = statement.where(Scan.target_id == target_id)
return session.execute(statement).all()
def project_scope(session: Session, project_id: UUID, dimension: str) -> QueryScope:
return QueryScope(
tuple(row.id for row in _picks(session, project_id, dimension)),
project_id=project_id,
)
def target_scope(
session: Session, project_id: UUID, target_id: UUID, dimension: str
) -> QueryScope:
return QueryScope(
tuple(row.id for row in _picks(session, project_id, dimension, target_id)),
project_id=project_id,
)