379 lines
12 KiB
Python
379 lines
12 KiB
Python
"""Tests for RDG 303 → workstation rdp.login.success correlation."""
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from app.config import get_settings
|
|
from app.models import Host
|
|
from app.services.event_summary import event_to_summary
|
|
from app.services.host_sessions import event_session_terminated
|
|
from app.services.ingest import ingest_event
|
|
from app.services.rdg_workstation_session import (
|
|
SESSION_CLOSED_BY_RDG_AT_KEY,
|
|
SESSION_CLOSED_BY_RDG_EVENT_ID_KEY,
|
|
close_workstation_session_for_rdg_end,
|
|
find_rdg_end_after_workstation_login,
|
|
resolve_workstation_login_closed,
|
|
)
|
|
from tests.test_ingest import VALID_EVENT
|
|
|
|
|
|
def _payload(**overrides):
|
|
base = {
|
|
**VALID_EVENT,
|
|
"event_id": str(uuid.uuid4()),
|
|
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _ingest(db, occurred_at: datetime, **overrides):
|
|
payload = _payload(**overrides)
|
|
payload["occurred_at"] = occurred_at.isoformat()
|
|
event, _ = ingest_event(db, payload)
|
|
db.flush()
|
|
return event
|
|
|
|
|
|
@pytest.fixture
|
|
def rdg_settings(monkeypatch):
|
|
monkeypatch.setenv("SAC_RDG_FLAP_WINDOW_MIN_SEC", "1")
|
|
monkeypatch.setenv("SAC_RDG_FLAP_WINDOW_MAX_SEC", "10")
|
|
monkeypatch.setenv("SAC_RDG_FLAP_DEDUP_SEC", "30")
|
|
get_settings.cache_clear()
|
|
yield
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def rdg_hosts(db_session):
|
|
ws = Host(
|
|
hostname="TSA-PC",
|
|
os_family="windows",
|
|
product="rdp-login-monitor",
|
|
ipv4="192.168.163.100",
|
|
)
|
|
gw = Host(
|
|
hostname="K6A-DC3",
|
|
os_family="windows",
|
|
product="rdp-login-monitor",
|
|
ipv4="192.168.160.40",
|
|
)
|
|
db_session.add_all([ws, gw])
|
|
db_session.commit()
|
|
return ws, gw
|
|
|
|
|
|
def test_rdg_end_marks_workstation_login_closed_on_ingest(db_session, rdg_settings, rdg_hosts):
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
internal_ip = ws.ipv4
|
|
|
|
login = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=1),
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDP login",
|
|
summary="1149",
|
|
details={"user": user},
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(hours=2),
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="RD Gateway event 303",
|
|
summary="303",
|
|
details={"user": user, "internal_ip": internal_ip},
|
|
)
|
|
|
|
assert login.details[SESSION_CLOSED_BY_RDG_AT_KEY] == end.occurred_at.isoformat()
|
|
assert login.details[SESSION_CLOSED_BY_RDG_EVENT_ID_KEY] == end.id
|
|
assert event_session_terminated(login, db=db_session) is True
|
|
|
|
summary = event_to_summary(login, db_session)
|
|
assert summary.session_terminated is True
|
|
|
|
|
|
def test_rdg_flap_does_not_close_workstation_login(db_session, rdg_settings, rdg_hosts):
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
internal_ip = ws.ipv4
|
|
details = {"user": user, "internal_ip": internal_ip}
|
|
|
|
login = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=1),
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDP login",
|
|
summary="1149",
|
|
details={"user": user},
|
|
)
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="302",
|
|
summary="302",
|
|
details=details,
|
|
)
|
|
_ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=4),
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details=details,
|
|
)
|
|
db_session.refresh(login)
|
|
|
|
assert SESSION_CLOSED_BY_RDG_AT_KEY not in (login.details or {})
|
|
assert event_session_terminated(login, db=db_session) is False
|
|
assert event_to_summary(login, db_session).session_terminated is False
|
|
|
|
|
|
def test_runtime_resolve_for_historical_login_without_flag(db_session, rdg_settings, rdg_hosts):
|
|
ws, gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
internal_ip = ws.ipv4
|
|
|
|
from app.models import Event
|
|
|
|
login = Event(
|
|
event_id=str(uuid.uuid4()),
|
|
host_id=ws.id,
|
|
occurred_at=t0 + timedelta(seconds=1),
|
|
received_at=t0,
|
|
category="auth",
|
|
type="rdp.login.success",
|
|
severity="info",
|
|
title="RDP login",
|
|
summary="1149",
|
|
payload={},
|
|
details={"user": "TSA"},
|
|
)
|
|
end = Event(
|
|
event_id=str(uuid.uuid4()),
|
|
host_id=gw.id,
|
|
occurred_at=t0 + timedelta(hours=2),
|
|
received_at=t0,
|
|
category="auth",
|
|
type="rdg.connection.disconnected",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
payload={},
|
|
details={"user": user, "internal_ip": internal_ip},
|
|
)
|
|
db_session.add_all([login, end])
|
|
db_session.commit()
|
|
|
|
assert resolve_workstation_login_closed(db_session, login) is True
|
|
assert find_rdg_end_after_workstation_login(db_session, login) is not None
|
|
assert event_to_summary(login, db_session).session_terminated is True
|
|
|
|
|
|
def test_user_mismatch_does_not_close_login(db_session, rdg_settings, rdg_hosts):
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
internal_ip = ws.ipv4
|
|
|
|
login = _ingest(
|
|
db_session,
|
|
t0,
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDP login",
|
|
summary="1149",
|
|
details={"user": r"B26\Alice"},
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(hours=1),
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details={"user": r"B26\Bob", "internal_ip": internal_ip},
|
|
)
|
|
db_session.refresh(login)
|
|
|
|
assert close_workstation_session_for_rdg_end(db_session, end) is None
|
|
assert event_session_terminated(login, db=db_session) is False
|
|
|
|
|
|
def test_ip_mismatch_does_not_close_login(db_session, rdg_settings, rdg_hosts):
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\TSA"
|
|
|
|
login = _ingest(
|
|
db_session,
|
|
t0,
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": ws.ipv4},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDP login",
|
|
summary="1149",
|
|
details={"user": user},
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(hours=1),
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.8-SAC"},
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details={"user": user, "internal_ip": "192.168.163.200"},
|
|
)
|
|
db_session.refresh(login)
|
|
|
|
assert close_workstation_session_for_rdg_end(db_session, end) is None
|
|
assert event_session_terminated(login, db=db_session) is False
|
|
|
|
|
|
def test_empty_rcm1149_user_enriched_from_prior_rdg302(db_session, rdg_settings, rdg_hosts):
|
|
"""COMM-PC class: EventLog 1149 has empty Param1; RDG 302 already has the account."""
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\s.shelkovaya"
|
|
internal_ip = ws.ipv4
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="warning",
|
|
title="RDG 302",
|
|
summary="302",
|
|
details={"user": user, "internal_ip": internal_ip},
|
|
)
|
|
login = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=3),
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="warning",
|
|
title="RDP connection (RCM 1149)",
|
|
summary="RCM 1149 - 192.168.160.40",
|
|
details={"user": "-", "ip_address": "192.168.160.40", "event_id_windows": 1149},
|
|
)
|
|
db_session.refresh(login)
|
|
|
|
assert login.details["user"] == user
|
|
assert login.details.get("user_enriched_from_rdg_event_id")
|
|
summary = event_to_summary(login, db_session)
|
|
assert summary.actor_user == user
|
|
|
|
|
|
def test_empty_rcm1149_backfilled_when_rdg302_arrives_later(db_session, rdg_settings, rdg_hosts):
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\s.shelkovaya"
|
|
internal_ip = ws.ipv4
|
|
|
|
login = _ingest(
|
|
db_session,
|
|
t0 + timedelta(seconds=1),
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDP connection (RCM 1149)",
|
|
summary="RCM 1149 - 1.2.3.4",
|
|
details={"user": "-", "event_id_windows": 1149},
|
|
)
|
|
assert login.details["user"] == "-"
|
|
|
|
_ingest(
|
|
db_session,
|
|
t0,
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
|
|
type="rdg.connection.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDG 302",
|
|
summary="302",
|
|
details={"user": user, "internal_ip": internal_ip},
|
|
)
|
|
db_session.refresh(login)
|
|
assert login.details["user"] == user
|
|
|
|
|
|
def test_empty_user_login_still_closed_by_rdg303(db_session, rdg_settings, rdg_hosts):
|
|
ws, _gw = rdg_hosts
|
|
t0 = datetime.now(timezone.utc)
|
|
user = r"B26\s.shelkovaya"
|
|
internal_ip = ws.ipv4
|
|
|
|
login = _ingest(
|
|
db_session,
|
|
t0,
|
|
host={"hostname": ws.hostname, "os_family": "windows", "ipv4": internal_ip},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
|
|
type="rdp.login.success",
|
|
category="auth",
|
|
severity="info",
|
|
title="RDP connection (RCM 1149)",
|
|
summary="RCM 1149",
|
|
details={"user": "-", "event_id_windows": 1149},
|
|
)
|
|
end = _ingest(
|
|
db_session,
|
|
t0 + timedelta(minutes=5),
|
|
host={"hostname": "K6A-DC3", "os_family": "windows"},
|
|
source={"product": "rdp-login-monitor", "product_version": "2.1.13-SAC"},
|
|
type="rdg.connection.disconnected",
|
|
category="auth",
|
|
severity="info",
|
|
title="303",
|
|
summary="303",
|
|
details={"user": user, "internal_ip": internal_ip},
|
|
)
|
|
db_session.refresh(login)
|
|
assert login.details.get(SESSION_CLOSED_BY_RDG_AT_KEY) == end.occurred_at.isoformat()
|
|
assert event_session_terminated(login, db=db_session) is True
|