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.
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.9.12"
|
||||
assert APP_VERSION == "0.9.13"
|
||||
assert APP_NAME == "Security Alert Center"
|
||||
assert APP_VERSION_LABEL == "Security Alert Center v.0.9.12"
|
||||
assert APP_VERSION_LABEL == "Security Alert Center v.0.9.13"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Tests for RDG 302→303 session flap rule."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models import Problem
|
||||
from app.services.ingest import ingest_event
|
||||
from app.services.problems import maybe_create_problem
|
||||
from app.services.problem_rules import RULE_RDG_SESSION_FLAP
|
||||
from app.services.rdg_session_flap import (
|
||||
evaluate_rdg_session_flap,
|
||||
event_has_rdg_flap,
|
||||
find_rdg_success_before_end,
|
||||
)
|
||||
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()
|
||||
|
||||
|
||||
def test_rdg_flap_detects_302_then_303(db_session, rdg_settings):
|
||||
t0 = datetime.now(timezone.utc)
|
||||
user = "B26\\test.user"
|
||||
details = {"user": user, "internal_ip": "192.168.163.32"}
|
||||
|
||||
_ingest(
|
||||
db_session,
|
||||
t0,
|
||||
type="rdg.connection.success",
|
||||
category="auth",
|
||||
severity="info",
|
||||
title="RD Gateway event 302",
|
||||
summary="302",
|
||||
details=details,
|
||||
)
|
||||
end = _ingest(
|
||||
db_session,
|
||||
t0 + timedelta(seconds=4),
|
||||
type="rdg.connection.disconnected",
|
||||
category="auth",
|
||||
severity="info",
|
||||
title="RD Gateway event 303",
|
||||
summary="303",
|
||||
details=details,
|
||||
)
|
||||
|
||||
match = evaluate_rdg_session_flap(db_session, end)
|
||||
assert match is not None
|
||||
assert match.rule_id == RULE_RDG_SESSION_FLAP
|
||||
assert event_has_rdg_flap(end) is True
|
||||
|
||||
problem, created = maybe_create_problem(db_session, end)
|
||||
assert created is True
|
||||
assert problem.rule_id == RULE_RDG_SESSION_FLAP
|
||||
|
||||
|
||||
def test_rdg_flap_ignores_gap_over_10_sec(db_session, rdg_settings):
|
||||
t0 = datetime.now(timezone.utc)
|
||||
user = "B26\\slow.user"
|
||||
details = {"user": user}
|
||||
|
||||
_ingest(
|
||||
db_session,
|
||||
t0,
|
||||
type="rdg.connection.success",
|
||||
category="auth",
|
||||
severity="info",
|
||||
title="302",
|
||||
summary="302",
|
||||
details=details,
|
||||
)
|
||||
end = _ingest(
|
||||
db_session,
|
||||
t0 + timedelta(seconds=15),
|
||||
type="rdg.connection.disconnected",
|
||||
category="auth",
|
||||
severity="info",
|
||||
title="303",
|
||||
summary="303",
|
||||
details=details,
|
||||
)
|
||||
|
||||
assert find_rdg_success_before_end(db_session, end) is None
|
||||
assert evaluate_rdg_session_flap(db_session, end) is None
|
||||
|
||||
|
||||
def test_rdg_flap_dedup_within_30_sec(db_session, rdg_settings):
|
||||
t0 = datetime.now(timezone.utc)
|
||||
user = "B26\\dup.user"
|
||||
details = {"user": user}
|
||||
|
||||
def pair(at: datetime):
|
||||
_ingest(
|
||||
db_session,
|
||||
at,
|
||||
type="rdg.connection.success",
|
||||
category="auth",
|
||||
severity="info",
|
||||
title="302",
|
||||
summary="302",
|
||||
details=details,
|
||||
)
|
||||
return _ingest(
|
||||
db_session,
|
||||
at + timedelta(seconds=5),
|
||||
type="rdg.connection.disconnected",
|
||||
category="auth",
|
||||
severity="info",
|
||||
title="303",
|
||||
summary="303",
|
||||
details=details,
|
||||
)
|
||||
|
||||
end1 = pair(t0)
|
||||
maybe_create_problem(db_session, end1)
|
||||
db_session.flush()
|
||||
|
||||
end2 = pair(t0 + timedelta(seconds=10))
|
||||
maybe_create_problem(db_session, end2)
|
||||
db_session.flush()
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
open_count = db_session.scalar(
|
||||
select(func.count()).select_from(Problem).where(
|
||||
Problem.rule_id == RULE_RDG_SESSION_FLAP,
|
||||
Problem.status == "open",
|
||||
)
|
||||
)
|
||||
assert open_count == 1
|
||||
Reference in New Issue
Block a user