mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +02:00
Every stage config field is a decision; rate, concurrency, timeout and retries come from a per-tool intensity profile the stage reads as its transport. 173 fields become 83: 41 transport values, 40 constants and the dead ones leave the models, and the builder groups by capability with support stages as checkboxes and advanced fields behind one disclosure. Aggressive now changes what a tool is handed. Every project carries a built-in Default engine, seeded on project creation and on first list, undeletable: stage defaults, subfinder as the only passive source, vulnerability scan on with nuclei. From the tracing review: tool args can no longer restate a flag the stage set, the session check obeys the probe scheme, excluded scope reaches every stage that reads addresses, hosts or paths, the sitemap and source-map providers throttle at the transport rate, the blind tier obeys its switch, the fuzzing lanes split the budget, and the removed private-address and evidence switches are back as advanced fields. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq4xYJbgW5euUa9sCWBKFw
151 lines
5.5 KiB
Python
151 lines
5.5 KiB
Python
from fastapi import HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.services.scan_engine.validation import (
|
|
_validate_intensity,
|
|
_validate_stages,
|
|
)
|
|
from shared.definitions.launch import ASSET_KIND_LABELS, seed_produces
|
|
from shared.enums.scan import Intensity
|
|
from shared.enums.target import TargetType
|
|
from shared.models.scan_context import ScanContext
|
|
from shared.models.scan_engine import EnginePreviewResult, PreviewResolved
|
|
from shared.models.scan_preview import PreviewPhase, PreviewTool, PreviewToolStatus
|
|
from shared.services.scan_resolve import merge_engine_context
|
|
from stages.registry import ordered_levels, phases
|
|
|
|
|
|
def stage_effects(
|
|
resolved, configured: set[str]
|
|
) -> tuple[list[PreviewPhase], list[str]]:
|
|
warnings: list[str] = []
|
|
statuses = _stage_statuses(resolved, configured, warnings)
|
|
out = [
|
|
PreviewPhase(
|
|
phase=phase_name,
|
|
label=phase_name.replace("_", " ").title(),
|
|
tools=[statuses[spec.name] for spec in specs],
|
|
)
|
|
for phase_name, specs in phases()
|
|
]
|
|
return out, warnings
|
|
|
|
|
|
def _stage_statuses(
|
|
resolved, configured: set[str], warnings: list[str]
|
|
) -> dict[str, PreviewTool]:
|
|
"""One status per stage, walking levels."""
|
|
available = set(seed_produces(resolved.target_type))
|
|
out: dict[str, PreviewTool] = {}
|
|
for level in ordered_levels():
|
|
produced: set[str] = set()
|
|
for spec in level:
|
|
tool = _stage_status(spec, resolved, configured, available, warnings)
|
|
out[spec.name] = tool
|
|
if tool.status is PreviewToolStatus.WILL_RUN:
|
|
produced |= spec.produces
|
|
available |= produced
|
|
return out
|
|
|
|
|
|
def _stage_status(
|
|
spec, resolved, configured: set[str], available: set[str], warnings: list[str]
|
|
) -> PreviewTool:
|
|
values = resolved.stage(spec.name)
|
|
|
|
def _skip(status: PreviewToolStatus, reason: str) -> PreviewTool:
|
|
return PreviewTool(
|
|
capability=spec.name, label=spec.title, status=status, reason=reason
|
|
)
|
|
|
|
if not values.get("enabled", True):
|
|
reason = (
|
|
"Skipped at passive intensity."
|
|
if resolved.intensity == Intensity.PASSIVE.value and spec.touches_target
|
|
else "Disabled in engine."
|
|
)
|
|
return _skip(PreviewToolStatus.SKIPPED_DISABLED, reason)
|
|
if resolved.target_type not in spec.applies_to:
|
|
return _skip(
|
|
PreviewToolStatus.SKIPPED_NOT_APPLICABLE,
|
|
f"Not applicable to {resolved.target_type} targets.",
|
|
)
|
|
if spec.consumes and not (spec.consumes & available):
|
|
kinds = " or ".join(ASSET_KIND_LABELS[k] for k in sorted(spec.consumes))
|
|
reason = f"No earlier stage produces {kinds}."
|
|
warnings.append(f"{spec.title}: {reason}")
|
|
return _skip(PreviewToolStatus.SKIPPED_NO_INPUT, reason)
|
|
|
|
missing = [k for k in spec.api_keys if k not in configured]
|
|
if missing and len(missing) == len(spec.api_keys) and spec.requires_api_keys:
|
|
reason = f"{spec.title} skipped. API key not configured."
|
|
warnings.append(reason)
|
|
return _skip(PreviewToolStatus.SKIPPED_NEEDS_KEY, reason)
|
|
if missing:
|
|
warnings.append(
|
|
f"{spec.title}: no API key for {', '.join(missing)}. Reduced coverage."
|
|
)
|
|
|
|
transport = (resolved.transports or {}).get(spec.name) or {}
|
|
return PreviewTool(
|
|
capability=spec.name,
|
|
label=spec.title,
|
|
status=PreviewToolStatus.WILL_RUN,
|
|
rate=transport.get("rate"),
|
|
threads=transport.get("threads"),
|
|
timeout=transport.get("timeout"),
|
|
)
|
|
|
|
|
|
class _DraftEngine:
|
|
def __init__(self, data) -> None:
|
|
self.intensity = data.intensity
|
|
self.global_headers = []
|
|
self.tool_options = {}
|
|
self.stages = _validate_stages(data.stages)
|
|
|
|
|
|
async def preview_engine(
|
|
data, session: AsyncSession | None = None
|
|
) -> EnginePreviewResult:
|
|
if data.target_type not in {t.value for t in TargetType}:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Unknown target type '{data.target_type}'.",
|
|
)
|
|
_validate_intensity(data.intensity)
|
|
context: object | None = None
|
|
if data.context is not None:
|
|
context = data.context.model_dump()
|
|
elif data.context_id is not None and session is not None:
|
|
context = (
|
|
await session.execute(
|
|
select(ScanContext).where(ScanContext.id == data.context_id)
|
|
)
|
|
).scalar_one_or_none()
|
|
if context is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Scan context not found"
|
|
)
|
|
resolved = merge_engine_context(
|
|
_DraftEngine(data), context, "preview", data.target_type
|
|
)
|
|
phases, warnings = stage_effects(resolved, set())
|
|
return EnginePreviewResult(
|
|
phases=phases,
|
|
resolved_stages=resolved.stages,
|
|
resolved=PreviewResolved(
|
|
header_names=list(resolved.headers),
|
|
global_rate_limit_ceiling=resolved.global_rate_limit_ceiling,
|
|
per_tool_rate_limits=resolved.per_tool_rate_limits,
|
|
excluded_subdomains=resolved.excluded_subdomains,
|
|
excluded_paths=resolved.excluded_paths,
|
|
excluded_ips=resolved.excluded_ips,
|
|
included_subdomains=resolved.included_subdomains,
|
|
follow_redirects=resolved.follow_redirects,
|
|
http_protocol=resolved.http_protocol,
|
|
),
|
|
warnings=warnings,
|
|
)
|