@simbimbo/brainstem 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +26 -0
- package/brainstem/__init__.py +1 -1
- package/brainstem/adapters.py +120 -0
- package/brainstem/api.py +468 -57
- package/brainstem/config.py +136 -0
- package/brainstem/connectors/logicmonitor.py +57 -0
- package/brainstem/demo.py +16 -2
- package/brainstem/fingerprint.py +54 -0
- package/brainstem/ingest.py +440 -33
- package/brainstem/interesting.py +56 -1
- package/brainstem/listener.py +181 -0
- package/brainstem/models.py +1 -0
- package/brainstem/recurrence.py +63 -9
- package/brainstem/scoring.py +6 -4
- package/brainstem/source_drivers.py +179 -0
- package/brainstem/storage.py +389 -12
- package/docs/README.md +103 -0
- package/docs/api.md +260 -280
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/tests/test_adapters.py +95 -0
- package/tests/test_api.py +812 -0
- package/tests/test_canonicalization.py +8 -0
- package/tests/test_config.py +39 -0
- package/tests/test_file_ingest.py +77 -0
- package/tests/test_fingerprint.py +51 -1
- package/tests/test_interesting.py +10 -0
- package/tests/test_listener.py +253 -0
- package/tests/test_logicmonitor.py +54 -1
- package/tests/test_recurrence.py +16 -0
- package/tests/test_source_drivers.py +95 -0
- package/tests/test_storage.py +178 -1
package/tests/test_api.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
+
import pytest
|
|
3
4
|
from fastapi.testclient import TestClient
|
|
4
5
|
|
|
6
|
+
from brainstem import __version__
|
|
5
7
|
from brainstem.api import app
|
|
8
|
+
from brainstem.fingerprint import normalize_message
|
|
6
9
|
from brainstem.models import RawInputEnvelope
|
|
7
10
|
from brainstem.storage import (
|
|
8
11
|
init_db,
|
|
12
|
+
get_raw_envelope_by_id,
|
|
9
13
|
set_raw_envelope_status,
|
|
10
14
|
store_raw_envelopes,
|
|
11
15
|
)
|
|
@@ -71,6 +75,376 @@ def test_ingest_batch_and_interesting(tmp_path: Path) -> None:
|
|
|
71
75
|
assert interesting_payload["items"]
|
|
72
76
|
|
|
73
77
|
|
|
78
|
+
def test_ingest_batch_mixed_success_and_failure_returns_per_item_accounting(tmp_path: Path) -> None:
|
|
79
|
+
client = TestClient(app)
|
|
80
|
+
db_path = tmp_path / "brainstem_batch_accounting.sqlite3"
|
|
81
|
+
payload = {
|
|
82
|
+
"threshold": 2,
|
|
83
|
+
"db_path": str(db_path),
|
|
84
|
+
"events": [
|
|
85
|
+
{
|
|
86
|
+
"tenant_id": "client-a",
|
|
87
|
+
"source_type": "syslog",
|
|
88
|
+
"message_raw": "Failed password for admin from 10.1.2.3",
|
|
89
|
+
"host": "fw-01",
|
|
90
|
+
"service": "sshd",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"tenant_id": "client-b",
|
|
94
|
+
"source_type": "syslog",
|
|
95
|
+
"message_raw": "VPN tunnel dropped and recovered",
|
|
96
|
+
"host": "fw-02",
|
|
97
|
+
"service": "charon",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"tenant_id": "client-a",
|
|
101
|
+
"source_type": "syslog",
|
|
102
|
+
"message_raw": "",
|
|
103
|
+
"host": "fw-01",
|
|
104
|
+
"service": "sshd",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
response = client.post("/ingest/batch", json=payload)
|
|
109
|
+
assert response.status_code == 200
|
|
110
|
+
batch_payload = response.json()
|
|
111
|
+
|
|
112
|
+
assert batch_payload["ok"] is True
|
|
113
|
+
assert batch_payload["item_count"] == 3
|
|
114
|
+
assert batch_payload["event_count"] == 2
|
|
115
|
+
assert batch_payload["parse_failed"] == 1
|
|
116
|
+
assert "item_results" in batch_payload
|
|
117
|
+
assert len(batch_payload["item_results"]) == 3
|
|
118
|
+
|
|
119
|
+
item_by_index = {item["index"]: item for item in batch_payload["item_results"]}
|
|
120
|
+
assert set(item_by_index.keys()) == {0, 1, 2}
|
|
121
|
+
assert item_by_index[0]["status"] == "canonicalized"
|
|
122
|
+
assert item_by_index[1]["status"] == "canonicalized"
|
|
123
|
+
assert item_by_index[2]["status"] == "parse_failed"
|
|
124
|
+
assert batch_payload["item_results"][2]["raw_envelope_id"] is not None
|
|
125
|
+
assert item_by_index[2]["failure_reason"] == "message_raw is empty and cannot be canonicalized"
|
|
126
|
+
|
|
127
|
+
for index, item in item_by_index.items():
|
|
128
|
+
assert item["tenant_id"] in {"client-a", "client-b"}
|
|
129
|
+
assert item["source_type"] == "syslog"
|
|
130
|
+
assert isinstance(item["index"], int)
|
|
131
|
+
assert "raw_envelope_id" in item
|
|
132
|
+
assert "failure_reason" in item
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_ingest_logicmonitor_endpoint_persists_and_surfaces_via_runtime_inspection(tmp_path: Path) -> None:
|
|
136
|
+
client = TestClient(app)
|
|
137
|
+
db_path = tmp_path / "logicmonitor.sqlite3"
|
|
138
|
+
|
|
139
|
+
response = client.post(
|
|
140
|
+
"/ingest/logicmonitor",
|
|
141
|
+
json={
|
|
142
|
+
"tenant_id": "client-a",
|
|
143
|
+
"source_path": "/logicmonitor/webhook",
|
|
144
|
+
"threshold": 2,
|
|
145
|
+
"db_path": str(db_path),
|
|
146
|
+
"events": [
|
|
147
|
+
{
|
|
148
|
+
"resource_id": 123,
|
|
149
|
+
"resource_name": "edge-fw-01",
|
|
150
|
+
"severity": "warning",
|
|
151
|
+
"timestamp": "2026-03-22T00:00:00Z",
|
|
152
|
+
"message_raw": "VPN tunnel dropped and recovered",
|
|
153
|
+
"metadata": {
|
|
154
|
+
"datasource": "IPSec Tunnel",
|
|
155
|
+
"instance_name": "site-b",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"resource_id": 124,
|
|
160
|
+
"resource_name": "edge-fw-02",
|
|
161
|
+
"severity": "warning",
|
|
162
|
+
"timestamp": "2026-03-22T00:00:01Z",
|
|
163
|
+
"message_raw": "VPN tunnel dropped and recovered",
|
|
164
|
+
"metadata": {
|
|
165
|
+
"datasource": "IPSec Tunnel",
|
|
166
|
+
"instance_name": "site-c",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
assert response.status_code == 200
|
|
173
|
+
payload = response.json()
|
|
174
|
+
assert payload["ok"] is True
|
|
175
|
+
assert payload["event_count"] == 2
|
|
176
|
+
assert payload["signature_count"] >= 1
|
|
177
|
+
|
|
178
|
+
raw_response = client.get(f"/raw_envelopes?db_path={db_path}&source_type=logicmonitor&limit=10")
|
|
179
|
+
assert raw_response.status_code == 200
|
|
180
|
+
raw_payload = raw_response.json()
|
|
181
|
+
assert raw_payload["count"] == 2
|
|
182
|
+
assert all(item["source_type"] == "logicmonitor" for item in raw_payload["items"])
|
|
183
|
+
assert all(item["canonicalization_status"] == "canonicalized" for item in raw_payload["items"])
|
|
184
|
+
|
|
185
|
+
canonical_response = client.get(f"/canonical_events?db_path={db_path}&source=logicmonitor&limit=10")
|
|
186
|
+
assert canonical_response.status_code == 200
|
|
187
|
+
canonical_payload = canonical_response.json()
|
|
188
|
+
assert canonical_payload["count"] == 2
|
|
189
|
+
assert all(item["source"] == "logicmonitor" for item in canonical_payload["items"])
|
|
190
|
+
|
|
191
|
+
sources_response = client.get(f"/sources?db_path={db_path}&limit=10")
|
|
192
|
+
assert sources_response.status_code == 200
|
|
193
|
+
sources_payload = sources_response.json()
|
|
194
|
+
source_types = [entry["value"] for entry in sources_payload["items"]["source_type"]]
|
|
195
|
+
assert "logicmonitor" in source_types
|
|
196
|
+
|
|
197
|
+
runtime_response = client.get("/runtime")
|
|
198
|
+
assert runtime_response.status_code == 200
|
|
199
|
+
runtime_payload = runtime_response.json()
|
|
200
|
+
assert runtime_payload["runtime"]["capability_flags"]["ingest_endpoints"]["logicmonitor_events"] is True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_candidates_endpoint_returns_candidate_inspection_payload_and_supports_filtering(tmp_path: Path) -> None:
|
|
204
|
+
client = TestClient(app)
|
|
205
|
+
db_path = tmp_path / "brainstem_candidates.sqlite3"
|
|
206
|
+
ingest_response = client.post(
|
|
207
|
+
"/ingest/batch",
|
|
208
|
+
json={
|
|
209
|
+
"threshold": 2,
|
|
210
|
+
"db_path": str(db_path),
|
|
211
|
+
"events": [
|
|
212
|
+
{
|
|
213
|
+
"tenant_id": "client-a",
|
|
214
|
+
"source_type": "syslog",
|
|
215
|
+
"message_raw": "Failed password for admin from 10.1.2.3",
|
|
216
|
+
"host": "fw-01",
|
|
217
|
+
"service": "sshd",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"tenant_id": "client-a",
|
|
221
|
+
"source_type": "syslog",
|
|
222
|
+
"message_raw": "Failed password for admin from 10.1.2.3",
|
|
223
|
+
"host": "fw-01",
|
|
224
|
+
"service": "sshd",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
assert ingest_response.status_code == 200
|
|
230
|
+
candidates = client.get(f"/candidates?db_path={db_path}&limit=10")
|
|
231
|
+
assert candidates.status_code == 200
|
|
232
|
+
candidates_payload = candidates.json()
|
|
233
|
+
assert candidates_payload["ok"] is True
|
|
234
|
+
assert candidates_payload["count"] >= 1
|
|
235
|
+
assert len(candidates_payload["items"]) >= 1
|
|
236
|
+
|
|
237
|
+
item = candidates_payload["items"][0]
|
|
238
|
+
assert item["title"]
|
|
239
|
+
assert item["summary"]
|
|
240
|
+
assert item["decision_band"] in {"watch", "review", "urgent_human_review", "promote_to_incident_memory", "ignore"}
|
|
241
|
+
assert item["attention_band"] in {"ignore_fast", "background", "watch", "investigate", "promote"}
|
|
242
|
+
assert item["attention_score"] >= 0
|
|
243
|
+
assert item["score_total"] == item["attention_score"]
|
|
244
|
+
assert isinstance(item["score_breakdown"], dict)
|
|
245
|
+
assert item["raw_envelope_ids"]
|
|
246
|
+
assert isinstance(item["raw_envelopes"], list)
|
|
247
|
+
assert [envelope["id"] for envelope in item["raw_envelopes"]] == item["raw_envelope_ids"]
|
|
248
|
+
|
|
249
|
+
filtered_by_decision = client.get(
|
|
250
|
+
f"/candidates?db_path={db_path}&decision_band={item['decision_band']}&limit=10"
|
|
251
|
+
)
|
|
252
|
+
assert filtered_by_decision.status_code == 200
|
|
253
|
+
filtered_payload = filtered_by_decision.json()
|
|
254
|
+
assert filtered_payload["count"] >= 1
|
|
255
|
+
assert all(i["decision_band"] == item["decision_band"] for i in filtered_payload["items"])
|
|
256
|
+
|
|
257
|
+
filtered_by_type = client.get(f"/candidates?db_path={db_path}&candidate_type={item['candidate_type']}&limit=1")
|
|
258
|
+
assert filtered_by_type.status_code == 200
|
|
259
|
+
limited_payload = filtered_by_type.json()
|
|
260
|
+
assert limited_payload["count"] == 1
|
|
261
|
+
assert len(limited_payload["items"]) <= 1
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_signatures_endpoint_returns_signature_payload_and_supports_filtering(tmp_path: Path) -> None:
|
|
265
|
+
client = TestClient(app)
|
|
266
|
+
db_path = tmp_path / "brainstem_signatures.sqlite3"
|
|
267
|
+
ingest_response = client.post(
|
|
268
|
+
"/ingest/batch",
|
|
269
|
+
json={
|
|
270
|
+
"threshold": 2,
|
|
271
|
+
"db_path": str(db_path),
|
|
272
|
+
"events": [
|
|
273
|
+
{
|
|
274
|
+
"tenant_id": "client-a",
|
|
275
|
+
"source_type": "syslog",
|
|
276
|
+
"message_raw": "Failed password for admin from 10.1.2.3",
|
|
277
|
+
"host": "fw-01",
|
|
278
|
+
"service": "sshd",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"tenant_id": "client-a",
|
|
282
|
+
"source_type": "syslog",
|
|
283
|
+
"message_raw": "Failed password for admin from 10.1.2.3",
|
|
284
|
+
"host": "fw-01",
|
|
285
|
+
"service": "sshd",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"tenant_id": "client-a",
|
|
289
|
+
"source_type": "syslog",
|
|
290
|
+
"message_raw": "Different event in another family",
|
|
291
|
+
"host": "fw-01",
|
|
292
|
+
"service": "systemd",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
assert ingest_response.status_code == 200
|
|
298
|
+
|
|
299
|
+
signatures = client.get(f"/signatures?db_path={db_path}&limit=10")
|
|
300
|
+
assert signatures.status_code == 200
|
|
301
|
+
signatures_payload = signatures.json()
|
|
302
|
+
assert signatures_payload["ok"] is True
|
|
303
|
+
assert signatures_payload["count"] >= 2
|
|
304
|
+
assert len(signatures_payload["items"]) >= 2
|
|
305
|
+
|
|
306
|
+
first_signature = signatures_payload["items"][0]
|
|
307
|
+
assert first_signature["signature_key"]
|
|
308
|
+
assert first_signature["event_family"]
|
|
309
|
+
assert first_signature["normalized_pattern"]
|
|
310
|
+
assert isinstance(first_signature["occurrence_count"], int)
|
|
311
|
+
assert first_signature["occurrence_count"] >= 2
|
|
312
|
+
assert isinstance(first_signature["raw_envelope_ids"], list)
|
|
313
|
+
assert first_signature["raw_envelope_count"] == len(first_signature["raw_envelope_ids"])
|
|
314
|
+
assert isinstance(first_signature["recurrence"], dict)
|
|
315
|
+
assert first_signature["recurrence"]["signature_id"] > 0
|
|
316
|
+
assert first_signature["raw_envelope_count"] >= 1
|
|
317
|
+
|
|
318
|
+
family_filtered = client.get(
|
|
319
|
+
f"/signatures?db_path={db_path}&event_family={first_signature['event_family']}&limit=10"
|
|
320
|
+
)
|
|
321
|
+
assert family_filtered.status_code == 200
|
|
322
|
+
family_filtered_payload = family_filtered.json()
|
|
323
|
+
assert family_filtered_payload["count"] >= 1
|
|
324
|
+
assert all(item["event_family"] == first_signature["event_family"] for item in family_filtered_payload["items"])
|
|
325
|
+
|
|
326
|
+
service_filtered = client.get(
|
|
327
|
+
f"/signatures?db_path={db_path}&service=sshd&limit=10"
|
|
328
|
+
)
|
|
329
|
+
assert service_filtered.status_code == 200
|
|
330
|
+
service_filtered_payload = service_filtered.json()
|
|
331
|
+
assert service_filtered_payload["count"] >= 1
|
|
332
|
+
assert all(item["service"] == "sshd" for item in service_filtered_payload["items"])
|
|
333
|
+
|
|
334
|
+
min_occurrence_filtered = client.get(
|
|
335
|
+
f"/signatures?db_path={db_path}&min_occurrence_count=2&limit=10"
|
|
336
|
+
)
|
|
337
|
+
assert min_occurrence_filtered.status_code == 200
|
|
338
|
+
min_occurrence_payload = min_occurrence_filtered.json()
|
|
339
|
+
assert min_occurrence_payload["count"] >= 1
|
|
340
|
+
assert all(item["occurrence_count"] >= 2 for item in min_occurrence_payload["items"])
|
|
341
|
+
|
|
342
|
+
limited = client.get(f"/signatures?db_path={db_path}&limit=1")
|
|
343
|
+
assert limited.status_code == 200
|
|
344
|
+
limited_payload = limited.json()
|
|
345
|
+
assert limited_payload["count"] == 1
|
|
346
|
+
assert len(limited_payload["items"]) <= 1
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_canonical_events_endpoint_returns_normalized_fields_and_supports_filters(tmp_path: Path) -> None:
|
|
350
|
+
client = TestClient(app)
|
|
351
|
+
db_path = tmp_path / "brainstem_canonical_events.sqlite3"
|
|
352
|
+
ingest_response = client.post(
|
|
353
|
+
"/ingest/batch",
|
|
354
|
+
json={
|
|
355
|
+
"threshold": 1,
|
|
356
|
+
"db_path": str(db_path),
|
|
357
|
+
"events": [
|
|
358
|
+
{
|
|
359
|
+
"tenant_id": "client-a",
|
|
360
|
+
"source_type": "syslog",
|
|
361
|
+
"message_raw": "IPsec SA rekey succeeded on host 10.1.2.3",
|
|
362
|
+
"host": "fw-01",
|
|
363
|
+
"service": "charon",
|
|
364
|
+
"severity": "info",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"tenant_id": "client-a",
|
|
368
|
+
"source_type": "syslog",
|
|
369
|
+
"message_raw": "Service restart detected on node 2",
|
|
370
|
+
"host": "fw-01",
|
|
371
|
+
"service": "systemd",
|
|
372
|
+
"severity": "warning",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"tenant_id": "client-a",
|
|
376
|
+
"source_type": "file",
|
|
377
|
+
"message_raw": "Configuration drift detected for node 3",
|
|
378
|
+
"host": "fw-02",
|
|
379
|
+
"service": "charon",
|
|
380
|
+
"severity": "critical",
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"tenant_id": "client-b",
|
|
384
|
+
"source_type": "file",
|
|
385
|
+
"message_raw": "",
|
|
386
|
+
"host": "fw-02",
|
|
387
|
+
"service": "sshd",
|
|
388
|
+
"severity": "info",
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
)
|
|
393
|
+
assert ingest_response.status_code == 200
|
|
394
|
+
|
|
395
|
+
tenant_events = client.get(f"/canonical_events?db_path={db_path}&tenant_id=client-a&limit=10")
|
|
396
|
+
assert tenant_events.status_code == 200
|
|
397
|
+
tenant_payload = tenant_events.json()
|
|
398
|
+
assert tenant_payload["ok"] is True
|
|
399
|
+
assert tenant_payload["count"] == 3
|
|
400
|
+
assert tenant_payload["items"][0]["tenant_id"] == "client-a"
|
|
401
|
+
expected_normalized = {
|
|
402
|
+
normalize_message("IPsec SA rekey succeeded on host 10.1.2.3"),
|
|
403
|
+
normalize_message("Service restart detected on node 2"),
|
|
404
|
+
normalize_message("Configuration drift detected for node 3"),
|
|
405
|
+
}
|
|
406
|
+
first = tenant_payload["items"][0]
|
|
407
|
+
assert first["raw_envelope_id"] > 0
|
|
408
|
+
assert first["tenant_id"]
|
|
409
|
+
assert first["source"] in {"syslog", "file"}
|
|
410
|
+
assert first["host"]
|
|
411
|
+
assert first["service"]
|
|
412
|
+
assert first["severity"] in {"info", "warning", "critical"}
|
|
413
|
+
assert first["message_raw"]
|
|
414
|
+
assert first["message_normalized"] == normalize_message(first["message_raw"])
|
|
415
|
+
assert set(item["message_normalized"] for item in tenant_payload["items"]) == expected_normalized
|
|
416
|
+
|
|
417
|
+
limited = client.get(f"/canonical_events?db_path={db_path}&tenant_id=client-a&limit=1")
|
|
418
|
+
assert limited.status_code == 200
|
|
419
|
+
limited_payload = limited.json()
|
|
420
|
+
assert limited_payload["count"] == 1
|
|
421
|
+
assert len(limited_payload["items"]) <= 1
|
|
422
|
+
|
|
423
|
+
host_filtered = client.get(f"/canonical_events?db_path={db_path}&tenant_id=client-a&host=fw-01")
|
|
424
|
+
assert host_filtered.status_code == 200
|
|
425
|
+
host_payload = host_filtered.json()
|
|
426
|
+
assert host_payload["count"] == 2
|
|
427
|
+
assert all(item["host"] == "fw-01" for item in host_payload["items"])
|
|
428
|
+
|
|
429
|
+
source_filtered = client.get(f"/canonical_events?db_path={db_path}&tenant_id=client-a&source=file")
|
|
430
|
+
assert source_filtered.status_code == 200
|
|
431
|
+
source_payload = source_filtered.json()
|
|
432
|
+
assert source_payload["count"] == 1
|
|
433
|
+
assert source_payload["items"][0]["source"] == "file"
|
|
434
|
+
|
|
435
|
+
service_filtered = client.get(f"/canonical_events?db_path={db_path}&tenant_id=client-a&service=charon")
|
|
436
|
+
assert service_filtered.status_code == 200
|
|
437
|
+
service_payload = service_filtered.json()
|
|
438
|
+
assert service_payload["count"] == 2
|
|
439
|
+
assert all(item["service"] == "charon" for item in service_payload["items"])
|
|
440
|
+
|
|
441
|
+
severity_filtered = client.get(f"/canonical_events?db_path={db_path}&severity=warning&tenant_id=client-a")
|
|
442
|
+
assert severity_filtered.status_code == 200
|
|
443
|
+
severity_payload = severity_filtered.json()
|
|
444
|
+
assert severity_payload["count"] == 1
|
|
445
|
+
assert severity_payload["items"][0]["severity"] == "warning"
|
|
446
|
+
|
|
447
|
+
|
|
74
448
|
def test_stats_after_successful_and_failed_ingest(tmp_path: Path) -> None:
|
|
75
449
|
client = TestClient(app)
|
|
76
450
|
db_path = tmp_path / "brainstem.sqlite3"
|
|
@@ -127,6 +501,175 @@ def test_healthz_is_ready() -> None:
|
|
|
127
501
|
assert response.json()["ok"] is True
|
|
128
502
|
|
|
129
503
|
|
|
504
|
+
def test_healthz_reports_api_token_status(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
505
|
+
client = TestClient(app)
|
|
506
|
+
monkeypatch.delenv("BRAINSTEM_API_TOKEN", raising=False)
|
|
507
|
+
response = client.get("/healthz")
|
|
508
|
+
assert response.status_code == 200
|
|
509
|
+
payload = response.json()
|
|
510
|
+
assert payload["api_token_enabled"] is False
|
|
511
|
+
assert payload["runtime"]["auth_state"]["api_token_configured"] is False
|
|
512
|
+
|
|
513
|
+
monkeypatch.setenv("BRAINSTEM_API_TOKEN", "local-token")
|
|
514
|
+
response = client.get("/healthz")
|
|
515
|
+
assert response.status_code == 200
|
|
516
|
+
payload = response.json()
|
|
517
|
+
assert payload["api_token_enabled"] is True
|
|
518
|
+
assert payload["runtime"]["auth_state"]["api_token_configured"] is True
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_healthz_reports_runtime_summary() -> None:
|
|
522
|
+
client = TestClient(app)
|
|
523
|
+
response = client.get("/healthz")
|
|
524
|
+
assert response.status_code == 200
|
|
525
|
+
payload = response.json()
|
|
526
|
+
runtime = payload["runtime"]
|
|
527
|
+
assert runtime["version"] == __version__
|
|
528
|
+
assert runtime["capability_flags"]["ingest_endpoints"]["single_event"] is True
|
|
529
|
+
assert runtime["defaults"]["interesting_limit"] == 5
|
|
530
|
+
assert runtime["limits"]["replay_raw_max_ids"] == 32
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_status_endpoint_reports_operator_summary() -> None:
|
|
534
|
+
client = TestClient(app)
|
|
535
|
+
response = client.get("/status")
|
|
536
|
+
assert response.status_code == 200
|
|
537
|
+
payload = response.json()
|
|
538
|
+
assert payload["ok"] is True
|
|
539
|
+
assert payload["status"] == "ok"
|
|
540
|
+
assert payload["api_token_enabled"] == payload["runtime"]["auth_state"]["api_token_configured"]
|
|
541
|
+
assert payload["runtime"]["capability_flags"]["inspection_endpoints"]["raw_envelopes"] is True
|
|
542
|
+
assert payload["runtime"]["runtime"]["api_token_env"] == "BRAINSTEM_API_TOKEN"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_status_and_healthz_are_coherent() -> None:
|
|
546
|
+
client = TestClient(app)
|
|
547
|
+
status_response = client.get("/status")
|
|
548
|
+
healthz_response = client.get("/healthz")
|
|
549
|
+
assert status_response.status_code == 200
|
|
550
|
+
assert healthz_response.status_code == 200
|
|
551
|
+
assert status_response.json() == healthz_response.json()
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def test_runtime_endpoint_includes_source_capability_matrix() -> None:
|
|
555
|
+
client = TestClient(app)
|
|
556
|
+
response = client.get("/runtime")
|
|
557
|
+
assert response.status_code == 200
|
|
558
|
+
source_capabilities = response.json()["runtime"]["capability_flags"]["source_capabilities"]
|
|
559
|
+
source_types = source_capabilities["source_types"]
|
|
560
|
+
|
|
561
|
+
assert "syslog" in source_types
|
|
562
|
+
assert "file" in source_types
|
|
563
|
+
assert "logicmonitor" in source_types
|
|
564
|
+
|
|
565
|
+
per_source = {item["source_type"]: item["modes"] for item in source_capabilities["ingest_modes_by_source_type"]}
|
|
566
|
+
assert {"mode": "udp_listener", "endpoint": "brainstem.listener"} in per_source["syslog"]
|
|
567
|
+
assert {"mode": "single_event_api", "endpoint": "/ingest/event"} in per_source["file"]
|
|
568
|
+
assert {"mode": "batch_api", "endpoint": "/ingest/batch"} in per_source["file"]
|
|
569
|
+
assert {"mode": "logicmonitor_webhook", "endpoint": "/ingest/logicmonitor"} in per_source["logicmonitor"]
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_runtime_endpoint_reports_config_object(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
573
|
+
custom_db = tmp_path / "runtime.sqlite3"
|
|
574
|
+
monkeypatch.setenv("BRAINSTEM_DB_PATH", str(custom_db))
|
|
575
|
+
client = TestClient(app)
|
|
576
|
+
|
|
577
|
+
response = client.get("/runtime")
|
|
578
|
+
assert response.status_code == 200
|
|
579
|
+
runtime = response.json()["runtime"]
|
|
580
|
+
|
|
581
|
+
config = runtime["runtime"]["config"]
|
|
582
|
+
assert config["api_token_env_var"] == "BRAINSTEM_API_TOKEN"
|
|
583
|
+
assert config["listener"]["syslog_host"] == "127.0.0.1"
|
|
584
|
+
assert config["listener"]["syslog_port"] == 5514
|
|
585
|
+
assert config["listener"]["syslog_source_path"] == "/dev/udp"
|
|
586
|
+
assert config["defaults"]["ingest_threshold"] == 2
|
|
587
|
+
assert config["db"]["default_path"] == str(custom_db)
|
|
588
|
+
assert runtime["defaults"] == config["defaults"]
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_runtime_endpoint_provides_same_summary(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
592
|
+
client = TestClient(app)
|
|
593
|
+
monkeypatch.setenv("BRAINSTEM_API_TOKEN", "runtime-token")
|
|
594
|
+
response = client.get("/runtime")
|
|
595
|
+
assert response.status_code == 200
|
|
596
|
+
payload = response.json()
|
|
597
|
+
assert payload["ok"] is True
|
|
598
|
+
runtime = payload["runtime"]
|
|
599
|
+
assert runtime["auth_state"]["api_token_configured"] is True
|
|
600
|
+
assert runtime["runtime"]["api_token_env"] == "BRAINSTEM_API_TOKEN"
|
|
601
|
+
assert runtime["limits"]["replay_raw_max_ids"] == 32
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def test_unprotected_routes_remain_open_when_api_token_not_configured(
|
|
605
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
606
|
+
tmp_path: Path,
|
|
607
|
+
) -> None:
|
|
608
|
+
monkeypatch.delenv("BRAINSTEM_API_TOKEN", raising=False)
|
|
609
|
+
client = TestClient(app)
|
|
610
|
+
db_path = tmp_path / "open.sqlite3"
|
|
611
|
+
|
|
612
|
+
ingest_response = client.post(
|
|
613
|
+
f"/ingest/event?threshold=1&db_path={db_path}",
|
|
614
|
+
json={
|
|
615
|
+
"tenant_id": "client-a",
|
|
616
|
+
"source_type": "syslog",
|
|
617
|
+
"message_raw": "Service restarted",
|
|
618
|
+
"host": "fw-01",
|
|
619
|
+
"service": "systemd",
|
|
620
|
+
},
|
|
621
|
+
)
|
|
622
|
+
assert ingest_response.status_code == 200
|
|
623
|
+
|
|
624
|
+
healthz_response = client.get(f"/interesting?db_path={db_path}&limit=10")
|
|
625
|
+
assert healthz_response.status_code == 200
|
|
626
|
+
assert healthz_response.json()["ok"] is True
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_api_token_is_required_for_write_and_inspection_routes_when_enabled(
|
|
630
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
631
|
+
tmp_path: Path,
|
|
632
|
+
) -> None:
|
|
633
|
+
monkeypatch.setenv("BRAINSTEM_API_TOKEN", "valid-token")
|
|
634
|
+
client = TestClient(app)
|
|
635
|
+
db_path = tmp_path / "auth.sqlite3"
|
|
636
|
+
|
|
637
|
+
unauthenticated = client.post(
|
|
638
|
+
f"/ingest/event?threshold=1&db_path={db_path}",
|
|
639
|
+
json={
|
|
640
|
+
"tenant_id": "client-a",
|
|
641
|
+
"source_type": "syslog",
|
|
642
|
+
"message_raw": "Service restarted",
|
|
643
|
+
"host": "fw-01",
|
|
644
|
+
"service": "systemd",
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
assert unauthenticated.status_code == 401
|
|
648
|
+
|
|
649
|
+
wrong_token = client.get(f"/interesting?db_path={db_path}&limit=10", headers={"X-API-Token": "wrong"})
|
|
650
|
+
assert wrong_token.status_code == 401
|
|
651
|
+
|
|
652
|
+
authed = client.post(
|
|
653
|
+
f"/ingest/event?threshold=1&db_path={db_path}",
|
|
654
|
+
headers={"Authorization": "Bearer valid-token"},
|
|
655
|
+
json={
|
|
656
|
+
"tenant_id": "client-a",
|
|
657
|
+
"source_type": "syslog",
|
|
658
|
+
"message_raw": "Service restarted",
|
|
659
|
+
"host": "fw-01",
|
|
660
|
+
"service": "systemd",
|
|
661
|
+
},
|
|
662
|
+
)
|
|
663
|
+
assert authed.status_code == 200
|
|
664
|
+
|
|
665
|
+
read_authed = client.get(
|
|
666
|
+
f"/interesting?db_path={db_path}&limit=10",
|
|
667
|
+
headers={"X-API-Token": "valid-token"},
|
|
668
|
+
)
|
|
669
|
+
assert read_authed.status_code == 200
|
|
670
|
+
assert read_authed.json()["ok"] is True
|
|
671
|
+
|
|
672
|
+
|
|
130
673
|
def test_failures_endpoint_lists_recent_parse_failures(tmp_path: Path) -> None:
|
|
131
674
|
client = TestClient(app)
|
|
132
675
|
db_path = tmp_path / "brainstem.sqlite3"
|
|
@@ -213,6 +756,94 @@ def test_failures_endpoint_filters_by_status_and_fetches_single_record(tmp_path:
|
|
|
213
756
|
assert invalid.status_code == 422
|
|
214
757
|
|
|
215
758
|
|
|
759
|
+
def test_raw_envelopes_endpoint_supports_status_and_source_filters(tmp_path: Path) -> None:
|
|
760
|
+
client = TestClient(app)
|
|
761
|
+
db_path = tmp_path / "brainstem.sqlite3"
|
|
762
|
+
init_db(str(db_path))
|
|
763
|
+
raw_ids = store_raw_envelopes(
|
|
764
|
+
[
|
|
765
|
+
RawInputEnvelope(
|
|
766
|
+
tenant_id="tenant-a",
|
|
767
|
+
source_type="syslog",
|
|
768
|
+
source_id="fw-01",
|
|
769
|
+
source_path="/var/log/syslog",
|
|
770
|
+
timestamp="2026-03-22T00:00:01Z",
|
|
771
|
+
message_raw="VPN tunnel recovered",
|
|
772
|
+
),
|
|
773
|
+
RawInputEnvelope(
|
|
774
|
+
tenant_id="tenant-a",
|
|
775
|
+
source_type="syslog",
|
|
776
|
+
source_id="fw-01",
|
|
777
|
+
source_path="/var/log/auth.log",
|
|
778
|
+
timestamp="2026-03-22T00:00:02Z",
|
|
779
|
+
message_raw="",
|
|
780
|
+
),
|
|
781
|
+
RawInputEnvelope(
|
|
782
|
+
tenant_id="tenant-b",
|
|
783
|
+
source_type="file",
|
|
784
|
+
source_id="agent-01",
|
|
785
|
+
source_path="/tmp/agent.log",
|
|
786
|
+
timestamp="2026-03-22T00:00:03Z",
|
|
787
|
+
message_raw="backup finished",
|
|
788
|
+
),
|
|
789
|
+
RawInputEnvelope(
|
|
790
|
+
tenant_id="tenant-a",
|
|
791
|
+
source_type="file",
|
|
792
|
+
source_id="fw-01",
|
|
793
|
+
source_path="/var/log/syslog",
|
|
794
|
+
timestamp="2026-03-22T00:00:04Z",
|
|
795
|
+
message_raw="disk pressure warning",
|
|
796
|
+
),
|
|
797
|
+
],
|
|
798
|
+
db_path=str(db_path),
|
|
799
|
+
)
|
|
800
|
+
set_raw_envelope_status(raw_ids[1], "parse_failed", db_path=str(db_path), failure_reason="seeded parse failure")
|
|
801
|
+
set_raw_envelope_status(raw_ids[2], "unsupported", db_path=str(db_path), failure_reason="seeded unsupported")
|
|
802
|
+
|
|
803
|
+
response = client.get(f"/raw_envelopes?db_path={db_path}&limit=10")
|
|
804
|
+
assert response.status_code == 200
|
|
805
|
+
payload = response.json()
|
|
806
|
+
assert payload["ok"] is True
|
|
807
|
+
assert payload["count"] == 4
|
|
808
|
+
assert [item["id"] for item in payload["items"]] == [raw_ids[3], raw_ids[2], raw_ids[1], raw_ids[0]]
|
|
809
|
+
|
|
810
|
+
parse_failed = client.get(f"/raw_envelopes?db_path={db_path}&status=parse_failed&limit=10")
|
|
811
|
+
assert parse_failed.status_code == 200
|
|
812
|
+
parse_payload = parse_failed.json()
|
|
813
|
+
assert parse_payload["count"] == 1
|
|
814
|
+
assert parse_payload["items"][0]["id"] == raw_ids[1]
|
|
815
|
+
assert parse_payload["items"][0]["canonicalization_status"] == "parse_failed"
|
|
816
|
+
|
|
817
|
+
syslog_only = client.get(f"/raw_envelopes?db_path={db_path}&source_type=syslog&limit=10")
|
|
818
|
+
assert syslog_only.status_code == 200
|
|
819
|
+
syslog_payload = syslog_only.json()
|
|
820
|
+
assert [item["id"] for item in syslog_payload["items"]] == [raw_ids[1], raw_ids[0]]
|
|
821
|
+
|
|
822
|
+
fw_source = client.get(f"/raw_envelopes?db_path={db_path}&source_id=fw-01&limit=10")
|
|
823
|
+
assert fw_source.status_code == 200
|
|
824
|
+
fw_payload = fw_source.json()
|
|
825
|
+
assert [item["id"] for item in fw_payload["items"]] == [raw_ids[3], raw_ids[1], raw_ids[0]]
|
|
826
|
+
|
|
827
|
+
source_path = client.get(f"/raw_envelopes?db_path={db_path}&source_path=/var/log/syslog&limit=10")
|
|
828
|
+
assert source_path.status_code == 200
|
|
829
|
+
path_payload = source_path.json()
|
|
830
|
+
assert [item["id"] for item in path_payload["items"]] == [raw_ids[3], raw_ids[0]]
|
|
831
|
+
|
|
832
|
+
tenant_and_source = client.get(
|
|
833
|
+
f"/raw_envelopes?db_path={db_path}&tenant_id=tenant-a&source_type=file&source_path=/var/log/syslog&limit=10"
|
|
834
|
+
)
|
|
835
|
+
assert tenant_and_source.status_code == 200
|
|
836
|
+
tenant_source_payload = tenant_and_source.json()
|
|
837
|
+
assert [item["id"] for item in tenant_source_payload["items"]] == [raw_ids[3]]
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def test_raw_envelopes_endpoint_rejects_invalid_status_filter(tmp_path: Path) -> None:
|
|
841
|
+
client = TestClient(app)
|
|
842
|
+
db_path = tmp_path / "brainstem.sqlite3"
|
|
843
|
+
response = client.get(f"/raw_envelopes?db_path={db_path}&status=bogus")
|
|
844
|
+
assert response.status_code == 422
|
|
845
|
+
|
|
846
|
+
|
|
216
847
|
def test_sources_endpoint_summarizes_ingest_dimensions(tmp_path: Path) -> None:
|
|
217
848
|
client = TestClient(app)
|
|
218
849
|
db_path = tmp_path / "brainstem.sqlite3"
|
|
@@ -274,6 +905,73 @@ def test_sources_endpoint_summarizes_ingest_dimensions(tmp_path: Path) -> None:
|
|
|
274
905
|
}
|
|
275
906
|
|
|
276
907
|
|
|
908
|
+
def test_sources_status_endpoint_returns_source_health_like_summary(tmp_path: Path) -> None:
|
|
909
|
+
client = TestClient(app)
|
|
910
|
+
db_path = tmp_path / "brainstem.sqlite3"
|
|
911
|
+
ingest_response = client.post(
|
|
912
|
+
"/ingest/batch",
|
|
913
|
+
json={
|
|
914
|
+
"threshold": 1,
|
|
915
|
+
"db_path": str(db_path),
|
|
916
|
+
"events": [
|
|
917
|
+
{
|
|
918
|
+
"tenant_id": "client-a",
|
|
919
|
+
"source_type": "syslog",
|
|
920
|
+
"source_id": "fw-01",
|
|
921
|
+
"source_name": "edge-fw-01",
|
|
922
|
+
"source_path": "/var/log/syslog",
|
|
923
|
+
"message_raw": "Service restarted",
|
|
924
|
+
"host": "fw-01",
|
|
925
|
+
"service": "systemd",
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
"tenant_id": "client-a",
|
|
929
|
+
"source_type": "syslog",
|
|
930
|
+
"source_id": "fw-01",
|
|
931
|
+
"source_name": "edge-fw-01",
|
|
932
|
+
"source_path": "/var/log/syslog",
|
|
933
|
+
"message_raw": "",
|
|
934
|
+
"host": "fw-01",
|
|
935
|
+
"service": "systemd",
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
"tenant_id": "client-a",
|
|
939
|
+
"source_type": "logicmonitor",
|
|
940
|
+
"source_id": "lm-01",
|
|
941
|
+
"source_name": "edge-lm-01",
|
|
942
|
+
"source_path": "/alerts",
|
|
943
|
+
"message_raw": "Disk space low",
|
|
944
|
+
"host": "lm-01",
|
|
945
|
+
"service": "logicmonitor",
|
|
946
|
+
},
|
|
947
|
+
],
|
|
948
|
+
},
|
|
949
|
+
)
|
|
950
|
+
assert ingest_response.status_code == 200
|
|
951
|
+
|
|
952
|
+
response = client.get(f"/sources/status?db_path={db_path}&limit=10")
|
|
953
|
+
assert response.status_code == 200
|
|
954
|
+
payload = response.json()
|
|
955
|
+
assert payload["ok"] is True
|
|
956
|
+
assert payload["count"] == 2
|
|
957
|
+
fw01 = next(item for item in payload["items"] if item["source_type"] == "syslog" and item["source_id"] == "fw-01")
|
|
958
|
+
assert fw01["raw_count"] == 2
|
|
959
|
+
assert fw01["canonicalized_count"] == 1
|
|
960
|
+
assert fw01["parse_failed_count"] == 1
|
|
961
|
+
assert fw01["unsupported_count"] == 0
|
|
962
|
+
assert fw01["source_path"] == "/var/log/syslog"
|
|
963
|
+
assert fw01["first_seen_at"] <= fw01["last_seen_at"]
|
|
964
|
+
|
|
965
|
+
filtered = client.get(
|
|
966
|
+
f"/sources/status?db_path={db_path}&source_type=syslog&source_id=fw-01&source_path=/var/log/syslog&limit=10"
|
|
967
|
+
)
|
|
968
|
+
assert filtered.status_code == 200
|
|
969
|
+
filtered_payload = filtered.json()
|
|
970
|
+
assert filtered_payload["count"] == 1
|
|
971
|
+
assert filtered_payload["items"][0]["source_id"] == "fw-01"
|
|
972
|
+
assert filtered_payload["items"][0]["source_path"] == "/var/log/syslog"
|
|
973
|
+
|
|
974
|
+
|
|
277
975
|
def test_ingest_recent_endpoint_returns_recent_intake_and_allows_status_filter(tmp_path: Path) -> None:
|
|
278
976
|
client = TestClient(app)
|
|
279
977
|
db_path = tmp_path / "brainstem.sqlite3"
|
|
@@ -317,3 +1015,117 @@ def test_ingest_recent_endpoint_returns_recent_intake_and_allows_status_filter(t
|
|
|
317
1015
|
failed_payload = failed.json()
|
|
318
1016
|
assert failed_payload["count"] == 1
|
|
319
1017
|
assert failed_payload["items"][0]["canonicalization_status"] == "parse_failed"
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def test_replay_raw_endpoint_replays_parse_failed_and_received_records(tmp_path: Path) -> None:
|
|
1021
|
+
client = TestClient(app)
|
|
1022
|
+
db_path = tmp_path / "brainstem.sqlite3"
|
|
1023
|
+
init_db(str(db_path))
|
|
1024
|
+
raw_envelope_ids = store_raw_envelopes(
|
|
1025
|
+
[
|
|
1026
|
+
RawInputEnvelope(
|
|
1027
|
+
tenant_id="client-a",
|
|
1028
|
+
source_type="syslog",
|
|
1029
|
+
timestamp="2026-03-22T00:00:01Z",
|
|
1030
|
+
message_raw="can canonicalize first",
|
|
1031
|
+
host="fw-01",
|
|
1032
|
+
service="sshd",
|
|
1033
|
+
),
|
|
1034
|
+
RawInputEnvelope(
|
|
1035
|
+
tenant_id="client-a",
|
|
1036
|
+
source_type="syslog",
|
|
1037
|
+
timestamp="2026-03-22T00:00:02Z",
|
|
1038
|
+
message_raw="can canonicalize second",
|
|
1039
|
+
host="fw-01",
|
|
1040
|
+
service="sshd",
|
|
1041
|
+
),
|
|
1042
|
+
RawInputEnvelope(
|
|
1043
|
+
tenant_id="client-a",
|
|
1044
|
+
source_type="syslog",
|
|
1045
|
+
timestamp="2026-03-22T00:00:03Z",
|
|
1046
|
+
message_raw="",
|
|
1047
|
+
host="fw-01",
|
|
1048
|
+
service="sshd",
|
|
1049
|
+
),
|
|
1050
|
+
],
|
|
1051
|
+
db_path=str(db_path),
|
|
1052
|
+
)
|
|
1053
|
+
set_raw_envelope_status(raw_envelope_ids[0], "parse_failed", db_path=str(db_path), failure_reason="seeded parse failure")
|
|
1054
|
+
set_raw_envelope_status(raw_envelope_ids[2], "parse_failed", db_path=str(db_path), failure_reason="seeded parse failure")
|
|
1055
|
+
|
|
1056
|
+
response = client.post(
|
|
1057
|
+
"/replay/raw",
|
|
1058
|
+
json={
|
|
1059
|
+
"db_path": str(db_path),
|
|
1060
|
+
"raw_envelope_ids": raw_envelope_ids,
|
|
1061
|
+
"threshold": 1,
|
|
1062
|
+
},
|
|
1063
|
+
)
|
|
1064
|
+
assert response.status_code == 200
|
|
1065
|
+
payload = response.json()
|
|
1066
|
+
assert payload["ok"] is True
|
|
1067
|
+
assert payload["attempted_raw_envelope_ids"] == raw_envelope_ids
|
|
1068
|
+
assert payload["event_count"] == 2
|
|
1069
|
+
assert payload["parse_failed"] == 1
|
|
1070
|
+
|
|
1071
|
+
parse_failed_row = get_raw_envelope_by_id(raw_envelope_ids[0], db_path=str(db_path))
|
|
1072
|
+
assert parse_failed_row is not None
|
|
1073
|
+
assert parse_failed_row["canonicalization_status"] == "canonicalized"
|
|
1074
|
+
|
|
1075
|
+
received_row = get_raw_envelope_by_id(raw_envelope_ids[1], db_path=str(db_path))
|
|
1076
|
+
assert received_row is not None
|
|
1077
|
+
assert received_row["canonicalization_status"] == "canonicalized"
|
|
1078
|
+
|
|
1079
|
+
still_failed_row = get_raw_envelope_by_id(raw_envelope_ids[2], db_path=str(db_path))
|
|
1080
|
+
assert still_failed_row is not None
|
|
1081
|
+
assert still_failed_row["canonicalization_status"] == "parse_failed"
|
|
1082
|
+
assert still_failed_row["failure_reason"] == "message_raw is empty and cannot be canonicalized"
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def test_replay_raw_endpoint_skips_non_replayable_statuses_without_force(tmp_path: Path) -> None:
|
|
1086
|
+
client = TestClient(app)
|
|
1087
|
+
db_path = tmp_path / "brainstem.sqlite3"
|
|
1088
|
+
init_db(str(db_path))
|
|
1089
|
+
(canonicalized_id,) = store_raw_envelopes(
|
|
1090
|
+
[
|
|
1091
|
+
RawInputEnvelope(
|
|
1092
|
+
tenant_id="client-a",
|
|
1093
|
+
source_type="syslog",
|
|
1094
|
+
timestamp="2026-03-22T00:00:01Z",
|
|
1095
|
+
message_raw="already canonicalized",
|
|
1096
|
+
host="fw-01",
|
|
1097
|
+
service="sshd",
|
|
1098
|
+
)
|
|
1099
|
+
],
|
|
1100
|
+
db_path=str(db_path),
|
|
1101
|
+
)
|
|
1102
|
+
set_raw_envelope_status(canonicalized_id, "canonicalized", db_path=str(db_path))
|
|
1103
|
+
|
|
1104
|
+
skip = client.post(
|
|
1105
|
+
"/replay/raw",
|
|
1106
|
+
json={
|
|
1107
|
+
"db_path": str(db_path),
|
|
1108
|
+
"raw_envelope_ids": [canonicalized_id],
|
|
1109
|
+
"threshold": 1,
|
|
1110
|
+
},
|
|
1111
|
+
)
|
|
1112
|
+
assert skip.status_code == 200
|
|
1113
|
+
skipped_payload = skip.json()
|
|
1114
|
+
assert skipped_payload["attempted_raw_envelope_ids"] == []
|
|
1115
|
+
assert skipped_payload["event_count"] == 0
|
|
1116
|
+
assert skipped_payload["skipped"][0]["reason"] == "not_replayable"
|
|
1117
|
+
|
|
1118
|
+
force = client.post(
|
|
1119
|
+
"/replay/raw",
|
|
1120
|
+
json={
|
|
1121
|
+
"db_path": str(db_path),
|
|
1122
|
+
"raw_envelope_ids": [canonicalized_id],
|
|
1123
|
+
"threshold": 1,
|
|
1124
|
+
"force": True,
|
|
1125
|
+
"allowed_statuses": ["canonicalized"],
|
|
1126
|
+
},
|
|
1127
|
+
)
|
|
1128
|
+
assert force.status_code == 200
|
|
1129
|
+
force_payload = force.json()
|
|
1130
|
+
assert force_payload["attempted_raw_envelope_ids"] == [canonicalized_id]
|
|
1131
|
+
assert force_payload["event_count"] == 1
|