Initial curated EventLens source from 0d80d3b38dfb817285481665b9e71e4b8a0656a0
This commit is contained in:
commit
663ea8c829
85 files changed
+12395
No files matched your search
@@ -0,0 +1,267 @@
|
||||
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
|
||||
Reference in new issue
Block a user