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
327 lines
9.9 KiB
Python
327 lines
9.9 KiB
Python
"""The vocabulary of a scan-to-scan comparison: verbs, watched fields and named signals."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
from shared.definitions.surface import SurfaceDimension
|
|
|
|
|
|
class ChangeVerb(StrEnum):
|
|
APPEARED = "appeared"
|
|
CHANGED = "changed"
|
|
DISAPPEARED = "disappeared"
|
|
UNCONFIRMED = "unconfirmed"
|
|
UNCHANGED = "unchanged"
|
|
|
|
|
|
VERB_ORDER: tuple[str, ...] = (
|
|
ChangeVerb.APPEARED.value,
|
|
ChangeVerb.CHANGED.value,
|
|
ChangeVerb.DISAPPEARED.value,
|
|
ChangeVerb.UNCONFIRMED.value,
|
|
ChangeVerb.UNCHANGED.value,
|
|
)
|
|
|
|
LISTED_VERBS: tuple[str, ...] = VERB_ORDER[:-1]
|
|
|
|
|
|
class Comparability(StrEnum):
|
|
LIKE_FOR_LIKE = "like_for_like"
|
|
SETTINGS_DIFFER = "settings_differ"
|
|
QUALITY_DIFFERS = "quality_differs"
|
|
NOT_COVERED = "not_covered"
|
|
|
|
|
|
class FieldKind(StrEnum):
|
|
SCALAR = "scalar"
|
|
LIST = "list"
|
|
BOOL = "bool"
|
|
DATE = "date"
|
|
OPAQUE = "opaque"
|
|
|
|
|
|
class Tone(StrEnum):
|
|
UP = "up"
|
|
DOWN = "down"
|
|
NEUTRAL = "neutral"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WatchedField:
|
|
name: str
|
|
label: str
|
|
kind: str = FieldKind.SCALAR.value
|
|
|
|
|
|
# ---------- watched fields ----------
|
|
|
|
_HOST_FIELDS: tuple[WatchedField, ...] = (
|
|
WatchedField("http_status", "Status"),
|
|
WatchedField("final_url", "Final URL"),
|
|
WatchedField("page_title", "Title"),
|
|
WatchedField("webserver", "Server"),
|
|
WatchedField("waf", "WAF"),
|
|
WatchedField("cdn_name", "CDN"),
|
|
WatchedField("cname", "CNAME"),
|
|
WatchedField("favicon_hash", "Favicon", FieldKind.OPAQUE.value),
|
|
WatchedField("is_active", "Responding", FieldKind.BOOL.value),
|
|
WatchedField("tls_expired", "Certificate expired", FieldKind.BOOL.value),
|
|
WatchedField("tls_self_signed", "Self-signed", FieldKind.BOOL.value),
|
|
WatchedField("tls_not_after", "Certificate expiry", FieldKind.DATE.value),
|
|
WatchedField("tech", "Technology", FieldKind.LIST.value),
|
|
WatchedField("hygiene_issues", "Hygiene", FieldKind.LIST.value),
|
|
WatchedField("resolved_ips", "Addresses", FieldKind.LIST.value),
|
|
)
|
|
|
|
_ENDPOINT_FIELDS: tuple[WatchedField, ...] = (
|
|
WatchedField("status_code", "Status"),
|
|
WatchedField("content_hash", "Body", FieldKind.OPAQUE.value),
|
|
WatchedField("content_type", "Content type"),
|
|
WatchedField("redirect_location", "Redirects to"),
|
|
WatchedField("param_count", "Parameters"),
|
|
WatchedField("is_probed", "Probed", FieldKind.BOOL.value),
|
|
WatchedField("tech", "Technology", FieldKind.LIST.value),
|
|
WatchedField("interest", "Interest", FieldKind.LIST.value),
|
|
)
|
|
|
|
_SERVICE_FIELDS: tuple[WatchedField, ...] = (
|
|
WatchedField("state", "State"),
|
|
WatchedField("service_name", "Service"),
|
|
WatchedField("service_class", "Class"),
|
|
WatchedField("product", "Product"),
|
|
WatchedField("version", "Version"),
|
|
WatchedField("banner", "Banner", FieldKind.OPAQUE.value),
|
|
WatchedField("is_http", "HTTP", FieldKind.BOOL.value),
|
|
WatchedField("tls", "TLS", FieldKind.BOOL.value),
|
|
WatchedField("cpe", "CPE", FieldKind.LIST.value),
|
|
)
|
|
|
|
_ADDRESS_FIELDS: tuple[WatchedField, ...] = (
|
|
WatchedField("asn", "ASN"),
|
|
WatchedField("asn_org", "Network"),
|
|
WatchedField("prefix", "Prefix"),
|
|
WatchedField("country", "Country"),
|
|
WatchedField("cdn_name", "CDN"),
|
|
WatchedField("is_cdn", "Behind CDN", FieldKind.BOOL.value),
|
|
WatchedField("is_alive", "Responding", FieldKind.BOOL.value),
|
|
WatchedField("ptr_hostnames", "PTR", FieldKind.LIST.value),
|
|
)
|
|
|
|
_VULN_FIELDS: tuple[WatchedField, ...] = (
|
|
WatchedField("severity", "Severity"),
|
|
WatchedField("template_available", "Template available", FieldKind.BOOL.value),
|
|
WatchedField("extracted_results", "Extracted", FieldKind.LIST.value),
|
|
)
|
|
|
|
_SOFTWARE_FIELDS: tuple[WatchedField, ...] = (
|
|
WatchedField("severity", "Severity"),
|
|
WatchedField("confidence", "Confidence"),
|
|
WatchedField("is_kev", "Known exploited", FieldKind.BOOL.value),
|
|
WatchedField("caveats", "Caveats", FieldKind.LIST.value),
|
|
)
|
|
|
|
WATCHED_FIELDS: dict[str, tuple[WatchedField, ...]] = {
|
|
SurfaceDimension.WEB_ASSETS.value: _HOST_FIELDS,
|
|
SurfaceDimension.ENDPOINTS.value: _ENDPOINT_FIELDS,
|
|
SurfaceDimension.SERVICES.value: _SERVICE_FIELDS,
|
|
SurfaceDimension.IPS.value: _ADDRESS_FIELDS,
|
|
SurfaceDimension.VULNERABILITIES.value: _VULN_FIELDS,
|
|
SurfaceDimension.SOFTWARE.value: _SOFTWARE_FIELDS,
|
|
}
|
|
|
|
# Class D: moves without a scan
|
|
INTEL_FIELDS: tuple[str, ...] = (
|
|
"is_kev",
|
|
"kev_ransomware",
|
|
"kev_due_date",
|
|
"epss_score",
|
|
"epss_percentile",
|
|
"exploit_score",
|
|
"intel_kinds",
|
|
"poc_count",
|
|
)
|
|
|
|
|
|
# ---------- identity ----------
|
|
|
|
COMPARE_KEYS: dict[str, tuple[str, ...]] = {
|
|
SurfaceDimension.WEB_ASSETS.value: ("name",),
|
|
SurfaceDimension.ENDPOINTS.value: ("signature",),
|
|
SurfaceDimension.SERVICES.value: ("ip", "number", "protocol"),
|
|
SurfaceDimension.IPS.value: ("ip",),
|
|
SurfaceDimension.VULNERABILITIES.value: ("fingerprint",),
|
|
SurfaceDimension.SOFTWARE.value: ("fingerprint",),
|
|
}
|
|
|
|
|
|
# ---------- named signals ----------
|
|
|
|
|
|
class ChangeSignal(StrEnum):
|
|
KEV_APPEARED = "kev_appeared"
|
|
CRITICAL_APPEARED = "critical_appeared"
|
|
FINDING_APPEARED = "finding_appeared"
|
|
SEVERITY_RAISED = "severity_raised"
|
|
SENSITIVE_SERVICE_OPENED = "sensitive_service_opened"
|
|
AUTH_DROPPED = "auth_dropped"
|
|
SERVICE_OPENED = "service_opened"
|
|
CERT_EXPIRED = "cert_expired"
|
|
WAF_GONE = "waf_gone"
|
|
CDN_GONE = "cdn_gone"
|
|
HOSTING_MOVED = "hosting_moved"
|
|
HOST_WOKE = "host_woke"
|
|
BODY_CHANGED = "body_changed"
|
|
EXPOSED_HOST_APPEARED = "exposed_host_appeared"
|
|
HOST_APPEARED = "host_appeared"
|
|
ADDRESS_APPEARED = "address_appeared"
|
|
ENDPOINT_APPEARED = "endpoint_appeared"
|
|
ATTRIBUTES_CHANGED = "attributes_changed"
|
|
FINDING_GONE = "finding_gone"
|
|
SERVICE_CLOSED = "service_closed"
|
|
HOST_GONE = "host_gone"
|
|
ASSET_GONE = "asset_gone"
|
|
|
|
|
|
# lower ranks first
|
|
SIGNAL_RANK: dict[str, int] = {
|
|
ChangeSignal.KEV_APPEARED.value: 0,
|
|
ChangeSignal.CRITICAL_APPEARED.value: 1,
|
|
ChangeSignal.SENSITIVE_SERVICE_OPENED.value: 2,
|
|
ChangeSignal.AUTH_DROPPED.value: 3,
|
|
ChangeSignal.FINDING_APPEARED.value: 4,
|
|
ChangeSignal.SEVERITY_RAISED.value: 5,
|
|
ChangeSignal.CERT_EXPIRED.value: 6,
|
|
ChangeSignal.WAF_GONE.value: 7,
|
|
ChangeSignal.EXPOSED_HOST_APPEARED.value: 8,
|
|
ChangeSignal.HOSTING_MOVED.value: 9,
|
|
ChangeSignal.HOST_WOKE.value: 10,
|
|
ChangeSignal.CDN_GONE.value: 11,
|
|
ChangeSignal.SERVICE_OPENED.value: 12,
|
|
ChangeSignal.BODY_CHANGED.value: 13,
|
|
ChangeSignal.HOST_APPEARED.value: 14,
|
|
ChangeSignal.ADDRESS_APPEARED.value: 15,
|
|
ChangeSignal.ENDPOINT_APPEARED.value: 16,
|
|
ChangeSignal.ATTRIBUTES_CHANGED.value: 17,
|
|
ChangeSignal.FINDING_GONE.value: 18,
|
|
ChangeSignal.SERVICE_CLOSED.value: 19,
|
|
ChangeSignal.HOST_GONE.value: 20,
|
|
ChangeSignal.ASSET_GONE.value: 21,
|
|
}
|
|
|
|
# ---------- run-level facets ----------
|
|
|
|
|
|
class FacetKind(StrEnum):
|
|
SCALAR = "scalar"
|
|
LIST = "list"
|
|
BOOL = "bool"
|
|
COUNT = "count"
|
|
MASKED = "masked"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RunFacet:
|
|
key: str
|
|
label: str
|
|
path: str
|
|
kind: str = FacetKind.SCALAR.value
|
|
material: bool = True
|
|
|
|
|
|
RUN_FACETS: tuple[RunFacet, ...] = (
|
|
RunFacet("auth", "Authentication", "_auth.auth_type"),
|
|
RunFacet("headers", "Extra headers", "headers", FacetKind.COUNT.value),
|
|
RunFacet("intensity", "Intensity", "intensity"),
|
|
RunFacet("proxy", "Proxy", "proxy_url", FacetKind.MASKED.value),
|
|
RunFacet("http_protocol", "HTTP protocol", "http_protocol"),
|
|
RunFacet(
|
|
"excluded_subdomains",
|
|
"Excluded hosts",
|
|
"excluded_subdomains",
|
|
FacetKind.LIST.value,
|
|
),
|
|
RunFacet(
|
|
"excluded_paths", "Excluded paths", "excluded_paths", FacetKind.LIST.value
|
|
),
|
|
RunFacet(
|
|
"excluded_ips", "Excluded addresses", "excluded_ips", FacetKind.LIST.value
|
|
),
|
|
RunFacet(
|
|
"included_subdomains",
|
|
"Only these hosts",
|
|
"included_subdomains",
|
|
FacetKind.LIST.value,
|
|
),
|
|
RunFacet("seed_assets", "Seed assets", "seed_assets", FacetKind.COUNT.value),
|
|
RunFacet(
|
|
"rate_ceiling",
|
|
"Global rate limit",
|
|
"global_rate_limit_ceiling",
|
|
material=False,
|
|
),
|
|
RunFacet(
|
|
"thread_multiplier", "Thread multiplier", "thread_multiplier", material=False
|
|
),
|
|
RunFacet(
|
|
"timeout_multiplier", "Timeout multiplier", "timeout_multiplier", material=False
|
|
),
|
|
)
|
|
|
|
|
|
# ---------- why a pair cannot be compared ----------
|
|
|
|
|
|
class Refusal(StrEnum):
|
|
DIFFERENT_TARGET = "different_target"
|
|
UNFINISHED = "unfinished"
|
|
FOCUSED_AGAINST_FULL = "focused_against_full"
|
|
FULL_AGAINST_FOCUSED = "full_against_focused"
|
|
SAME_RUN = "same_run"
|
|
NO_EARLIER_RUN = "no_earlier_run"
|
|
NEED_ONE_MORE = "need_one_more"
|
|
TOO_MANY = "too_many"
|
|
|
|
|
|
REFUSAL_REASON: dict[str, str] = {
|
|
Refusal.DIFFERENT_TARGET.value: "Both runs must cover the same target.",
|
|
Refusal.UNFINISHED.value: "This run has not finished.",
|
|
Refusal.FOCUSED_AGAINST_FULL.value: (
|
|
"A focused run is compared with its parent run only. Open the focused run."
|
|
),
|
|
Refusal.FULL_AGAINST_FOCUSED.value: (
|
|
"A full run cannot be compared with a focused run. Select two full runs."
|
|
),
|
|
Refusal.SAME_RUN.value: "Pick two different runs.",
|
|
Refusal.NO_EARLIER_RUN.value: "No earlier run of this target to compare with.",
|
|
Refusal.NEED_ONE_MORE.value: "Select one more run of the same target.",
|
|
Refusal.TOO_MANY.value: "Select exactly two runs.",
|
|
}
|
|
|
|
MAX_RUN_DIFFERENCES = 12
|
|
|
|
|
|
# ---------- unified diff ----------
|
|
|
|
DIFF_MARK: dict[str, str] = {
|
|
ChangeVerb.APPEARED.value: "+",
|
|
ChangeVerb.CHANGED.value: "~",
|
|
ChangeVerb.DISAPPEARED.value: "-",
|
|
ChangeVerb.UNCONFIRMED.value: "?",
|
|
ChangeVerb.UNCHANGED.value: " ",
|
|
}
|
|
|
|
MAX_DIFF_LINES = 5000
|
|
DIFF_PAGE = 200
|
|
DIFF_FIELD_INDENT = " "
|
|
|
|
DEFAULT_RANK = 99
|
|
|
|
AUTH_STATUS: tuple[int, ...] = (401, 403)
|
|
OK_MIN = 200
|
|
OK_MAX = 400
|
|
MAX_ROWS_PER_PAGE = 100
|
|
DEFAULT_ROWS_PER_PAGE = 50
|
|
MAX_COMPARABLE_RUNS = 40
|