Files
rengine/shared/models/software.py
T
Yogesh OjhaandClaude Opus 5 873f8ef4c4 fix(search): reject a filter field that does not exist
A search filter took any JSON object, so a client sending the wrong
field names got the whole scope back with no error and no sign anything
was ignored. The repo's own lead-cache test was built on it: it passed
page and size, which SubdomainFilter has never had. Every dimension
filter now forbids an unknown field, the way ScanCreate already does,
and the test uses the real offset and limit.

Two more while here: nuclei's _note_gaps had been copied inline into
_one and left behind unused, so the copy is gone; and the launch preview
now says when a target already has a run in flight, since nothing
stopped two scans pointing at one host from the same address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxW9yfg1SEchtg6HLGHRkK
2026-09-13 05:15:59 +05:30

204 lines
6.5 KiB
Python

import uuid
from datetime import date, datetime
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text
from sqlalchemy.types import JSON
from sqlmodel import Field, SQLModel, UniqueConstraint
from shared.definitions.evidence import Evidence
from shared.definitions.software import Confidence, VersionSource
from shared.definitions.vulnerabilities import Severity
from shared.models.asset_query import QueryError
from shared.utils.datetime import utc_now
def _json_list() -> Field:
return Field(default_factory=list, sa_column=Column(JSON, nullable=False))
class NvdCve(SQLModel, table=True):
"""One published CVE, as NVD states it."""
__tablename__ = "nvd_cves"
cve: str = Field(max_length=30, primary_key=True)
severity: str | None = Field(default=None, max_length=16, index=True)
cvss_score: float | None = Field(default=None)
cvss_vector: str | None = Field(default=None, max_length=200)
description: str | None = Field(default=None, sa_column=Column(Text))
published_at: datetime | None = Field(default=None, index=True)
last_modified_at: datetime | None = Field(default=None)
class NvdCpeMatch(SQLModel, table=True):
"""One version range NVD states a CVE applies to."""
__tablename__ = "nvd_cpe_matches"
id: int | None = Field(
default=None, sa_column=Column(BigInteger, primary_key=True, autoincrement=True)
)
cve: str = Field(max_length=30, index=True)
vendor: str = Field(max_length=200)
product: str = Field(max_length=200, index=True)
version_kind: str = Field(max_length=1)
exact_key: str | None = Field(default=None, max_length=160)
start_key: str | None = Field(default=None, max_length=160)
start_incl: bool = Field(default=False)
end_key: str | None = Field(default=None, max_length=160)
end_incl: bool = Field(default=False)
conditional: bool = Field(default=False)
class SoftwareCve(SQLModel, table=True):
"""A CVE inferred from the version an asset reports. Nothing was sent to confirm it."""
__tablename__ = "software_cves"
__table_args__ = (
UniqueConstraint("scan_id", "fingerprint", name="uq_software_scan_fingerprint"),
)
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)
fingerprint: str = Field(max_length=64, index=True)
cve: str = Field(max_length=30, index=True)
# what it runs
name: str = Field(max_length=120, index=True)
version: str = Field(max_length=64)
vendor: str = Field(max_length=200)
product: str = Field(max_length=200, index=True)
cpe: str = Field(max_length=300)
version_source: str = Field(default=VersionSource.BANNER.value, max_length=16)
# what it is worth
severity: str = Field(default=Severity.UNKNOWN.value, max_length=16, index=True)
cvss_score: float | None = Field(default=None)
epss_score: float | None = Field(default=None)
epss_percentile: float | None = Field(default=None)
is_kev: bool = Field(default=False, index=True)
kev_ransomware: bool = Field(default=False)
kev_due_date: date | None = Field(default=None)
exploit_score: int = Field(default=0, index=True)
intel_kinds: list = _json_list()
confidence: str = Field(default=Confidence.HIGH.value, max_length=16, index=True)
caveats: list = _json_list()
evidence: str = Field(default=Evidence.INFERRED.value, max_length=16, index=True)
# where it is
host: str | None = Field(default=None, max_length=500, index=True)
ip: str | None = Field(default=None, max_length=45, index=True)
port: int | None = Field(default=None, index=True)
url: str | None = Field(default=None, max_length=2000)
http_asset_id: uuid.UUID | None = Field(default=None, index=True)
port_id: uuid.UUID | None = Field(default=None, index=True)
discovered_at: datetime = Field(default_factory=utc_now, index=True)
created_at: datetime = Field(default_factory=utc_now)
# ---------- read models ----------
class SoftwareCveRead(BaseModel):
id: uuid.UUID
scan_id: uuid.UUID
target_id: uuid.UUID
target_value: str | None = None
cve: str
name: str
version: str
vendor: str
product: str
cpe: str
version_source: str
version_source_label: str
severity: str
cvss_score: float | None = None
epss_score: float | None = None
epss_percentile: float | None = None
band: str | None = None
is_kev: bool = False
kev_ransomware: bool = False
kev_due_date: date | None = None
exploit_score: int = 0
intel_kinds: list[str] = []
confidence: str
confidence_label: str
caveats: list[dict] = []
evidence: str = Evidence.INFERRED.value
evidence_label: str = ""
description: str | None = None
host: str | None = None
ip: str | None = None
port: int | None = None
url: str | None = None
http_asset_id: uuid.UUID | None = None
port_id: uuid.UUID | None = None
discovered_at: datetime
is_new: bool = False
class SoftwareComponentRead(BaseModel):
"""One piece of software an asset reports, and whether it could be looked up."""
name: str
version: str | None = None
vendor: str | None = None
product: str | None = None
version_source: str
mapped: bool = False
cves: int = 0
assets: int = 0
class SoftwareCoverage(BaseModel):
"""What the inference reached, and what it could not."""
components: int = 0
mapped: int = 0
unmapped: int = 0
matched: int = 0
findings: int = 0
feed_ready: bool = False
feed_age_hours: float | None = None
stale: bool = False
unmapped_names: list[SoftwareComponentRead] = []
class SoftwareFacet(BaseModel):
key: str
label: str
count: int = 0
class SoftwareFacets(BaseModel):
severity: list[SoftwareFacet] = []
confidence: list[SoftwareFacet] = []
source: list[SoftwareFacet] = []
caveat: list[SoftwareFacet] = []
evidence: list[SoftwareFacet] = []
product: list[SoftwareFacet] = []
class SoftwarePage(BaseModel):
items: list[SoftwareCveRead] = []
total: int = 0
total_capped: bool = False
error: QueryError | None = None
class SoftwareFilter(BaseModel):
model_config = ConfigDict(extra="forbid")
q: str | None = None
limit: int = 50
offset: int = 0
sort: str | None = None
direction: str | None = None