91be282ec6
Ingest correlates direct RDP logoff with open login events on the same host and user. Terminate-session treats missing qwinsta as already logged off.
149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
"""Correlate direct RDP logoff (Security 4634/4647) with workstation rdp.login.success."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.orm.attributes import flag_modified
|
|
|
|
from app.models import Event
|
|
from app.services.host_sessions import (
|
|
SESSION_TERMINATED_AT_KEY,
|
|
_details_dict,
|
|
_event_login_user,
|
|
)
|
|
from app.services.rdg_workstation_session import (
|
|
WORKSTATION_LOGIN_TYPE,
|
|
event_closed_by_rdg,
|
|
users_match_rdg,
|
|
)
|
|
|
|
SESSION_CLOSED_BY_LOGOFF_AT_KEY = "session_closed_by_logoff_at"
|
|
SESSION_CLOSED_BY_LOGOFF_EVENT_ID_KEY = "session_closed_by_logoff_event_id"
|
|
|
|
LOGOFF_EVENT_TYPE = "rdp.session.logoff"
|
|
LOGOFF_EVENT_TYPES = frozenset({LOGOFF_EVENT_TYPE})
|
|
|
|
|
|
def event_closed_by_logoff(event: Event) -> bool:
|
|
details = _details_dict(event)
|
|
at = details.get(SESSION_CLOSED_BY_LOGOFF_AT_KEY)
|
|
return at is not None and str(at).strip() != ""
|
|
|
|
|
|
def mark_login_closed_by_logoff(login_event: Event, *, logoff_event: Event) -> None:
|
|
details = dict(_details_dict(login_event))
|
|
details[SESSION_CLOSED_BY_LOGOFF_AT_KEY] = logoff_event.occurred_at.isoformat()
|
|
details[SESSION_CLOSED_BY_LOGOFF_EVENT_ID_KEY] = logoff_event.id
|
|
login_event.details = details
|
|
flag_modified(login_event, "details")
|
|
|
|
|
|
def _login_already_closed(login_event: Event) -> bool:
|
|
details = _details_dict(login_event)
|
|
if details.get("session_terminated") is True:
|
|
return True
|
|
at = details.get(SESSION_TERMINATED_AT_KEY)
|
|
if at is not None and str(at).strip() != "":
|
|
return True
|
|
if event_closed_by_rdg(login_event):
|
|
return True
|
|
return event_closed_by_logoff(login_event)
|
|
|
|
|
|
def find_workstation_login_for_logoff(db: Session, logoff_event: Event) -> Event | None:
|
|
if logoff_event.type not in LOGOFF_EVENT_TYPES:
|
|
return None
|
|
|
|
logoff_user = _event_login_user(logoff_event)
|
|
if not logoff_user:
|
|
return None
|
|
logoff_at = logoff_event.occurred_at
|
|
host_id = logoff_event.host_id
|
|
if host_id is None:
|
|
return None
|
|
|
|
candidates = db.scalars(
|
|
select(Event)
|
|
.where(
|
|
Event.host_id == host_id,
|
|
Event.type == WORKSTATION_LOGIN_TYPE,
|
|
Event.occurred_at <= logoff_at,
|
|
Event.id != logoff_event.id,
|
|
)
|
|
.order_by(Event.occurred_at.desc())
|
|
).all()
|
|
|
|
logoff_details = _details_dict(logoff_event)
|
|
logoff_ip = str(logoff_details.get("ip_address") or "").strip()
|
|
|
|
for login in candidates:
|
|
if _login_already_closed(login):
|
|
continue
|
|
login_user = _event_login_user(login)
|
|
if not users_match_rdg(login_user, logoff_user):
|
|
continue
|
|
if logoff_ip and logoff_ip not in ("", "-"):
|
|
login_ip = str(_details_dict(login).get("ip_address") or "").strip()
|
|
if login_ip and login_ip not in ("", "-") and login_ip != logoff_ip:
|
|
continue
|
|
return login
|
|
return None
|
|
|
|
|
|
def find_logoff_after_workstation_login(db: Session, login_event: Event) -> Event | None:
|
|
"""Runtime lookup for historical logins without persisted close flag."""
|
|
if login_event.type != WORKSTATION_LOGIN_TYPE:
|
|
return None
|
|
if _login_already_closed(login_event):
|
|
return None
|
|
|
|
host_id = login_event.host_id
|
|
if host_id is None:
|
|
return None
|
|
|
|
login_user = _event_login_user(login_event)
|
|
if not login_user:
|
|
return None
|
|
login_at = login_event.occurred_at
|
|
login_ip = str(_details_dict(login_event).get("ip_address") or "").strip()
|
|
|
|
candidates = db.scalars(
|
|
select(Event)
|
|
.where(
|
|
Event.host_id == host_id,
|
|
Event.type == LOGOFF_EVENT_TYPE,
|
|
Event.occurred_at >= login_at,
|
|
Event.id != login_event.id,
|
|
)
|
|
.order_by(Event.occurred_at.asc())
|
|
).all()
|
|
|
|
for logoff in candidates:
|
|
if not users_match_rdg(_event_login_user(logoff), login_user):
|
|
continue
|
|
logoff_ip = str(_details_dict(logoff).get("ip_address") or "").strip()
|
|
if login_ip and login_ip not in ("", "-") and logoff_ip and logoff_ip not in ("", "-"):
|
|
if login_ip != logoff_ip:
|
|
continue
|
|
return logoff
|
|
return None
|
|
|
|
|
|
def resolve_workstation_login_closed_by_logoff(db: Session, login_event: Event) -> bool:
|
|
if event_closed_by_logoff(login_event):
|
|
return True
|
|
return find_logoff_after_workstation_login(db, login_event) is not None
|
|
|
|
|
|
def close_workstation_session_for_rdp_logoff(db: Session, logoff_event: Event) -> Event | None:
|
|
"""On direct RDP logoff, mark matching open workstation login as session-closed."""
|
|
if logoff_event.type not in LOGOFF_EVENT_TYPES:
|
|
return None
|
|
|
|
login = find_workstation_login_for_logoff(db, logoff_event)
|
|
if login is None:
|
|
return None
|
|
mark_login_closed_by_logoff(login, logoff_event=logoff_event)
|
|
return login
|