mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +02:00
Every result and management table gains multi-row selection and the floating action bar targets and scans already had: copy, export selection, delete, and the dimension's own actions. Rescan and triage move off the inline strip, which now only offers the rescan that acts on the whole query. Delete is dimension-local. A row takes only what cannot stand without it: a web asset its stored responses, a secret its sightings. Endpoints, services and findings on a deleted host are kept, so every count stays equal to its table. POST /surface/delete is the one route and the scope is a WHERE, so an id from another scan deletes nothing. Vulnerabilities are addressable by template_id so the Issues view deletes a whole check. Exposures dismiss rather than delete: an exposure row is a web asset. Selection export runs through the export pipeline. Every surface filter gains ids, capped by MAX_SELECTED_ROWS. Secrets and software become exportable, which fixes two breaks: a bundle export raised KeyError on the secrets dimension, and mcp read a SURFACE_COLUMNS entry for secrets that did not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XyDpdVXL7YVsr5D7BVj2zt
350 lines
9.9 KiB
Python
350 lines
9.9 KiB
Python
import uuid
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from shared.definitions.asset_query import MAX_QUERY_LENGTH
|
|
from shared.definitions.surface import MAX_SELECTED_ROWS
|
|
from shared.models.asset_query import QueryError
|
|
from shared.models.http_asset import HttpAssetRead
|
|
from shared.models.ip_address import IpAddressRead
|
|
from shared.models.port import PortRead
|
|
from shared.models.subdomain import Facet, SubdomainRelation
|
|
|
|
|
|
class SurfaceStat(BaseModel):
|
|
key: str
|
|
label: str
|
|
value: int
|
|
filter: str = ""
|
|
|
|
|
|
class AttentionItem(BaseModel):
|
|
key: str
|
|
label: str
|
|
count: int
|
|
filter: str
|
|
tone: str
|
|
|
|
|
|
class Bucket(BaseModel):
|
|
key: str
|
|
label: str
|
|
count: int
|
|
klass: str
|
|
|
|
|
|
class Tally(BaseModel):
|
|
name: str
|
|
count: int
|
|
|
|
|
|
class ClusterStat(BaseModel):
|
|
kind: str
|
|
reason: str
|
|
value: str
|
|
count: int
|
|
|
|
|
|
class SubdomainInsights(BaseModel):
|
|
surface: list[SurfaceStat] = Field(default_factory=list)
|
|
attention: list[AttentionItem] = Field(default_factory=list)
|
|
sources: list[Tally] = Field(default_factory=list)
|
|
single_source: int = 0
|
|
resolution: list[Bucket] = Field(default_factory=list)
|
|
status_reframe: list[Bucket] = Field(default_factory=list)
|
|
cert_buckets: list[Bucket] = Field(default_factory=list)
|
|
top_tech: list[Tally] = Field(default_factory=list)
|
|
tech_total: int = 0
|
|
top_asn: list[Tally] = Field(default_factory=list)
|
|
geography: list[Tally] = Field(default_factory=list)
|
|
geo_total: int = 0
|
|
clusters: list[ClusterStat] = Field(default_factory=list)
|
|
|
|
|
|
class SubdomainCorrelation(BaseModel):
|
|
primary_asset: HttpAssetRead | None = None
|
|
services: list[HttpAssetRead] = Field(default_factory=list)
|
|
ports: list[PortRead] = Field(default_factory=list)
|
|
ip_metas: list[IpAddressRead] = Field(default_factory=list)
|
|
related: list[SubdomainRelation] = Field(default_factory=list)
|
|
|
|
|
|
class IpGroupRead(BaseModel):
|
|
ip: str
|
|
version: int
|
|
targets: list[str] = Field(default_factory=list)
|
|
asn: int | None = None
|
|
asn_org: str | None = None
|
|
country: str | None = None
|
|
prefix: str | None = None
|
|
is_cdn: bool = False
|
|
cdn_name: str | None = None
|
|
is_alive: bool | None = None
|
|
ptr_hostnames: list[str] = Field(default_factory=list)
|
|
ports: list[PortRead] = Field(default_factory=list)
|
|
host_count: int = 0
|
|
hosts: list[str] = Field(default_factory=list)
|
|
port_count: int = 0
|
|
has_sensitive: bool = False
|
|
asset_count: int = 0
|
|
|
|
|
|
class IpGroupPage(BaseModel):
|
|
items: list[IpGroupRead] = Field(default_factory=list)
|
|
total: int = 0
|
|
total_capped: bool = False
|
|
error: QueryError | None = None
|
|
|
|
|
|
class IpGroupFilter(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
q: str | None = Field(default=None, max_length=MAX_QUERY_LENGTH)
|
|
ids: list[str] = Field(default_factory=list, max_length=MAX_SELECTED_ROWS)
|
|
exposure: list[str] = Field(default_factory=list, max_length=4)
|
|
asns: list[int] = Field(default_factory=list, max_length=100)
|
|
countries: list[str] = Field(default_factory=list, max_length=100)
|
|
ports: list[int] = Field(default_factory=list, max_length=200)
|
|
services: list[str] = Field(default_factory=list, max_length=200)
|
|
cdn: str = Field(default="any", max_length=10)
|
|
alive: str = Field(default="any", max_length=10)
|
|
version: int = Field(default=0, ge=0, le=6)
|
|
sensitive: bool = False
|
|
hosted: bool = False
|
|
open: bool = False
|
|
sort: str = Field(default="hosts", max_length=20)
|
|
order: str = Field(default="desc", max_length=4)
|
|
limit: int = Field(default=50, ge=1, le=200)
|
|
offset: int = Field(default=0, ge=0, le=100_000_000)
|
|
|
|
def has_facets(self) -> bool:
|
|
return bool(
|
|
self.exposure
|
|
or self.asns
|
|
or self.countries
|
|
or self.ports
|
|
or self.services
|
|
or self.cdn != "any"
|
|
or self.alive != "any"
|
|
or self.version
|
|
or self.sensitive
|
|
or self.hosted
|
|
or self.open
|
|
)
|
|
|
|
|
|
class IpFacets(BaseModel):
|
|
exposure: list[Facet] = Field(default_factory=list)
|
|
asn: list[Facet] = Field(default_factory=list)
|
|
country: list[Facet] = Field(default_factory=list)
|
|
port: list[Facet] = Field(default_factory=list)
|
|
service: list[Facet] = Field(default_factory=list)
|
|
|
|
|
|
class ServiceRead(BaseModel):
|
|
id: uuid.UUID
|
|
scan_id: uuid.UUID | None = None
|
|
target_id: uuid.UUID | None = None
|
|
target_value: str | None = None
|
|
ip: str
|
|
port: int
|
|
protocol: str
|
|
state: str
|
|
service_name: str | None = None
|
|
service_class: str
|
|
description: str = ""
|
|
registered: bool = False
|
|
source: str
|
|
is_http: bool = False
|
|
tls: bool = False
|
|
product: str | None = None
|
|
version: str | None = None
|
|
banner: str | None = None
|
|
asn: int | None = None
|
|
asn_org: str | None = None
|
|
country: str | None = None
|
|
prefix: str | None = None
|
|
is_cdn: bool = False
|
|
cdn_name: str | None = None
|
|
scan_policy: str | None = None
|
|
host_count: int = 0
|
|
hosts: list[str] = Field(default_factory=list)
|
|
web_count: int = 0
|
|
status_code: int | None = None
|
|
url: str | None = None
|
|
title: str | None = None
|
|
screenshot_path: str | None = None
|
|
is_sensitive: bool = False
|
|
is_new: bool = False
|
|
|
|
|
|
class ServicePage(BaseModel):
|
|
items: list[ServiceRead] = Field(default_factory=list)
|
|
total: int = 0
|
|
total_capped: bool = False
|
|
error: QueryError | None = None
|
|
|
|
|
|
class ServiceFilter(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
q: str | None = Field(default=None, max_length=MAX_QUERY_LENGTH)
|
|
ids: list[uuid.UUID] = Field(default_factory=list, max_length=MAX_SELECTED_ROWS)
|
|
classes: list[str] = Field(default_factory=list, max_length=10)
|
|
ports: list[int] = Field(default_factory=list, max_length=200)
|
|
services: list[str] = Field(default_factory=list, max_length=200)
|
|
sources: list[str] = Field(default_factory=list, max_length=10)
|
|
asns: list[int] = Field(default_factory=list, max_length=100)
|
|
countries: list[str] = Field(default_factory=list, max_length=100)
|
|
cdn: str = Field(default="any", max_length=10)
|
|
http: str = Field(default="any", max_length=10)
|
|
sensitive: bool = False
|
|
named: bool = False
|
|
new: bool = False
|
|
sort: str = Field(default="exposure", max_length=20)
|
|
order: str = Field(default="desc", max_length=4)
|
|
limit: int = Field(default=50, ge=1, le=200)
|
|
offset: int = Field(default=0, ge=0, le=100_000_000)
|
|
|
|
def has_facets(self) -> bool:
|
|
return bool(
|
|
self.classes
|
|
or self.ports
|
|
or self.services
|
|
or self.sources
|
|
or self.asns
|
|
or self.countries
|
|
or self.cdn != "any"
|
|
or self.http != "any"
|
|
or self.sensitive
|
|
or self.named
|
|
or self.new
|
|
)
|
|
|
|
|
|
class ServiceFacets(BaseModel):
|
|
klass: list[Facet] = Field(default_factory=list, serialization_alias="class")
|
|
port: list[Facet] = Field(default_factory=list)
|
|
service: list[Facet] = Field(default_factory=list)
|
|
source: list[Facet] = Field(default_factory=list)
|
|
asn: list[Facet] = Field(default_factory=list)
|
|
country: list[Facet] = Field(default_factory=list)
|
|
|
|
|
|
class ExposureBand(BaseModel):
|
|
key: str
|
|
label: str
|
|
count: int
|
|
addresses: int
|
|
query: str
|
|
|
|
|
|
class ExposureLine(BaseModel):
|
|
key: str
|
|
label: str
|
|
detail: str | None = None
|
|
count: int
|
|
query: str
|
|
|
|
|
|
class ScanExposure(BaseModel):
|
|
services: int = 0
|
|
addresses: int = 0
|
|
web_services: int = 0
|
|
non_web_services: int = 0
|
|
answering_http: int = 0
|
|
sensitive: int = 0
|
|
named: int = 0
|
|
passive_only: int = 0
|
|
nonstandard_web: int = 0
|
|
bands: list[ExposureBand] = Field(default_factory=list)
|
|
top_services: list[ExposureLine] = Field(default_factory=list)
|
|
coverage: list[ExposureLine] = Field(default_factory=list)
|
|
scanned: int = 0
|
|
|
|
|
|
class OriginEvidence(BaseModel):
|
|
kind: str
|
|
label: str
|
|
value: str
|
|
|
|
|
|
class OriginSample(BaseModel):
|
|
host: str
|
|
url: str
|
|
ip: str | None = None
|
|
port: int = 0
|
|
status_code: int | None = None
|
|
title: str | None = None
|
|
webserver: str | None = None
|
|
cdn_name: str | None = None
|
|
asn_org: str | None = None
|
|
screenshot_path: str | None = None
|
|
|
|
|
|
class OriginFinding(BaseModel):
|
|
kind: str
|
|
confidence: str
|
|
exposed: OriginSample
|
|
fronted: list[OriginSample] = Field(default_factory=list)
|
|
fronted_total: int = 0
|
|
evidence: list[OriginEvidence] = Field(default_factory=list)
|
|
open_ports: list[int] = Field(default_factory=list)
|
|
sensitive_ports: list[int] = Field(default_factory=list)
|
|
query: str = ""
|
|
|
|
|
|
class OriginExposure(BaseModel):
|
|
findings: list[OriginFinding] = Field(default_factory=list)
|
|
probed_addresses: int = 0
|
|
fronted_assets: int = 0
|
|
|
|
|
|
class CorrelationHost(BaseModel):
|
|
id: uuid.UUID
|
|
name: str
|
|
live: bool = False
|
|
status: int | None = None
|
|
title: str | None = None
|
|
hubs: int = 0
|
|
target: str = ""
|
|
|
|
|
|
class CorrelationHub(BaseModel):
|
|
"""One shared identity and the hosts that carry it."""
|
|
|
|
id: str
|
|
kind: str
|
|
value: str
|
|
label: str
|
|
count: int
|
|
targets: int = 1
|
|
share: float = 0.0
|
|
common: bool = False
|
|
platform: bool = False
|
|
platform_label: str = ""
|
|
query: str
|
|
members: list[int] = Field(default_factory=list)
|
|
|
|
|
|
class CorrelationKindStat(BaseModel):
|
|
key: str
|
|
label: str
|
|
help: str
|
|
default: bool = True
|
|
hubs: int = 0
|
|
total: int = 0
|
|
hosts: int = 0
|
|
common: int = 0
|
|
platform: int = 0
|
|
crossing: int = 0
|
|
|
|
|
|
class CorrelationGraph(BaseModel):
|
|
hosts: list[CorrelationHost] = Field(default_factory=list)
|
|
hubs: list[CorrelationHub] = Field(default_factory=list)
|
|
kinds: list[CorrelationKindStat] = Field(default_factory=list)
|
|
total_hosts: int = 0
|
|
estate_hosts: int = 0
|
|
shared_hosts: int = 0
|
|
targets_total: int = 1
|
|
truncated: bool = False
|