mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-21 07:41:28 +02:00
Second round of the copy sweep. The first pass covered page and panel
descriptions; this one covers the strings the pipeline emits at runtime and
the ones a broader detector found.
Em-dash fragments, which the house style rejects, survived in stage notes and
warnings — the text that lands in the stage timeline and the activity log.
"dnsx unavailable — no PTR lookup for 812 addresses" is now two sentences,
and so are the equivalents in subdomain discovery, http_probe, origin_probe,
screenshot, reverse_dns, target_enrichment, vulnerability_scan and the engine
preview warnings. Semicolon splices went the same way, in four stage config
descriptions, the body-search field help and the schedule timezone error.
Toasts: single-clause messages lose the trailing period to match the ninety
that already had none; four narrated rather than reported ("Judging this
scan. Refresh in a moment to see what it found.").
The AI page called one thing three names — "Written passages", "Cached
narratives", "cached passages". It is narratives everywhere, matching
cached_narratives and the ai_narratives table.
Checked and found clean: hedging adverbs, marketing adjectives, meta-asides,
first person, rhetorical questions, title case, and trailing periods on
label-style descriptions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016AeRG6G2FdSyWjG4AXPhBA
161 lines
5.9 KiB
Python
161 lines
5.9 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_global_threads,
|
|
_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.config import Scale
|
|
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 so a producer only feeds later 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. This stage sends traffic to the target."
|
|
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 the {kinds} this stage reads."
|
|
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."
|
|
)
|
|
|
|
scaled = spec.config_model.scaled_fields()
|
|
|
|
def _scaled(kind: Scale):
|
|
return next((values[f] for f, (s, _) in scaled.items() if s is kind), None)
|
|
|
|
return PreviewTool(
|
|
capability=spec.name,
|
|
label=spec.title,
|
|
status=PreviewToolStatus.WILL_RUN,
|
|
rate=_scaled(Scale.RATE),
|
|
threads=_scaled(Scale.THREADS),
|
|
timeout=_scaled(Scale.TIMEOUT),
|
|
)
|
|
|
|
|
|
class _DraftEngine:
|
|
def __init__(self, data) -> None:
|
|
self.intensity = data.intensity
|
|
self.global_threads = data.global_threads
|
|
self.global_http_crawl = True
|
|
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)
|
|
_validate_global_threads(data.global_threads)
|
|
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_threads=resolved.global_threads,
|
|
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,
|
|
)
|