Files
rengine/toolbox/tools/__init__.py
T
Yogesh OjhaandClaude Opus 5 e38ec4acc6 feat(toolbox): one-off lookups behind the wrench in the top bar
reNgine 2 had a Tools section; 3.0 lost it, so a hunter left the product
for a whois or a dns lookup even though every capability was already here
inside a scan.

`toolbox/` is a top-level package mounted into api and worker, and a tool
is one file under `toolbox/tools/` — auto-discovered exactly like stages,
report sections and MCP tools, with a pydantic `Input` whose flattened
schema IS the form the UI renders. Six ship: WHOIS, DNS records, subdomain
finder, HTTP probe, IP address and CVE.

Where a tool runs is a declaration the registry enforces. The api image
carries no tool binaries, so `INLINE` answers from RDAP, the offline range
tables or the threat feeds on the request's AsyncSession, and `QUEUED`
shells out on the worker; `_validate` rejects a class whose `run` is async
or sync the wrong way round, so the two cannot drift.

A run is a question rather than inventory, so it lives in Redis with a
seven-day TTL and no migration, keyed by user id. A queued run nobody
collects fails after two minutes instead of spinning for ever.

A tool answers in blocks — facts, table, tags, code, note — so one
frontend renderer serves every tool and a new one costs no frontend work.
An empty block with nothing to say about being empty is dropped.
`require_public` refuses loopback, private and link-local before the HTTP
probe sends anything, because an authenticated user typing 169.254.169.254
is an SSRF.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtRcxFCPSYxW5sXam3SUWB
2026-09-09 12:02:59 +05:30

41 lines
1.2 KiB
Python

"""Tools are discovered by module. A new tool is a file in this directory."""
from __future__ import annotations
import importlib
from functools import lru_cache
from pathlib import Path
from shared.logging import get_logger
from toolbox.base import Tool
logger = get_logger(__name__)
_SKIP = {"__init__"}
@lru_cache(maxsize=1)
def discover() -> dict[str, type[Tool]]:
found: dict[str, type[Tool]] = {}
for module in sorted(Path(__file__).parent.glob("*.py")):
if module.stem in _SKIP or module.stem.startswith("_"):
continue
try:
namespace = importlib.import_module(f"toolbox.tools.{module.stem}")
except Exception as exc:
logger.warning("toolbox tool skipped", module=module.stem, error=str(exc))
continue
for obj in vars(namespace).values():
if (
isinstance(obj, type)
and issubclass(obj, Tool)
and obj is not Tool
and not getattr(obj, "__abstractmethods__", None)
and getattr(obj, "name", None)
):
found.setdefault(obj.name, obj)
return found
__all__ = ["discover"]