Files
rengine/shared/models/endpoint.py
T
Yogesh OjhaandClaude Opus 5 0b853e04b6 chore: production-readiness review across the tree
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
2026-09-13 18:03:29 +05:30

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