Files
rengine/shared/models/interest.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

270 lines
8.3 KiB
Python

import uuid
from datetime import datetime
from pydantic import BaseModel, ConfigDict
from pydantic import Field as PField
from sqlalchemy import Column, Index
from sqlalchemy.types import JSON
from sqlmodel import Field, SQLModel, UniqueConstraint
from shared.definitions.interest import (
MAX_EVIDENCE,
MAX_KEYWORDS,
MAX_REASON,
MAX_RULE_NAME,
RuleMode,
)
from shared.utils.datetime import utc_now
class InterestRule(SQLModel, table=True):
__tablename__ = "interest_rules"
__table_args__ = (
Index("ix_interest_rules_project_enabled", "project_id", "enabled"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
project_id: uuid.UUID | None = Field(
default=None, foreign_key="projects.id", index=True, ondelete="CASCADE"
)
name: str = Field(max_length=MAX_RULE_NAME)
description: str | None = Field(default=None, max_length=300)
mode: str = Field(default=RuleMode.QUERY.value, max_length=16)
query: str = Field(default="", max_length=2000)
keywords: list = Field(default_factory=list, sa_column=Column(JSON, nullable=False))
keyword_fields: list = Field(
default_factory=list, sa_column=Column(JSON, nullable=False)
)
live_only: bool = Field(default=False)
kind: str = Field(max_length=40)
weight: int | None = Field(default=None)
enabled: bool = Field(default=True, index=True)
builtin: bool = Field(default=False, index=True)
notify: bool = Field(default=False)
created_by: uuid.UUID | None = Field(default=None, foreign_key="users.id")
created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)
class InterestSignal(SQLModel, table=True):
__tablename__ = "interest_signals"
__table_args__ = (
UniqueConstraint(
"scan_id", "subdomain_id", "source", "key", name="uq_interest_signal"
),
Index("ix_interest_signals_scan_score", "scan_id", "weight"),
)
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)
subdomain_id: uuid.UUID = Field(
foreign_key="subdomains.id", index=True, ondelete="CASCADE"
)
host: str = Field(max_length=500, index=True)
source: str = Field(max_length=16, index=True)
key: str = Field(max_length=120)
kind: str = Field(max_length=40, index=True)
weight: int = Field(default=0)
label: str = Field(default="", max_length=MAX_RULE_NAME)
reason: str = Field(default="", max_length=MAX_REASON)
evidence: str | None = Field(default=None, max_length=MAX_EVIDENCE)
rule_id: uuid.UUID | None = Field(default=None, index=True)
model: str | None = Field(default=None, max_length=80)
prompt_version: str | None = Field(default=None, max_length=16)
created_at: datetime = Field(default_factory=utc_now)
class InterestDismissal(SQLModel, table=True):
__tablename__ = "interest_dismissals"
__table_args__ = (
UniqueConstraint("target_id", "host", "kind", name="uq_interest_dismissal"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
target_id: uuid.UUID = Field(
foreign_key="targets.id", index=True, ondelete="CASCADE"
)
project_id: uuid.UUID = Field(foreign_key="projects.id", index=True)
host: str = Field(max_length=500, index=True)
kind: str = Field(default="", max_length=40)
note: str | None = Field(default=None, max_length=300)
created_by: uuid.UUID | None = Field(default=None, foreign_key="users.id")
created_at: datetime = Field(default_factory=utc_now)
class InterestRuleCreate(BaseModel):
name: str = PField(min_length=1, max_length=MAX_RULE_NAME)
description: str | None = None
mode: str = RuleMode.QUERY.value
query: str = ""
keywords: list[str] = PField(default_factory=list, max_length=MAX_KEYWORDS)
keyword_fields: list[str] = PField(default_factory=list)
live_only: bool = False
kind: str
weight: int | None = None
enabled: bool = True
notify: bool = False
project_id: uuid.UUID | None = None
class InterestRuleUpdate(BaseModel):
name: str | None = None
description: str | None = None
query: str | None = None
keywords: list[str] | None = PField(default=None, max_length=MAX_KEYWORDS)
keyword_fields: list[str] | None = None
live_only: bool | None = None
kind: str | None = None
weight: int | None = None
enabled: bool | None = None
notify: bool | None = None
class InterestRuleRead(BaseModel):
id: uuid.UUID
project_id: uuid.UUID | None = None
name: str
description: str | None = None
mode: str
query: str
keywords: list[str] = PField(default_factory=list)
keyword_fields: list[str] = PField(default_factory=list)
live_only: bool = False
kind: str
kind_label: str = ""
weight: int
enabled: bool
builtin: bool
notify: bool
updated_at: datetime
matches: int | None = None
error: str | None = None
class SignalRead(BaseModel):
source: str
kind: str
kind_label: str
label: str
reason: str
evidence: str | None = None
weight: int
rule_id: uuid.UUID | None = None
model: str | None = None
judgement: bool = False
class InterestRow(BaseModel):
subdomain_id: uuid.UUID
target_id: uuid.UUID
target_value: str | None = None
host: str
score: int
band: str
kinds: list[str] = PField(default_factory=list)
signals: list[SignalRead] = PField(default_factory=list)
sources: list[str] = PField(default_factory=list)
http_status: int | None = None
page_title: str | None = None
tech: list[str] = PField(default_factory=list)
webserver: str | None = None
resolved_ips: list[str] = PField(default_factory=list)
asn: int | None = None
asn_org: str | None = None
is_cdn: bool = False
screenshot_path: str | None = None
is_new: bool = False
dismissed: bool = False
class InterestSummary(BaseModel):
total: int = 0
bands: dict[str, int] = PField(default_factory=dict)
sources: dict[str, int] = PField(default_factory=dict)
kinds: dict[str, int] = PField(default_factory=dict)
dismissed: int = 0
judged_hosts: int = 0
judged_at: datetime | None = None
model: str | None = None
ai_available: bool = False
ai_enabled: bool = False
stale: bool = False
class InterestFilter(BaseModel):
model_config = ConfigDict(extra="forbid")
q: str | None = PField(default=None, max_length=200)
bands: list[str] = PField(default_factory=list, max_length=8)
sources: list[str] = PField(default_factory=list, max_length=8)
kinds: list[str] = PField(default_factory=list, max_length=40)
sort: str = "score"
order: str = "desc"
limit: int = PField(default=50, ge=1, le=200)
offset: int = PField(default=0, ge=0, le=100_000)
class InterestPage(BaseModel):
rows: list[InterestRow] = PField(default_factory=list)
total: int = 0
summary: InterestSummary = PField(default_factory=InterestSummary)
class KindEntry(BaseModel):
key: str
label: str
help: str
weight: int
tone: str
class SourceEntry(BaseModel):
key: str
label: str
help: str
judgement: bool = False
class BandEntry(BaseModel):
key: str
label: str
tone: str
floor: int
class InterestCatalog(BaseModel):
kinds: list[KindEntry] = PField(default_factory=list)
sources: list[SourceEntry] = PField(default_factory=list)
bands: list[BandEntry] = PField(default_factory=list)
modes: dict[str, str] = PField(default_factory=dict)
keyword_fields: dict[str, str] = PField(default_factory=dict)
max_score: int = 0
providers: list[str] = PField(default_factory=list)
class RulePreview(BaseModel):
matches: int = 0
capped: bool = False
error: str | None = None
sample: list[str] = PField(default_factory=list)
class RuleSuggestion(BaseModel):
name: str
kind: str
kind_label: str = ""
query: str
reason: str = ""
matches: int = 0
class DismissRequest(BaseModel):
host: str = PField(min_length=1, max_length=500)
target_id: uuid.UUID
kind: str = ""
note: str | None = None