mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +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
522 lines
16 KiB
Python
522 lines
16 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
from pydantic import Field as PydanticField
|
|
from sqlalchemy import Column, Text
|
|
from sqlalchemy.types import JSON
|
|
from sqlmodel import Field, Index, SQLModel, UniqueConstraint
|
|
|
|
from shared.definitions.asset_query import MAX_QUERY_LENGTH
|
|
from shared.definitions.endpoints import (
|
|
MAX_FILENAME_LENGTH,
|
|
MAX_HOST_LENGTH,
|
|
MAX_PATH_LENGTH,
|
|
MAX_URL_LENGTH,
|
|
EndpointClass,
|
|
EndpointSource,
|
|
FolderGlyph,
|
|
)
|
|
from shared.definitions.vulnerabilities import CoverageStatus
|
|
from shared.models.asset_query import MatchEvidence, QueryError
|
|
from shared.utils.datetime import utc_now
|
|
|
|
|
|
def _json_list() -> Field:
|
|
return Field(default_factory=list, sa_column=Column(JSON, nullable=False))
|
|
|
|
|
|
def _json_dict() -> Field:
|
|
return Field(default_factory=dict, sa_column=Column(JSON, nullable=False))
|
|
|
|
|
|
def _text() -> Field:
|
|
return Field(default=None, sa_column=Column(Text, nullable=True))
|
|
|
|
|
|
class Endpoint(SQLModel, table=True):
|
|
__tablename__ = "endpoints"
|
|
__table_args__ = (
|
|
UniqueConstraint("scan_id", "signature", name="uq_endpoint_scan_signature"),
|
|
Index("ix_endpoints_scan_family", "scan_id", "family"),
|
|
)
|
|
|
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
scan_id: uuid.UUID = Field(foreign_key="scans.id", index=True, ondelete="CASCADE")
|
|
target_id: uuid.UUID = Field(
|
|
foreign_key="targets.id", index=True, ondelete="CASCADE"
|
|
)
|
|
project_id: uuid.UUID = Field(foreign_key="projects.id", index=True)
|
|
|
|
signature: str = Field(max_length=64, index=True)
|
|
family: str = Field(default="", max_length=64)
|
|
|
|
# where it is
|
|
url: str = Field(max_length=MAX_URL_LENGTH)
|
|
host: str = Field(max_length=MAX_HOST_LENGTH, index=True)
|
|
port: int = Field(default=443)
|
|
scheme: str = Field(default="https", max_length=8)
|
|
path: str = Field(max_length=MAX_PATH_LENGTH)
|
|
shape: str = Field(default="", max_length=MAX_PATH_LENGTH)
|
|
dir_path: str = Field(max_length=MAX_PATH_LENGTH, index=True)
|
|
filename: str | None = Field(default=None, max_length=MAX_FILENAME_LENGTH)
|
|
extension: str | None = Field(default=None, max_length=10, index=True)
|
|
depth: int = Field(default=0, index=True)
|
|
|
|
# what it accepts
|
|
params: list = _json_list()
|
|
param_count: int = Field(default=0, index=True)
|
|
param_samples: list = _json_list()
|
|
variants: int = Field(default=1)
|
|
more_variants: bool = Field(default=False)
|
|
methods: list = _json_list()
|
|
|
|
# who says so
|
|
sources: list = _json_list()
|
|
primary_source: str = Field(
|
|
default=EndpointSource.OTHER.value, max_length=24, index=True
|
|
)
|
|
discovery: dict = _json_dict()
|
|
found_on: str | None = Field(default=None, max_length=2000)
|
|
|
|
# what it answered, if anything asked
|
|
is_probed: bool = Field(default=False, index=True)
|
|
status_code: int | None = Field(default=None, index=True)
|
|
content_type: str | None = Field(default=None, max_length=255)
|
|
content_length: int | None = Field(default=None)
|
|
title: str | None = Field(default=None, max_length=1000)
|
|
words: int | None = Field(default=None)
|
|
lines: int | None = Field(default=None)
|
|
response_time: float | None = Field(default=None)
|
|
redirect_location: str | None = Field(default=None, max_length=2000)
|
|
content_hash: str | None = Field(default=None, max_length=80, index=True)
|
|
tech: list = _json_list()
|
|
|
|
# how it reads
|
|
endpoint_class: str = Field(
|
|
default=EndpointClass.OTHER.value, max_length=16, index=True
|
|
)
|
|
interest: list = _json_list()
|
|
|
|
http_asset_id: uuid.UUID | None = Field(default=None, index=True)
|
|
subdomain_id: uuid.UUID | None = Field(default=None, index=True)
|
|
|
|
archive_last_seen: datetime | None = Field(default=None)
|
|
discovered_at: datetime = Field(default_factory=utc_now, index=True)
|
|
created_at: datetime = Field(default_factory=utc_now)
|
|
|
|
|
|
class EndpointCoverage(SQLModel, table=True):
|
|
"""One provider's account of what it covered."""
|
|
|
|
__tablename__ = "endpoint_coverage"
|
|
|
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
|
scan_id: uuid.UUID = Field(foreign_key="scans.id", index=True, ondelete="CASCADE")
|
|
target_id: uuid.UUID = Field(
|
|
foreign_key="targets.id", index=True, ondelete="CASCADE"
|
|
)
|
|
project_id: uuid.UUID = Field(foreign_key="projects.id", index=True)
|
|
|
|
source: str = Field(max_length=24, index=True)
|
|
tool: str | None = Field(default=None, max_length=40)
|
|
status: str = Field(default=CoverageStatus.COMPLETED.value, max_length=16)
|
|
|
|
hosts_total: int = Field(default=0)
|
|
hosts_scanned: int | None = Field(default=None)
|
|
hosts_dropped: list = _json_list()
|
|
urls_found: int | None = Field(default=None)
|
|
urls_stored: int | None = Field(default=None)
|
|
urls_probed: int | None = Field(default=None)
|
|
pages_fetched: int | None = Field(default=None)
|
|
depth_reached: int | None = Field(default=None)
|
|
errors: int | None = Field(default=None)
|
|
capped: bool = Field(default=False)
|
|
cap_reason: str | None = Field(default=None, max_length=200)
|
|
urls_dropped: dict = _json_dict()
|
|
|
|
command: str | None = _text()
|
|
error: str | None = Field(default=None, max_length=2000)
|
|
|
|
started_at: datetime = Field(default_factory=utc_now)
|
|
ended_at: datetime | None = Field(default=None)
|
|
duration_seconds: float | None = Field(default=None)
|
|
|
|
|
|
class SourceEvidence(BaseModel):
|
|
"""Why one provider believes this endpoint exists."""
|
|
|
|
source: str
|
|
label: str
|
|
kind: str
|
|
detail: str | None = None
|
|
found_on: str | None = None
|
|
observed_at: datetime | None = None
|
|
|
|
|
|
class EndpointRead(BaseModel):
|
|
id: uuid.UUID
|
|
scan_id: uuid.UUID
|
|
target_id: uuid.UUID
|
|
target_value: str | None = None
|
|
signature: str
|
|
url: str
|
|
host: str
|
|
port: int
|
|
scheme: str
|
|
path: str
|
|
dir_path: str
|
|
filename: str | None = None
|
|
extension: str | None = None
|
|
depth: int
|
|
params: list[str] = PydanticField(default_factory=list)
|
|
param_count: int = 0
|
|
variants: int = 1
|
|
more_variants: bool = False
|
|
methods: list[str] = PydanticField(default_factory=list)
|
|
sources: list[str] = PydanticField(default_factory=list)
|
|
primary_source: str
|
|
evidence: list[SourceEvidence] = PydanticField(default_factory=list)
|
|
found_on: str | None = None
|
|
is_probed: bool = False
|
|
status_code: int | None = None
|
|
content_type: str | None = None
|
|
content_length: int | None = None
|
|
title: str | None = None
|
|
words: int | None = None
|
|
lines: int | None = None
|
|
response_time: float | None = None
|
|
redirect_location: str | None = None
|
|
tech: list[str] = PydanticField(default_factory=list)
|
|
endpoint_class: str
|
|
interest: list[str] = PydanticField(default_factory=list)
|
|
http_asset_id: uuid.UUID | None = None
|
|
subdomain_id: uuid.UUID | None = None
|
|
archive_last_seen: datetime | None = None
|
|
discovered_at: datetime
|
|
is_new: bool = False
|
|
matched_in: list[MatchEvidence] = PydanticField(default_factory=list)
|
|
|
|
|
|
class EndpointDetail(EndpointRead):
|
|
param_samples: list = PydanticField(default_factory=list)
|
|
discovery: dict = PydanticField(default_factory=dict)
|
|
content_hash: str | None = None
|
|
siblings: int = 0
|
|
|
|
|
|
class EndpointFilter(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
q: str | None = PydanticField(default=None, max_length=MAX_QUERY_LENGTH)
|
|
host: str | None = None
|
|
dir_path: str | None = None
|
|
subtree: bool = True
|
|
endpoint_class: str | None = None
|
|
source: str | None = None
|
|
interest: str | None = None
|
|
status_class: str | None = None
|
|
probed: bool | None = None
|
|
new: bool = False
|
|
hide_static: bool = False
|
|
hide_root_only: bool = False
|
|
sort: str = "path"
|
|
direction: str = "asc"
|
|
page: int = 1
|
|
size: int = 50
|
|
|
|
def has_facets(self) -> bool:
|
|
return any(
|
|
(
|
|
self.host,
|
|
self.dir_path,
|
|
self.endpoint_class,
|
|
self.source,
|
|
self.interest,
|
|
self.status_class,
|
|
self.probed is not None,
|
|
self.new,
|
|
self.hide_static,
|
|
)
|
|
)
|
|
|
|
|
|
class EndpointPage(BaseModel):
|
|
items: list[EndpointRead] = PydanticField(default_factory=list)
|
|
total: int = 0
|
|
total_capped: bool = False
|
|
page: int = 1
|
|
size: int = 50
|
|
error: QueryError | None = None
|
|
|
|
|
|
class EndpointFacet(BaseModel):
|
|
value: str
|
|
label: str
|
|
count: int
|
|
|
|
|
|
class EndpointFacets(BaseModel):
|
|
endpoint_class: list[EndpointFacet] = PydanticField(default_factory=list)
|
|
source: list[EndpointFacet] = PydanticField(default_factory=list)
|
|
interest: list[EndpointFacet] = PydanticField(default_factory=list)
|
|
status_class: list[EndpointFacet] = PydanticField(default_factory=list)
|
|
extension: list[EndpointFacet] = PydanticField(default_factory=list)
|
|
host: list[EndpointFacet] = PydanticField(default_factory=list)
|
|
total: int = 0
|
|
static_total: int = 0
|
|
|
|
|
|
class TreeLeaf(BaseModel):
|
|
"""The one endpoint a folder holds when that folder is only its own index."""
|
|
|
|
id: uuid.UUID
|
|
url: str
|
|
host: str
|
|
path: str
|
|
params: list[str] = PydanticField(default_factory=list)
|
|
param_count: int = 0
|
|
endpoint_class: str
|
|
is_probed: bool = False
|
|
status_code: int | None = None
|
|
content_length: int | None = None
|
|
sources: list[str] = PydanticField(default_factory=list)
|
|
interest: list[str] = PydanticField(default_factory=list)
|
|
|
|
|
|
class FolderChip(BaseModel):
|
|
"""One top-level folder of a host, sized and toned."""
|
|
|
|
name: str
|
|
path: str
|
|
count: int = 0
|
|
glyph: str = FolderGlyph.FOLDER.value
|
|
archive_only: bool = False
|
|
query: str
|
|
|
|
|
|
class HostIdentity(BaseModel):
|
|
"""What the host's own HTTP asset says it is."""
|
|
|
|
status_code: int | None = None
|
|
title: str | None = None
|
|
tech: list[str] = PydanticField(default_factory=list)
|
|
http_asset_id: uuid.UUID | None = None
|
|
|
|
|
|
class TreeNode(BaseModel):
|
|
"""One directory or host in the site tree, counted through the same filter as the table."""
|
|
|
|
key: str
|
|
name: str
|
|
path: str
|
|
host: str | None = None
|
|
kind: str = "directory"
|
|
depth: int = 0
|
|
direct_count: int = 0
|
|
subtree_count: int = 0
|
|
child_count: int = 0
|
|
hosts: int = 1
|
|
status_mix: dict[str, int] = PydanticField(default_factory=dict)
|
|
class_mix: dict[str, int] = PydanticField(default_factory=dict)
|
|
sources: list[str] = PydanticField(default_factory=list)
|
|
interest: list[str] = PydanticField(default_factory=list)
|
|
has_params: bool = False
|
|
params: int = 0
|
|
verified: int = 0
|
|
unprobed: int = 0
|
|
new_count: int = 0
|
|
gone_count: int = 0
|
|
anomaly: str | None = None
|
|
archive_only: bool = False
|
|
glyph: str = FolderGlyph.FOLDER.value
|
|
sample_url: str | None = None
|
|
leaf: TreeLeaf | None = None
|
|
query: str
|
|
children: list["TreeNode"] = PydanticField(default_factory=list)
|
|
lazy: bool = False
|
|
folders: int = 0
|
|
top_folders: list[str] = PydanticField(default_factory=list)
|
|
chips: list[FolderChip] = PydanticField(default_factory=list)
|
|
api: int = 0
|
|
walled: int = 0
|
|
unfiltered_count: int = 0
|
|
identity: HostIdentity | None = None
|
|
|
|
|
|
class HostPage(BaseModel):
|
|
"""One page of hosts with their rollups, ranked by attack surface."""
|
|
|
|
items: list[TreeNode] = PydanticField(default_factory=list)
|
|
total: int = 0
|
|
total_endpoints: int = 0
|
|
root_only: int = 0
|
|
page: int = 1
|
|
size: int = 50
|
|
error: QueryError | None = None
|
|
|
|
|
|
class ParamStat(BaseModel):
|
|
name: str
|
|
count: int = 0
|
|
interest: str | None = None
|
|
|
|
|
|
class HostBrief(BaseModel):
|
|
"""The header of one host's sitemap: identity, facts that pivot, and the parameter surface."""
|
|
|
|
host: str
|
|
identity: HostIdentity | None = None
|
|
total: int = 0
|
|
probed: int = 0
|
|
live: int = 0
|
|
with_params: int = 0
|
|
api: int = 0
|
|
walled: int = 0
|
|
interesting: int = 0
|
|
new: int = 0
|
|
gone: int = 0
|
|
findings: int = 0
|
|
previous_scan_at: datetime | None = None
|
|
params: list[ParamStat] = PydanticField(default_factory=list)
|
|
params_total: int = 0
|
|
by_class: dict[str, int] = PydanticField(default_factory=dict)
|
|
static_total: int = 0
|
|
|
|
|
|
class MergedLeaf(BaseModel):
|
|
"""One path inside a folder, folded across every host that serves it."""
|
|
|
|
key: str
|
|
path: str
|
|
name: str
|
|
params: list[str] = PydanticField(default_factory=list)
|
|
param_count: int = 0
|
|
endpoint_class: str
|
|
hosts: int = 0
|
|
endpoints: int = 0
|
|
status_mix: dict[str, int] = PydanticField(default_factory=dict)
|
|
unprobed: int = 0
|
|
interest: list[str] = PydanticField(default_factory=list)
|
|
sources: list[str] = PydanticField(default_factory=list)
|
|
host_names: list[str] = PydanticField(default_factory=list)
|
|
new_count: int = 0
|
|
sample_id: uuid.UUID
|
|
sample_url: str
|
|
sample_status: int | None = None
|
|
query: str
|
|
|
|
|
|
class MergedLeafPage(BaseModel):
|
|
items: list[MergedLeaf] = PydanticField(default_factory=list)
|
|
total: int = 0
|
|
truncated: bool = False
|
|
|
|
|
|
class EndpointTree(BaseModel):
|
|
mode: str = "host"
|
|
nodes: list[TreeNode] = PydanticField(default_factory=list)
|
|
total_endpoints: int = 0
|
|
total_nodes: int = 0
|
|
truncated: bool = False
|
|
error: QueryError | None = None
|
|
|
|
|
|
class CoverageRead(BaseModel):
|
|
id: uuid.UUID
|
|
source: str
|
|
label: str
|
|
tool: str | None = None
|
|
status: str
|
|
hosts_total: int = 0
|
|
hosts_scanned: int | None = None
|
|
hosts_dropped: list[str] = PydanticField(default_factory=list)
|
|
urls_found: int | None = None
|
|
urls_stored: int | None = None
|
|
urls_probed: int | None = None
|
|
pages_fetched: int | None = None
|
|
depth_reached: int | None = None
|
|
errors: int | None = None
|
|
capped: bool = False
|
|
cap_reason: str | None = None
|
|
urls_dropped: dict[str, int] = PydanticField(default_factory=dict)
|
|
error: str | None = None
|
|
started_at: datetime
|
|
ended_at: datetime | None = None
|
|
duration_seconds: float | None = None
|
|
|
|
|
|
class EndpointSummary(BaseModel):
|
|
total: int = 0
|
|
probed: int = 0
|
|
live: int = 0
|
|
with_params: int = 0
|
|
interesting: int = 0
|
|
hosts: int = 0
|
|
new: int = 0
|
|
gone: int = 0
|
|
previous_scan_id: uuid.UUID | None = None
|
|
previous_scan_at: datetime | None = None
|
|
by_class: dict[str, int] = PydanticField(default_factory=dict)
|
|
by_source: dict[str, int] = PydanticField(default_factory=dict)
|
|
|
|
|
|
class GonePage(EndpointPage):
|
|
"""Endpoints the previous scan of this target recorded and this scan did not."""
|
|
|
|
previous_scan_id: uuid.UUID | None = None
|
|
previous_scan_at: datetime | None = None
|
|
|
|
|
|
class StructureFinding(BaseModel):
|
|
"""A structural fact about the surface."""
|
|
|
|
kind: str
|
|
label: str
|
|
detail: str
|
|
count: int
|
|
query: str
|
|
samples: list[str] = PydanticField(default_factory=list)
|
|
|
|
|
|
class PathSpread(BaseModel):
|
|
path: str
|
|
hosts: int
|
|
endpoints: int
|
|
query: str
|
|
|
|
|
|
class StructureLine(BaseModel):
|
|
key: str
|
|
label: str
|
|
detail: str | None = None
|
|
count: int
|
|
hosts: int = 0
|
|
query: str
|
|
|
|
|
|
class ScanStructure(BaseModel):
|
|
endpoints: int = 0
|
|
hosts: int = 0
|
|
probed: int = 0
|
|
directories: int = 0
|
|
max_depth: int = 0
|
|
with_params: int = 0
|
|
headline: str | None = None
|
|
findings: list[StructureFinding] = PydanticField(default_factory=list)
|
|
shared_paths: list[PathSpread] = PydanticField(default_factory=list)
|
|
interest: list[StructureLine] = PydanticField(default_factory=list)
|
|
by_class: list[StructureLine] = PydanticField(default_factory=list)
|
|
by_source: list[StructureLine] = PydanticField(default_factory=list)
|
|
|
|
|
|
class VerifyBranchRequest(BaseModel):
|
|
host: str = PydanticField(max_length=500)
|
|
dir_path: str | None = PydanticField(default=None, max_length=1500)
|
|
limit: int = PydanticField(default=500, ge=1, le=2000)
|
|
|
|
|
|
class VerifyBranchResponse(BaseModel):
|
|
queued: int = 0
|
|
unverified: int = 0
|
|
accepted: bool = False
|