mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 15:21:30 +02:00
wafw00f's shard concurrency comes from the httpx transport at 1/25, so intensity and the context multipliers move it. The launch preview sums the weighted per-stage rate the footprint shows instead of listing the unweighted per-tool ceilings, and a passive-capable stage reports no rate under passive. The frontend mirror of the resolved config carries transports instead of two fields the backend no longer sends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq4xYJbgW5euUa9sCWBKFw
154 lines
5.6 KiB
Python
154 lines
5.6 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
|
|
and not spec.passive_capable
|
|
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 {}
|
|
quiet = resolved.intensity == Intensity.PASSIVE.value and spec.passive_capable
|
|
return PreviewTool(
|
|
capability=spec.name,
|
|
label=spec.title,
|
|
status=PreviewToolStatus.WILL_RUN,
|
|
rate=None if quiet else 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,
|
|
)
|