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