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,171 @@
|
||||
import asyncio
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app import main
|
||||
from app.models import NormalizedEvent, ParseIssue
|
||||
from app.parser import ParseResult
|
||||
from app.version import VERSION
|
||||
|
||||
|
||||
def request(method: str, url: str, **kwargs) -> httpx.Response:
|
||||
async def send() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
return await client.request(method, url, **kwargs)
|
||||
|
||||
return asyncio.run(send())
|
||||
|
||||
|
||||
def _fake_parse(_data: bytes) -> ParseResult:
|
||||
event = NormalizedEvent(id="1",timestamp=datetime(2026,9,9,tzinfo=UTC),provider="Test Provider",event_id=1000,level="error",channel="Application",computer="LAB-PC",category="crash",summary="Synthetic crash",fields={})
|
||||
return ParseResult(events=[event], issues=[], truncated=False, duration_ms=1)
|
||||
|
||||
|
||||
def test_api_sets_no_store_headers(monkeypatch):
|
||||
monkeypatch.setattr(main, "parse_evtx_bytes", _fake_parse)
|
||||
response = request("POST", "/api/analyze", files={"file": ("sample.evtx", b"synthetic", "application/octet-stream")})
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.headers["pragma"] == "no-cache"
|
||||
|
||||
|
||||
def test_api_sets_response_security_headers():
|
||||
response = request("GET", "/health")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["x-content-type-options"] == "nosniff"
|
||||
assert response.headers["referrer-policy"] == "no-referrer"
|
||||
assert response.headers["x-frame-options"] == "DENY"
|
||||
assert "frame-ancestors 'none'" in response.headers["content-security-policy"]
|
||||
|
||||
|
||||
def test_api_exposes_semantic_partial_parse_metrics(monkeypatch):
|
||||
def partial_parse(_data: bytes) -> ParseResult:
|
||||
event = _fake_parse(_data).events[0]
|
||||
return ParseResult(
|
||||
events=[event],
|
||||
issues=[ParseIssue(code="event_limit_reached", message="later records were not analyzed", skipped_record=True)],
|
||||
truncated=True,
|
||||
duration_ms=12,
|
||||
status="partial",
|
||||
truncation_reason="event_limit_reached",
|
||||
records_seen=4,
|
||||
skipped_records=1,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(main, "parse_evtx_bytes", partial_parse)
|
||||
response = request("POST", "/api/analyze", files={"file": ("sample.evtx", b"synthetic", "application/octet-stream")})
|
||||
assert response.status_code == 200
|
||||
parse = response.json()["parse"]
|
||||
assert parse["status"] == "partial"
|
||||
assert parse["parsed_events"] == 1
|
||||
assert parse["records_seen"] == 4
|
||||
assert parse["skipped_records"] == 1
|
||||
assert parse["issue_count"] == 1
|
||||
assert parse["issues"][0]["code"] == "event_limit_reached"
|
||||
|
||||
|
||||
def test_health_sets_no_store_headers():
|
||||
response = request("GET", "/health")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.headers["pragma"] == "no-cache"
|
||||
|
||||
|
||||
def test_health_uses_the_repository_version_source():
|
||||
response = request("GET", "/health")
|
||||
assert response.json()["version"] == VERSION
|
||||
|
||||
|
||||
def test_health_exposes_exact_build_revision_and_preserves_existing_fields(monkeypatch):
|
||||
revision = "0123456789abcdef0123456789abcdef01234567"
|
||||
monkeypatch.setenv("EVENTLENS_REVISION", revision)
|
||||
|
||||
response = request("GET", "/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"status": "ok",
|
||||
"evtx_ingestion": True,
|
||||
"version": VERSION,
|
||||
"revision": revision,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("revision", [None, "", "unknown", "A" * 40, "a" * 39, "a" * 64])
|
||||
def test_health_uses_deterministic_local_build_revision_fallback(monkeypatch, revision):
|
||||
if revision is None:
|
||||
monkeypatch.delenv("EVENTLENS_REVISION", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("EVENTLENS_REVISION", revision)
|
||||
|
||||
response = request("GET", "/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["revision"] == "local"
|
||||
|
||||
|
||||
def test_analysis_does_not_persist_upload_in_working_directory(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(main, "parse_evtx_bytes", _fake_parse)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
before = list(tmp_path.iterdir())
|
||||
response = request("POST", "/api/analyze", files={"file": ("sample.evtx", b"synthetic", "application/octet-stream")})
|
||||
after = list(tmp_path.iterdir())
|
||||
assert response.status_code == 200
|
||||
assert before == after == []
|
||||
|
||||
|
||||
def test_api_sanitizes_filename_in_analysis_source(monkeypatch):
|
||||
monkeypatch.setattr(main, "parse_evtx_bytes", _fake_parse)
|
||||
response = request(
|
||||
"POST",
|
||||
"/api/analyze",
|
||||
files={"file": (r"..\private\LAB-PC.evtx", b"synthetic", "application/octet-stream")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["source"] == "LAB-PC.evtx"
|
||||
|
||||
|
||||
def test_api_rejects_oversized_upload_before_parser(monkeypatch):
|
||||
monkeypatch.setattr(main, "MAX_UPLOAD_BYTES", 8)
|
||||
response = request(
|
||||
"POST", "/api/analyze", files={"file": ("sample.evtx", b"123456789", "application/octet-stream")}
|
||||
)
|
||||
assert response.status_code == 413
|
||||
|
||||
|
||||
def test_request_limit_response_is_uncacheable_and_nosniff():
|
||||
response = request(
|
||||
"POST",
|
||||
"/api/analyze",
|
||||
headers={"Content-Length": str(main.MAX_REQUEST_BYTES + 1)},
|
||||
content=b"oversized",
|
||||
)
|
||||
assert response.status_code == 413
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.headers["pragma"] == "no-cache"
|
||||
assert response.headers["x-content-type-options"] == "nosniff"
|
||||
|
||||
|
||||
def test_api_rejects_when_parse_capacity_is_full(monkeypatch):
|
||||
import time
|
||||
|
||||
def slow_parse(_data: bytes) -> ParseResult:
|
||||
time.sleep(0.35)
|
||||
return _fake_parse(_data)
|
||||
|
||||
async def send_two():
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
return await asyncio.gather(
|
||||
client.post("/api/analyze", files={"file": ("one.evtx", b"synthetic", "application/octet-stream")}),
|
||||
client.post("/api/analyze", files={"file": ("two.evtx", b"synthetic", "application/octet-stream")}),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(main, "parse_evtx_bytes", slow_parse)
|
||||
monkeypatch.setattr(main, "PARSE_QUEUE_LIMIT", 0)
|
||||
monkeypatch.setattr(main, "_parse_inflight", 0)
|
||||
responses = asyncio.run(send_two())
|
||||
assert sorted(response.status_code for response in responses) == [200, 503]
|
||||
Reference in new issue
Block a user