Files
rengine/shared/services/scan_factory.py
T
Yogesh OjhaandClaude Opus 5 c41f0c860e feat(targets): store the assets you already know and seed every scan with them
A target carries a list of host names, addresses and URLs an operator already
holds. Every scan of that target starts from them, so a list earned elsewhere
no longer has to be re-pasted or re-discovered.

Each pasted line is classified by its own shape and scope-checked against the
target; a refusal names the line and the reason. Seeds enter a run through the
asset_seed stage, which resolves the names it has no answer for and materialises
what they resolve to. Stages the engine has switched off still do not run.

Seeds no longer force a focused run: scope reads the new seed_only, which only
rescan, watch and connector runs set, so a seeded census scan still covers its
target. Seed count is a material run facet, so Compare states it before any
delta it would otherwise explain.

Two stages share the subdomains table for a run: discovery's clear no longer
drops rows asset_seed wrote, and the run's seeded hosts join its merge so the
imported source survives a name discovery also finds.

Closes #1538

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

128 lines
4.0 KiB
Python

import uuid
from shared.enums.scan import ScanScope, ScanStatus
from shared.models.scan import Scan
from shared.services import target_seeds
from shared.services.proxy_resolve import resolve_proxy_url
from shared.services.scan_resolve import (
ResolvedScanConfig,
_mask_auth,
merge_engine_context,
seal_headers,
)
class ScanFactoryError(Exception):
pass
def build_scan_row(
*,
resolved: ResolvedScanConfig,
engine,
context,
target,
project_id: uuid.UUID,
created_by: uuid.UUID,
schedule_id: uuid.UUID | None = None,
schedule_type: str | None = None,
parent_scan_id: uuid.UUID | None = None,
run_group_id: uuid.UUID | None = None,
dimension: str | None = None,
) -> Scan:
"""Assemble an unsaved PENDING Scan from an already-resolved config."""
execution_config = resolved.model_dump()
execution_config["headers"] = seal_headers(execution_config.get("headers"))
if dimension:
execution_config["_dimension"] = dimension
execution_config["_auth_header_names"] = list(resolved._auth_header_names)
execution_config["_auth"] = (
_mask_auth(context.auth) if context is not None else {"auth_type": "none"}
)
return Scan(
project_id=project_id,
target_id=target.id,
engine_id=engine.id,
engine_name=engine.name,
context_id=context.id if context is not None else None,
context_name=context.name if context is not None else None,
execution_config=execution_config,
status=ScanStatus.PENDING.value,
subdomains_found=0,
ips_found=0,
open_ports_found=0,
vulnerabilities_found=0,
endpoints_found=0,
created_by=created_by,
schedule_id=schedule_id,
schedule_type=schedule_type,
scope=(ScanScope.FOCUSED.value if resolved.seed_only else ScanScope.FULL.value),
parent_scan_id=parent_scan_id,
run_group_id=run_group_id,
)
def build_scan_for_target_sync(
session,
*,
project_id: uuid.UUID,
target_id: uuid.UUID,
engine_id: uuid.UUID,
context_id: uuid.UUID | None,
created_by: uuid.UUID,
schedule_id: uuid.UUID | None = None,
schedule_type: str | None = None,
intensity: str | None = None,
) -> Scan:
"""Resolve engine/context/target on a sync Session and flush a PENDING Scan."""
from shared.models.proxy import Proxy # noqa: PLC0415
from shared.models.scan_context import ScanContext # noqa: PLC0415
from shared.models.scan_engine import ScanEngine # noqa: PLC0415
from shared.models.target import Target # noqa: PLC0415
engine = session.get(ScanEngine, engine_id)
if engine is None or engine.project_id != project_id:
msg = "scan engine not found"
raise ScanFactoryError(msg)
context = None
if context_id is not None:
context = session.get(ScanContext, context_id)
if context is None or context.project_id != project_id:
msg = "scan context not found"
raise ScanFactoryError(msg)
target = session.get(Target, target_id)
if target is None or target.project_id != project_id:
msg = "target not found"
raise ScanFactoryError(msg)
proxy_url = None
if context is not None and context.proxy_id is not None:
proxy_url = resolve_proxy_url(session.get(Proxy, context.proxy_id))
resolved = merge_engine_context(
engine,
context,
target.target_value,
target.target_type.value,
proxy_url=proxy_url,
intensity=intensity,
)
if target.seed_scans:
stored = target_seeds.load_sync(session, [target.id])
target_seeds.apply(resolved, stored.get(target.id) or [], seed_only=False)
scan = build_scan_row(
resolved=resolved,
engine=engine,
context=context,
target=target,
project_id=project_id,
created_by=created_by,
schedule_id=schedule_id,
schedule_type=schedule_type,
)
session.add(scan)
session.flush()
return scan