Files
EventLens-Public/backend/tests/test_post_v1_hardening.py
T

268 lines
11 KiB
Python

import asyncio
from datetime import UTC, datetime, timedelta
import pytest
from app import main, parser
from app.correlate import correlate
from app.models import NormalizedEvent
from app.normalize import normalize_record
from app.parser import UnexpectedParserError, parse_evtx_bytes
def normalized(
event_id: str,
timestamp: datetime | None,
*,
provider: str,
number: int,
channel: str = "Security",
computer: str = "LAB-PC",
fields: dict[str, str] | None = None,
level: str = "warning",
category: str = "authentication",
summary: str = "Synthetic event",
) -> NormalizedEvent:
return NormalizedEvent(
id=event_id,
timestamp=timestamp,
provider=provider,
event_id=number,
level=level,
channel=channel,
computer=computer,
category=category, # type: ignore[arg-type]
summary=summary,
fields=fields or {},
)
def record(provider: str, event_id: int, timestamp: str | None = "2026-09-09T08:00:00Z") -> dict:
system = {"Provider": {"#attributes": {"Name": provider}}, "EventID": event_id}
if timestamp is not None:
system["TimeCreated"] = {"#attributes": {"SystemTime": timestamp}}
return {"Event": {"System": system}}
def test_invalid_timestamps_are_marked_and_never_become_epoch_evidence():
event = normalize_record(record("Microsoft-Windows-Security-Auditing", 4625, "not-a-time"), record_timestamp="also-bad")
assert event.timestamp is None
assert event.timestamp_source == "invalid"
assert event.timestamp_valid is False
incident = correlate(
[event.model_copy(update={"level": "warning", "category": "other", "channel": "Security", "computer": "LAB-PC"})]
)[0]
assert incident.start is None
assert incident.end is None
def test_valid_record_timestamp_is_an_explicit_fallback():
event = normalize_record(record("Microsoft-Windows-Security-Auditing", 4625, "not-a-time"), record_timestamp="2026-09-09T08:00:00+02:00")
assert event.timestamp == datetime(2026, 9, 9, 6, 0, tzinfo=UTC)
assert event.timestamp_source == "record"
assert event.timestamp_valid is True
def test_invalid_record_timestamp_is_not_silent_missing_timestamp():
event = normalize_record(record("Microsoft-Windows-Security-Auditing", 4625, timestamp=None), record_timestamp="not-a-time")
assert event.timestamp is None
assert event.timestamp_source == "invalid"
assert event.timestamp_valid is False
def test_unreliable_timestamp_metadata_cannot_enter_temporal_correlation():
event = normalized(
"unreliable",
datetime(2026, 9, 9, 8, 0, tzinfo=UTC),
provider="Microsoft-Windows-Security-Auditing",
number=4625,
).model_copy(update={"timestamp_source": "invalid", "timestamp_valid": False})
incidents = correlate([event])
assert incidents[0].start is None
assert incidents[0].end is None
@pytest.mark.parametrize(
("provider", "event_id", "channel", "expected"),
[
("Other Provider", 4625, "Security", "other"),
("Other Provider", 1014, "System", "other"),
("Other Provider", 1000, "Application", "application"),
("Application Error", 1000, "System", "other"),
("Windows Error Reporting", 1001, "Application", "crash"),
],
)
def test_event_ids_are_scoped_to_provider_and_channel(provider, event_id, channel, expected):
event = normalize_record(record(provider, event_id), record_timestamp="2026-09-09T08:00:00Z")
event.channel = channel
# Re-normalize the explicit channel because the helper record intentionally stays small.
payload = record(provider, event_id)
payload["Event"]["System"]["Channel"] = channel
normalized_event = normalize_record(payload)
assert normalized_event.category == expected
def test_provider_rules_do_not_match_when_channel_context_is_missing():
payload = record("Microsoft-Windows-Security-Auditing", 4625)
normalized_event = normalize_record(payload)
assert normalized_event.category == "other"
incidents = correlate(
[normalized("1", datetime(2026, 9, 9, 8, 0, tzinfo=UTC), provider=normalized_event.provider, number=4625, channel="", fields={})]
)
assert all(incident.rule_id != "windows.auth.failed-logon" for incident in incidents)
def test_unknown_dimensions_are_isolated_and_one_failure_is_not_a_burst():
start = datetime(2026, 9, 9, 8, 0, tzinfo=UTC)
events = [
normalized(str(index), start + timedelta(seconds=index), provider="Microsoft-Windows-Security-Auditing", number=4625, fields={})
for index in range(2)
]
incidents = correlate(events)
assert len(incidents) == 2
assert all(incident.title == "Failed sign-in recorded" for incident in incidents)
assert all("same known account" not in reason for incident in incidents for reason in incident.reasons)
def test_known_failed_logons_use_thresholded_titles():
start = datetime(2026, 9, 9, 8, 0, tzinfo=UTC)
make = lambda index: normalized(
str(index),
start + timedelta(seconds=index),
provider="Microsoft-Windows-Security-Auditing",
number=4625,
fields={"EventData.TargetUserName": "jens"},
)
assert correlate([make(0)])[0].title == "Failed sign-in recorded"
assert correlate([make(0), make(1)])[0].title == "Repeated failed sign-ins"
assert correlate([make(0), make(1), make(2)])[0].title == "Failed sign-in burst"
def test_startup_and_unexpected_power_are_separate_rules():
start = datetime(2026, 9, 9, 8, 0, tzinfo=UTC)
events = [
normalized("boot", start, provider="Microsoft-Windows-Kernel-General", number=12, channel="System", category="system", summary="OS started", level="information"),
normalized("power", start + timedelta(seconds=20), provider="Microsoft-Windows-Kernel-Power", number=41, channel="System", category="system", summary="Unexpected restart"),
]
rules = {incident.rule_id for incident in correlate(events)}
assert rules == {"windows.system.startup", "windows.system.shutdown.unexpected"}
def test_parse_metrics_count_records_not_issue_messages(monkeypatch):
class FakeParser:
def __init__(self, *_args, **_kwargs):
pass
def records_json(self):
yield {"event_record_id": 1, "timestamp": "2026-09-09T08:00:00Z", "data": '{"Event":{"System":{"Provider":"P","EventID":1}}}'}
yield {"event_record_id": 2, "timestamp": "2026-09-09T08:00:01Z", "data": "not-json"}
monkeypatch.setattr(parser, "PyEvtxParser", FakeParser)
result = parse_evtx_bytes(b"ElfFile\x00synthetic")
assert len(result.events) == 1
assert result.records_seen == 2
assert result.skipped_records == 1
assert len(result.issues) == 1
assert result.issues[0].code == "malformed_record_skipped"
assert result.status == "partial"
def test_timestamp_issue_does_not_claim_records_were_omitted(monkeypatch):
class FakeParser:
def __init__(self, *_args, **_kwargs):
pass
def records_json(self):
yield {"event_record_id": 1, "timestamp": "not-a-time", "data": '{"Event":{"System":{"Provider":"P","EventID":1}}}'}
monkeypatch.setattr(parser, "PyEvtxParser", FakeParser)
result = parse_evtx_bytes(b"ElfFile\x00synthetic")
assert result.status == "complete"
assert result.skipped_records == 0
assert result.issues[0].code == "invalid_timestamp"
def test_event_limit_reports_one_observed_record_without_inventing_remaining_count(monkeypatch):
class FakeParser:
def __init__(self, *_args, **_kwargs):
pass
def records_json(self):
for index in range(3):
yield {"event_record_id": index + 1, "timestamp": "2026-09-09T08:00:00Z", "data": '{"Event":{"System":{"Provider":"P","EventID":1}}}'}
monkeypatch.setattr(parser, "PyEvtxParser", FakeParser)
result = parse_evtx_bytes(b"ElfFile\x00synthetic", max_events=2)
assert len(result.events) == 2
assert result.records_seen == 3
assert result.skipped_records == 1
assert result.truncation_reason == "event_limit_reached"
assert result.issues[0].code == "event_limit_reached"
def test_unexpected_parser_exception_is_not_disguised_as_partial(monkeypatch, caplog):
class FakeParser:
def __init__(self, *_args, **_kwargs):
pass
def records_json(self):
raise AssertionError("secret parser details")
yield # pragma: no cover
monkeypatch.setattr(parser, "PyEvtxParser", FakeParser)
with pytest.raises(UnexpectedParserError):
parse_evtx_bytes(b"ElfFile\x00synthetic")
assert "secret parser details" not in caplog.text
def run_asgi(app, messages, headers=None):
async def execute():
sent = []
pending = list(messages)
async def receive():
return pending.pop(0) if pending else {"type": "http.disconnect"}
async def send(message):
sent.append(message)
scope = {"type": "http", "method": "POST", "path": "/api/analyze", "headers": headers or [], "query_string": b"", "scheme": "http", "server": ("test", 80), "client": ("test", 1), "http_version": "1.1"}
await app(scope, receive, send)
return sent
return asyncio.run(execute())
def test_receive_level_body_limit_handles_chunked_like_requests():
async def sink(scope, receive, send):
while (await receive()).get("type") != "http.disconnect":
pass
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
messages = [{"type": "http.request", "body": b"abc", "more_body": True}, {"type": "http.request", "body": b"d", "more_body": False}]
sent = run_asgi(main.RequestBodyLimitMiddleware(sink, 3), messages)
assert sent[0]["status"] == 413
def test_receive_level_body_limit_accepts_exact_limit():
async def sink(scope, receive, send):
message = await receive()
assert message["body"] == b"abc"
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
sent = run_asgi(
main.RequestBodyLimitMiddleware(sink, 3),
[{"type": "http.request", "body": b"abc", "more_body": False}],
)
assert sent[0]["status"] == 200
def test_receive_level_body_limit_handles_oversized_content_length():
async def sink(scope, receive, send):
raise AssertionError("downstream must not receive an oversized request")
sent = run_asgi(main.RequestBodyLimitMiddleware(sink, 3), [], headers=[(b"content-length", b"4")])
assert sent[0]["status"] == 413