mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-21 07:41:28 +02:00
A full static review of the 3.0 tree, run as two fan-out passes with every
finding verified against the running stack rather than taken on inspection.
Correctness and security
- instance settings encrypted the mask over the stored AI key: `!= MASK` never
matched `••••••••abcd`, so saving settings destroyed the key and `test_ai`
posted the mask as a bearer token
- the MFA token was replayable for its full five-minute life; the gate now
checks revocation and consumes the token after a successful exchange
- logout revoked nothing for a bearer client while answering "Logged out"
- `POST /api-keys/{id}/test` returned the plaintext key inside httpx's error
string, which embeds the whole URL
- an interest rule could be edited or deleted through another project's id
- redis ran the celery broker under `allkeys-lru`, free to evict a queued task
or a chord counter; now `noeviction`
- nuclei's stored command carried the OAST token and every auth header
- a finalize step could poison the session and leave a scan RUNNING for ever
- `is:issue` raised TypeError on every search
- offset paging repeated and skipped rows in four dimensions
- a cancelled run kept a stale `scans.*_found` rollup
- every PDF report raised `KeyError: 'software'`
- `version_key` truncated a wide numeric run and inverted release ordering
- psycopg raises a NUL as a bare ValueError, above StatementError, so every
savepoint guard was blind to the case it was written for
Performance
- endpoints.host had no trigram index: 35.9ms seq scan -> 0.389ms
- free text compiled to one OR, not the prescribed UNION ALL: 132ms -> 41ms
- `/http-assets/summary` and `/ips/summary` materialised whole tables
- the cross-link `ip` kind ran an estate query per page and drew nothing
Frontend
- `$effect` loaders that read the state they write, an unbounded request loop
on a failing endpoint
- error states that rendered as empty states
- 2FA backup codes were unusable while the copy said they worked
- a "Scan history" dialog that always reported none
- ~400 duplicated lines across five filter bars, now one component
Build and tooling
- every scanner installed at @latest; all 13 pinned to the measured versions
- CI built `web/`, gone since 2.x, with tags that cannot be valid
- the prettier pre-commit hook discarded its filenames and always passed
- the test suite destroyed itself when run twice
Also: subdomains from public code, via a pinned github-subdomains source and a
GitHub API key that subfinder receives too; sublist3r and oneforall removed
after measuring one unique host in 89 across four targets.
Verified: 853 tests, ruff clean over eleven packages, svelte-check 0 errors,
the count-is-a-promise gate at 131 checks and 0 mismatches, and 143 GET routes
with no 5xx. Four migrations, each with an exercised downgrade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVkdx5ynZ6aof5kLFczmhX
143 lines
5.0 KiB
Python
143 lines
5.0 KiB
Python
"""One finding with evidence and remediation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from pydantic import Field
|
|
|
|
from mcp import links
|
|
from mcp.context import ToolContext
|
|
from mcp.dimensions import dimension
|
|
from mcp.errors import ToolError
|
|
from mcp.result import ToolResult
|
|
from mcp.tools._scope import resolve
|
|
from mcp.tools.base import Tool, ToolGroup, ToolInput
|
|
from shared.definitions.surface import SurfaceDimension
|
|
from shared.utils.text import counted
|
|
|
|
MAX_LOCATIONS = 25
|
|
MAX_EVIDENCE_CHARS = 1200
|
|
_FINGERPRINT = re.compile(r"^[0-9a-f]{64}$")
|
|
_CVE = re.compile(r"^CVE-\d{4}-\d{4,}$", re.IGNORECASE)
|
|
|
|
|
|
class Input(ToolInput):
|
|
target: str = Field(description="The target the finding was reported on.")
|
|
finding: str = Field(
|
|
description=(
|
|
"A template id, check name, CVE or finding fingerprint, as returned by "
|
|
"query_assets in template_id, template_name, cve_ids or fingerprint."
|
|
)
|
|
)
|
|
|
|
|
|
class ExplainFinding(Tool):
|
|
name = "explain_finding"
|
|
title = "Explain finding"
|
|
group = ToolGroup.EXPLAIN.value
|
|
description = (
|
|
"One finding in full: what the check tests for, impact, remediation, CVE, "
|
|
"CWE, CVSS, EPSS and KEV signals, every location on the target, and the "
|
|
"triage decision."
|
|
)
|
|
Input = Input
|
|
examples = (
|
|
"explain_finding target=example.com finding=CVE-2021-44228",
|
|
"explain_finding target=example.com finding=apache-detect",
|
|
)
|
|
|
|
async def run(self, ctx: ToolContext, args: Input) -> ToolResult:
|
|
from app.services.vulnerability import VulnerabilityService # noqa: PLC0415
|
|
|
|
dim = dimension(SurfaceDimension.VULNERABILITIES.value)
|
|
scope = await resolve(ctx, args.target)
|
|
scan_id = scope.require(dim)
|
|
|
|
needle = args.finding.strip()
|
|
query = _query_for(needle)
|
|
f = dim.build_filter(query, limit=MAX_LOCATIONS, offset=0)
|
|
page = await dim.search(ctx.session, scan_id, f, scope.project_id)
|
|
|
|
if getattr(page, "error", None) or not page.items:
|
|
msg = (
|
|
f"No finding on {scope.target.target_value} matches {needle!r}. "
|
|
"query_assets with dimension=vulnerabilities lists the findings."
|
|
)
|
|
raise ToolError(msg)
|
|
|
|
service = VulnerabilityService(ctx.session)
|
|
detail = await service.get(scan_id, page.items[0].id)
|
|
if detail is None:
|
|
msg = "The finding could not be loaded."
|
|
raise ToolError(msg)
|
|
|
|
locations = [
|
|
{
|
|
"matched_at": row.matched_at,
|
|
"host": row.host,
|
|
"ip": row.ip,
|
|
"port": row.port,
|
|
"state": row.state,
|
|
"is_new": row.is_new,
|
|
}
|
|
for row in page.items
|
|
]
|
|
|
|
return ToolResult(
|
|
summary=(
|
|
f"{detail.severity.upper()}: {detail.template_name} on "
|
|
f"{scope.target.target_value}, {counted(page.total, 'occurrence')}"
|
|
),
|
|
data={
|
|
"check": {
|
|
"template_id": detail.template_id,
|
|
"name": detail.template_name,
|
|
"severity": detail.severity,
|
|
"scanner": detail.scanner,
|
|
"description": _trim(detail.description),
|
|
"impact": _trim(detail.impact),
|
|
"remediation": _trim(detail.remediation),
|
|
"references": (detail.references or [])[:8],
|
|
"tags": (detail.tags or [])[:12],
|
|
},
|
|
"risk": {
|
|
"cve_ids": detail.cve_ids or [],
|
|
"cwe_ids": detail.cwe_ids or [],
|
|
"cvss_score": detail.cvss_score,
|
|
"epss_score": detail.epss_score,
|
|
"epss_percentile": detail.epss_percentile,
|
|
"known_exploited": detail.is_kev,
|
|
},
|
|
"review": {"state": detail.state, "note": detail.note},
|
|
"occurrences": page.total,
|
|
"locations": locations,
|
|
"evidence": {
|
|
"matched_at": detail.matched_at,
|
|
"extracted": (detail.extracted_results or [])[:10],
|
|
"curl": _trim(detail.curl_command, 600),
|
|
},
|
|
},
|
|
pivot=links.scan_tab(
|
|
ctx.ui_base_url, scan_id, dim.tab, f"template:{detail.template_id}"
|
|
),
|
|
caveats=scope.caveat(dim),
|
|
untrusted=True,
|
|
)
|
|
|
|
|
|
def _query_for(needle: str) -> str:
|
|
quoted = needle.replace('"', '\\"')
|
|
if _FINGERPRINT.match(needle):
|
|
return f'name:"{quoted}" or template:"{quoted}"'
|
|
if _CVE.match(needle):
|
|
return f'cve:"{quoted}"'
|
|
return f'template:"{quoted}" or name:"{quoted}"'
|
|
|
|
|
|
def _trim(value: str | None, limit: int = MAX_EVIDENCE_CHARS) -> str | None:
|
|
if not value:
|
|
return None
|
|
text = value.strip()
|
|
return text if len(text) <= limit else text[:limit] + "…"
|