mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-19 23:01:29 +02:00
- Store each run's row, first-seen and retired counts (scan_deltas, scan_retired), valid while the revision stamp matches scan_revisions; statement triggers on the seven result tables bump it. Finalize warms, a beat backfill fills the rest, a live run answers from its last count and queues a refresh. /scans 1.5 s -> 43 ms, dashboard overview 5-10 s -> 0.9 s. - lead_cache holds a live scope for 20 s with a stale copy, and every build is single-flight; search pages opt out. - Leads: lowercase subdomain names (CHECK) and LIKE for host:, a guard for empty JSON arrays, branches as MATERIALIZED CTEs read to the cap, every count in one statement. - Addresses and services: the derived CTE as one grouped pass, rows and count in one execution (page_rows), GIN lookups for page hosts, facets through grouping sets. - Correlation carriers counted by EXISTS; takeover latest-sighting in SQL, no silent cap. - Screenshot backfill partial index, nine unused low-cardinality indexes dropped, hostname helpers memoised, vhost names normalised.
198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
from datetime import timedelta
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import case, cast, func, select, true
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.services.domain_posture import DomainPostureService
|
|
from app.services.target_scope import Targets, keeps
|
|
from shared.definitions import domain_posture as posture_defs
|
|
from shared.definitions.domains import takeover_provider
|
|
from shared.enums.scan import ScanStatus
|
|
from shared.enums.target import TargetType
|
|
from shared.logging import get_logger
|
|
from shared.models.dashboard import (
|
|
DashboardSignals,
|
|
SpoofableDomain,
|
|
SpoofableSignal,
|
|
StaleSignal,
|
|
StaleTarget,
|
|
TakeoverCandidate,
|
|
TakeoverSignal,
|
|
)
|
|
from shared.models.scan import Scan
|
|
from shared.models.subdomain import Subdomain
|
|
from shared.models.target import Target
|
|
from shared.utils.datetime import utc_now
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
_ITEMS_CAP = 100
|
|
_STALE_DAYS = 30
|
|
|
|
_DOMAIN_TYPES = (TargetType.DOMAIN, TargetType.URL)
|
|
|
|
|
|
class DashboardService:
|
|
def __init__(self, session: AsyncSession):
|
|
self.session = session
|
|
|
|
async def signals(
|
|
self, project_id: UUID, targets: Targets = None
|
|
) -> DashboardSignals:
|
|
return DashboardSignals(
|
|
takeover=await self._takeover_candidates(project_id, targets),
|
|
spoofable=await self._spoofable_domains(project_id, targets),
|
|
stale=await self._stale_targets(project_id, targets),
|
|
)
|
|
|
|
async def _takeover_candidates(
|
|
self, project_id: UUID, targets: Targets = None
|
|
) -> TakeoverSignal:
|
|
conds = [
|
|
Subdomain.project_id == project_id,
|
|
Subdomain.cname.is_not(None),
|
|
Subdomain.cname != "",
|
|
]
|
|
if targets is not None:
|
|
conds.append(Subdomain.target_id.in_(targets))
|
|
latest_sighting = (
|
|
select(
|
|
Subdomain.target_id,
|
|
Subdomain.name,
|
|
Subdomain.cname,
|
|
Subdomain.resolved_ips,
|
|
Subdomain.discovered_at,
|
|
)
|
|
.where(*conds)
|
|
.distinct(Subdomain.target_id, Subdomain.name)
|
|
.order_by(
|
|
Subdomain.target_id,
|
|
Subdomain.name,
|
|
Subdomain.discovered_at.desc(),
|
|
Subdomain.scan_id.desc(),
|
|
)
|
|
.subquery()
|
|
)
|
|
ips = cast(latest_sighting.c.resolved_ips, JSONB)
|
|
unresolved = case(
|
|
(func.jsonb_typeof(ips) == "array", func.jsonb_array_length(ips) == 0),
|
|
else_=true(),
|
|
)
|
|
rows = (
|
|
await self.session.execute(select(latest_sighting).where(unresolved))
|
|
).all()
|
|
|
|
candidates: list[TakeoverCandidate] = []
|
|
for r in rows:
|
|
provider = takeover_provider(r.cname or "")
|
|
if provider is None:
|
|
continue
|
|
candidates.append(
|
|
TakeoverCandidate(
|
|
name=r.name,
|
|
target_id=r.target_id,
|
|
cname=r.cname,
|
|
provider=provider,
|
|
last_seen=r.discovered_at,
|
|
)
|
|
)
|
|
candidates.sort(key=lambda c: c.name)
|
|
candidates.sort(key=lambda c: c.last_seen, reverse=True)
|
|
return TakeoverSignal(count=len(candidates), items=candidates[:_ITEMS_CAP])
|
|
|
|
async def _spoofable_domains(
|
|
self, project_id: UUID, targets: Targets = None
|
|
) -> SpoofableSignal:
|
|
"""Zones whose newest run fails a sender check."""
|
|
hits = [
|
|
hit
|
|
for hit in await DomainPostureService(self.session).spoofable(project_id)
|
|
if keeps(targets, hit[0])
|
|
]
|
|
if not hits:
|
|
return SpoofableSignal(count=0, items=[])
|
|
names = dict(
|
|
(
|
|
await self.session.execute(
|
|
select(Target.id, Target.target_value).where(
|
|
Target.id.in_({tid for tid, *_ in hits})
|
|
)
|
|
)
|
|
).all()
|
|
)
|
|
items = [
|
|
SpoofableDomain(
|
|
target_id=tid,
|
|
target_value=names.get(tid, zone),
|
|
zone=zone,
|
|
reason=posture_defs.CHECK_BY_KEY[key].label,
|
|
check=key,
|
|
)
|
|
for tid, zone, key, _at in hits
|
|
if tid in names
|
|
]
|
|
items.sort(key=lambda d: (d.zone, d.target_value))
|
|
return SpoofableSignal(count=len(items), items=items[:_ITEMS_CAP])
|
|
|
|
async def _stale_targets(
|
|
self, project_id: UUID, targets: Targets = None
|
|
) -> StaleSignal:
|
|
cutoff = utc_now() - timedelta(days=_STALE_DAYS)
|
|
last_completed = (
|
|
select(
|
|
Scan.target_id.label("tid"),
|
|
func.max(func.coalesce(Scan.completed_at, Scan.started_at)).label(
|
|
"last"
|
|
),
|
|
)
|
|
.where(
|
|
Scan.project_id == project_id,
|
|
Scan.status == ScanStatus.COMPLETED.value,
|
|
)
|
|
.group_by(Scan.target_id)
|
|
.subquery()
|
|
)
|
|
rows = (
|
|
await self.session.execute(
|
|
select(
|
|
Target.id,
|
|
Target.target_value,
|
|
Target.target_type,
|
|
last_completed.c.last,
|
|
)
|
|
.join(last_completed, last_completed.c.tid == Target.id, isouter=True)
|
|
.where(Target.project_id == project_id)
|
|
)
|
|
).all()
|
|
|
|
never: list[StaleTarget] = []
|
|
stale: list[StaleTarget] = []
|
|
for tid, value, ttype, last in rows:
|
|
if not keeps(targets, tid):
|
|
continue
|
|
tt = getattr(ttype, "value", ttype)
|
|
if last is None:
|
|
never.append(
|
|
StaleTarget(
|
|
target_id=tid,
|
|
target_value=value,
|
|
target_type=tt,
|
|
last_scanned_at=None,
|
|
)
|
|
)
|
|
elif last < cutoff:
|
|
stale.append(
|
|
StaleTarget(
|
|
target_id=tid,
|
|
target_value=value,
|
|
target_type=tt,
|
|
last_scanned_at=last,
|
|
)
|
|
)
|
|
never.sort(key=lambda t: t.target_value)
|
|
stale.sort(key=lambda t: t.last_scanned_at or utc_now())
|
|
items = (never + stale)[:_ITEMS_CAP]
|
|
return StaleSignal(never_scanned=len(never), stale=len(stale), items=items)
|