feat: RDG 302/303 display path labels and RDS session break (0.3.1)
Show RDS access via RDG-Comp or Haproxy-RDG-Comp, enable qwinsta/logoff on any RDG event with internal_ip. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL
|
||||
|
||||
|
||||
def test_version_constants():
|
||||
assert APP_VERSION == "0.3.0"
|
||||
assert APP_VERSION == "0.3.1"
|
||||
assert APP_NAME == "Security Alert Center"
|
||||
assert APP_VERSION_LABEL == "Security Alert Center v.0.3.0"
|
||||
assert APP_VERSION_LABEL == "Security Alert Center v.0.3.1"
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Tests for RDG event display (access path, title/summary)."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models import Event, Host
|
||||
from app.services.event_summary import event_to_summary
|
||||
from app.services.rdg_display import (
|
||||
ACCESS_PATH_DIRECT,
|
||||
ACCESS_PATH_HAPROXY,
|
||||
build_rdg_display,
|
||||
classify_rdg_access_path,
|
||||
event_supports_rdg_client_qwinsta,
|
||||
)
|
||||
|
||||
|
||||
def _rdg_event(
|
||||
db_session,
|
||||
*,
|
||||
gw: Host,
|
||||
event_type: str = "rdg.connection.success",
|
||||
external_ip: str = "10.0.0.5",
|
||||
internal_ip: str = "192.168.160.3",
|
||||
win_id: int = 302,
|
||||
) -> Event:
|
||||
event = Event(
|
||||
event_id=f"ev-rdg-{win_id}",
|
||||
host_id=gw.id,
|
||||
occurred_at=datetime.now(timezone.utc),
|
||||
received_at=datetime.now(timezone.utc),
|
||||
category="auth",
|
||||
type=event_type,
|
||||
severity="info",
|
||||
title="RD Gateway event 302",
|
||||
summary="",
|
||||
payload={},
|
||||
details={
|
||||
"user": r"B26\papatramp",
|
||||
"external_ip": external_ip,
|
||||
"internal_ip": internal_ip,
|
||||
"event_id_windows": win_id,
|
||||
},
|
||||
)
|
||||
db_session.add(event)
|
||||
db_session.commit()
|
||||
db_session.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
def test_classify_rdg_access_path_direct(monkeypatch):
|
||||
monkeypatch.setenv("SAC_RDG_HAPROXY_EXTERNAL_IPS", "192.168.160.50")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
assert classify_rdg_access_path("10.0.0.5") == ACCESS_PATH_DIRECT
|
||||
|
||||
|
||||
def test_classify_rdg_access_path_haproxy(monkeypatch):
|
||||
monkeypatch.setenv("SAC_RDG_HAPROXY_EXTERNAL_IPS", "192.168.160.50,10.0.0.100")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
assert classify_rdg_access_path("10.0.0.100") == ACCESS_PATH_HAPROXY
|
||||
|
||||
|
||||
def test_build_rdg_display_title_and_path(db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_RDG_HAPROXY_EXTERNAL_IPS", "")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
gw = Host(hostname="K6A-DC3", os_family="windows", product="rdp-login-monitor", ipv4="192.168.160.40")
|
||||
ws = Host(hostname="WS-PC", os_family="windows", product="rdp-login-monitor", ipv4="192.168.160.3")
|
||||
db_session.add_all([gw, ws])
|
||||
db_session.commit()
|
||||
|
||||
event = _rdg_event(db_session, gw=gw)
|
||||
display = build_rdg_display(event, db_session)
|
||||
assert display is not None
|
||||
assert ACCESS_PATH_DIRECT in display.title
|
||||
assert "WS-PC" in display.title
|
||||
assert "192.168.160.3" in display.title
|
||||
assert display.access_path == ACCESS_PATH_DIRECT
|
||||
assert display.qwinsta_enabled is True
|
||||
assert "papatramp" in display.summary
|
||||
assert "шлюз K6A-DC3" in display.summary
|
||||
|
||||
|
||||
def test_event_to_summary_enriches_rdg(db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_RDG_HAPROXY_EXTERNAL_IPS", "10.0.0.5")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
gw = Host(hostname="K6A-DC3", os_family="windows", product="rdp-login-monitor", ipv4="192.168.160.40")
|
||||
db_session.add(gw)
|
||||
db_session.commit()
|
||||
|
||||
event = _rdg_event(db_session, gw=gw, external_ip="10.0.0.5")
|
||||
summary = event_to_summary(event, db_session)
|
||||
assert summary.rdg_access_path == ACCESS_PATH_HAPROXY
|
||||
assert summary.rdg_qwinsta_enabled is True
|
||||
assert "RDS подключение" in summary.title
|
||||
assert ACCESS_PATH_HAPROXY in summary.title
|
||||
|
||||
|
||||
def test_event_supports_rdg_client_qwinsta_without_flap(db_session):
|
||||
gw = Host(hostname="K6A-DC3", os_family="windows", product="rdp-login-monitor", ipv4="192.168.160.40")
|
||||
db_session.add(gw)
|
||||
db_session.commit()
|
||||
event = _rdg_event(db_session, gw=gw)
|
||||
assert event_supports_rdg_client_qwinsta(event) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"internal_ip",
|
||||
["", None],
|
||||
)
|
||||
def test_event_supports_rdg_client_qwinsta_requires_internal_ip(db_session, internal_ip):
|
||||
gw = Host(hostname="K6A-DC3", os_family="windows", product="rdp-login-monitor", ipv4="192.168.160.40")
|
||||
db_session.add(gw)
|
||||
db_session.commit()
|
||||
event = _rdg_event(db_session, gw=gw, internal_ip=internal_ip or "")
|
||||
if internal_ip is None:
|
||||
event.details = {k: v for k, v in event.details.items() if k != "internal_ip"}
|
||||
assert event_supports_rdg_client_qwinsta(event) is False
|
||||
@@ -109,3 +109,79 @@ def test_qwinsta_client_not_in_hosts(jwt_headers, client, db_session, monkeypatc
|
||||
response = client.post(f"/api/v1/events/{event.id}/actions/qwinsta", headers=jwt_headers)
|
||||
assert response.status_code == 404
|
||||
assert "not found" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_qwinsta_without_rdg_flap(jwt_headers, client, db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_WIN_ADMIN_USER", r"B26\admin")
|
||||
monkeypatch.setenv("SAC_WIN_ADMIN_PASSWORD", "pw")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
ws = Host(
|
||||
hostname="Andrisonova-PC",
|
||||
os_family="windows",
|
||||
product="rdp-login-monitor",
|
||||
ipv4="192.168.160.113",
|
||||
)
|
||||
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()
|
||||
|
||||
event = Event(
|
||||
event_id="ev-rdg-plain",
|
||||
host_id=gw.id,
|
||||
occurred_at=datetime.now(timezone.utc),
|
||||
received_at=datetime.now(timezone.utc),
|
||||
category="auth",
|
||||
type="rdg.connection.success",
|
||||
severity="info",
|
||||
title="302",
|
||||
summary="",
|
||||
payload={},
|
||||
details={"user": r"B26\papatramp", "internal_ip": ws.ipv4},
|
||||
)
|
||||
db_session.add(event)
|
||||
db_session.commit()
|
||||
|
||||
with patch("app.services.rdg_winrm_actions.run_winrm_on_host_targets") as mock_run:
|
||||
mock_run.return_value = (
|
||||
WinRmCmdResult(ok=True, message="OK", target="Andrisonova-PC", stdout="ok", exit_code=0),
|
||||
["Andrisonova-PC=OK"],
|
||||
)
|
||||
response = client.post(f"/api/v1/events/{event.id}/actions/qwinsta", headers=jwt_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
def test_qwinsta_rejects_rdg_without_internal_ip(jwt_headers, client, db_session, monkeypatch):
|
||||
monkeypatch.setenv("SAC_WIN_ADMIN_USER", r"B26\admin")
|
||||
monkeypatch.setenv("SAC_WIN_ADMIN_PASSWORD", "pw")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
gw = Host(hostname="K6A-DC3", os_family="windows", product="rdp-login-monitor", ipv4="192.168.160.40")
|
||||
db_session.add(gw)
|
||||
db_session.commit()
|
||||
|
||||
event = Event(
|
||||
event_id="ev-rdg-no-ip",
|
||||
host_id=gw.id,
|
||||
occurred_at=datetime.now(timezone.utc),
|
||||
received_at=datetime.now(timezone.utc),
|
||||
category="auth",
|
||||
type="rdg.connection.success",
|
||||
severity="info",
|
||||
title="302",
|
||||
summary="",
|
||||
payload={},
|
||||
details={"user": r"B26\user"},
|
||||
)
|
||||
db_session.add(event)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(f"/api/v1/events/{event.id}/actions/qwinsta", headers=jwt_headers)
|
||||
assert response.status_code == 400
|
||||
assert "internal_ip" in response.json()["detail"].lower()
|
||||
|
||||
Reference in New Issue
Block a user