Files
security-alert-center/backend/app/services/rdg_session_flap.py
T
PTah 2aec488226 feat: RDG session flap rule, qwinsta/logoff queue and dashboard UI (0.9.13)
Detect 302→303 within 1–10s, Problem with 30s dedup, rdg_flap flag.
Agent command poll API, admin qwinsta/logoff from Overview. Migration 016.
2026-06-19 23:34:01 +10:00

121 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""RDG 302→303 session flap: detect, flag event, build Problem match."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified
from app.config import get_settings
from app.models import Event
from app.services.problem_rules import RULE_RDG_SESSION_FLAP, RuleMatch
RDG_SUCCESS_TYPE = "rdg.connection.success"
RDG_END_TYPES = frozenset({"rdg.connection.disconnected", "rdg.connection.failed"})
def _event_user(event: Event) -> str:
details = event.details if isinstance(event.details, dict) else {}
user = details.get("user")
return str(user).strip() if user else ""
def _event_internal_ip(event: Event) -> str:
details = event.details if isinstance(event.details, dict) else {}
for key in ("internal_ip", "client_ip", "ip_address"):
val = details.get(key)
if val:
return str(val).strip()
return ""
def _users_match(end_event: Event, success_event: Event) -> bool:
return _event_user(end_event) != "" and _event_user(end_event) == _event_user(success_event)
def _internal_ips_compatible(end_event: Event, success_event: Event) -> bool:
end_ip = _event_internal_ip(end_event)
success_ip = _event_internal_ip(success_event)
if not end_ip or not success_ip:
return True
return end_ip == success_ip
def _as_utc(dt: datetime) -> datetime:
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def find_rdg_success_before_end(db: Session, end_event: Event) -> Event | None:
if end_event.type not in RDG_END_TYPES:
return None
settings = get_settings()
min_sec = settings.sac_rdg_flap_window_min_sec
max_sec = settings.sac_rdg_flap_window_max_sec
end_at = _as_utc(end_event.occurred_at)
window_start = end_at - timedelta(seconds=max_sec)
window_end = end_at - timedelta(seconds=min_sec)
candidates = db.scalars(
select(Event)
.where(
Event.host_id == end_event.host_id,
Event.type == RDG_SUCCESS_TYPE,
Event.occurred_at >= window_start,
Event.occurred_at <= window_end,
Event.id != end_event.id,
)
.order_by(Event.occurred_at.desc())
).all()
for prior in candidates:
if not _users_match(end_event, prior):
continue
if not _internal_ips_compatible(end_event, prior):
continue
prior_at = _as_utc(prior.occurred_at)
delta = (end_at - prior_at).total_seconds()
if min_sec <= delta <= max_sec:
return prior
return None
def mark_rdg_flap(event: Event, *, pair_event: Event) -> None:
details = dict(event.details) if isinstance(event.details, dict) else {}
details["rdg_flap"] = True
details["rdg_flap_pair_event_id"] = pair_event.id
event.details = details
flag_modified(event, "details")
def evaluate_rdg_session_flap(db: Session, event: Event) -> RuleMatch | None:
prior = find_rdg_success_before_end(db, event)
if prior is None:
return None
mark_rdg_flap(event, pair_event=prior)
user = _event_user(event)
internal_ip = _event_internal_ip(event)
ip_note = f", client {internal_ip}" if internal_ip else ""
delta_sec = int((_as_utc(event.occurred_at) - _as_utc(prior.occurred_at)).total_seconds())
return RuleMatch(
rule_id=RULE_RDG_SESSION_FLAP,
correlation_type="rdg.session.flap",
fingerprint_suffix=f"u{user}:ip{internal_ip or 'any'}",
title=f"RDG session flap: {user}",
summary=(
f"302→303 за {delta_sec} с "
f"({user}{ip_note}). Возможна зависшая сессия на ПК пользователя — qwinsta/logoff."
),
severity="warning",
)
def event_has_rdg_flap(event: Event) -> bool:
details = event.details if isinstance(event.details, dict) else {}
return details.get("rdg_flap") is True