mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-21 07:41:28 +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
161 lines
4.3 KiB
Python
161 lines
4.3 KiB
Python
"""Drop the transport and constant fields from stored engines.
|
|
|
|
Revision ID: b7d3e1f04a26
|
|
Revises: a4c2e7d91f53
|
|
Create Date: 2026-09-14
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "b7d3e1f04a26"
|
|
down_revision: str | None = "a4c2e7d91f53"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
_REMOVED: dict[str, set[str]] = {
|
|
"content_discovery": {"threads", "rate", "timeout"},
|
|
"dast_scan": {
|
|
"max_dirs_per_origin",
|
|
"dir_depth",
|
|
"fuzz_param_frequency",
|
|
"rate",
|
|
"threads",
|
|
"bulk_size",
|
|
"timeout",
|
|
"retries",
|
|
"max_host_error",
|
|
"waf_rate_divisor",
|
|
"honeypot_threshold",
|
|
"interactsh_server",
|
|
"oast_wait_minutes",
|
|
"store_evidence",
|
|
"max_evidence_recovery",
|
|
"max_findings",
|
|
},
|
|
"endpoint_probe": {"threads", "timeout", "rate", "follow_redirects"},
|
|
"host_discovery": {"rate", "threads", "timeout"},
|
|
"http_probe": {
|
|
"threads",
|
|
"timeout",
|
|
"rate",
|
|
"follow_redirects",
|
|
"probe_open_ports",
|
|
"max_ports_per_host",
|
|
},
|
|
"netblock_sweep": {
|
|
"min_addresses",
|
|
"max_asn_addresses",
|
|
"min_share",
|
|
"dns_threads",
|
|
"dns_timeout",
|
|
},
|
|
"origin_probe": {"threads", "timeout", "rate", "max_ports_per_address"},
|
|
"passive_ports": {"max_addresses"},
|
|
"port_scan": {"rate", "threads", "timeout", "retries", "port_threshold"},
|
|
"reverse_dns": {"dns_threads", "dns_timeout"},
|
|
"screenshot": {"threads", "rate", "timeout"},
|
|
"seed_resolution": {"cidr_skip_rfc1918"},
|
|
"service_fingerprint": {"threads", "timeout", "include_unknown"},
|
|
"session_check": {"timeout"},
|
|
"subdomain_discovery": {
|
|
"permutation_seeds",
|
|
"permutation_limit",
|
|
"dns_threads",
|
|
"dns_batch_size",
|
|
"dns_batch_concurrency",
|
|
"dns_idle_timeout",
|
|
"dns_retry_silent",
|
|
},
|
|
"target_enrichment": {"dns_threads", "dns_timeout"},
|
|
"url_discovery": {
|
|
"threads",
|
|
"timeout",
|
|
"rate",
|
|
"crawl_javascript",
|
|
"max_urls",
|
|
"max_known_file_hosts",
|
|
"max_source_maps",
|
|
"max_archive_domains",
|
|
},
|
|
"vhost": {"threads", "rate"},
|
|
"vulnerability_scan": {
|
|
"rate",
|
|
"threads",
|
|
"bulk_size",
|
|
"timeout",
|
|
"retries",
|
|
"max_host_error",
|
|
"waf_rate_divisor",
|
|
"honeypot_threshold",
|
|
"oast_wait_minutes",
|
|
"max_evidence_recovery",
|
|
"max_findings",
|
|
},
|
|
}
|
|
|
|
|
|
def _prune(stages: dict) -> tuple[dict, bool]:
|
|
changed = False
|
|
out: dict = {}
|
|
for name, config in (stages or {}).items():
|
|
if not isinstance(config, dict):
|
|
out[name] = config
|
|
continue
|
|
gone = _REMOVED.get(name, set())
|
|
kept = {k: v for k, v in config.items() if k not in gone}
|
|
if len(kept) != len(config):
|
|
changed = True
|
|
if kept:
|
|
out[name] = kept
|
|
elif config:
|
|
changed = True
|
|
return out, changed
|
|
|
|
|
|
def _yaml_mentions_removed(source: str | None) -> bool:
|
|
if not source:
|
|
return False
|
|
try:
|
|
import yaml # noqa: PLC0415
|
|
|
|
doc = yaml.safe_load(source) or {}
|
|
except Exception: # noqa: BLE001
|
|
return False
|
|
stages = doc.get("stages") if isinstance(doc, dict) else None
|
|
if not isinstance(stages, dict):
|
|
return False
|
|
return any(
|
|
isinstance(cfg, dict) and set(cfg) & _REMOVED.get(name, set())
|
|
for name, cfg in stages.items()
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
bind = op.get_bind()
|
|
rows = bind.execute(
|
|
sa.text("SELECT id, stages, yaml_source FROM scan_engines")
|
|
).all()
|
|
for id_, stages, yaml_source in rows:
|
|
raw = stages if isinstance(stages, dict) else json.loads(stages or "{}")
|
|
pruned, changed = _prune(raw)
|
|
drop_yaml = _yaml_mentions_removed(yaml_source)
|
|
if not changed and not drop_yaml:
|
|
continue
|
|
bind.execute(
|
|
sa.text(
|
|
"UPDATE scan_engines SET stages = :stages, "
|
|
"yaml_source = CASE WHEN :drop THEN NULL ELSE yaml_source END "
|
|
"WHERE id = :id"
|
|
),
|
|
{"stages": json.dumps(pruned), "drop": drop_yaml, "id": id_},
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
pass
|