@simbimbo/brainstem 0.0.4 → 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/tests/test_api.py CHANGED
@@ -132,6 +132,74 @@ def test_ingest_batch_mixed_success_and_failure_returns_per_item_accounting(tmp_
132
132
  assert "failure_reason" in item
133
133
 
134
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
+
135
203
  def test_candidates_endpoint_returns_candidate_inspection_payload_and_supports_filtering(tmp_path: Path) -> None:
136
204
  client = TestClient(app)
137
205
  db_path = tmp_path / "brainstem_candidates.sqlite3"
@@ -483,6 +551,24 @@ def test_status_and_healthz_are_coherent() -> None:
483
551
  assert status_response.json() == healthz_response.json()
484
552
 
485
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
+
486
572
  def test_runtime_endpoint_reports_config_object(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
487
573
  custom_db = tmp_path / "runtime.sqlite3"
488
574
  monkeypatch.setenv("BRAINSTEM_DB_PATH", str(custom_db))
@@ -12,10 +12,25 @@ def test_runtime_config_exposes_shared_defaults() -> None:
12
12
  assert cfg.listener.syslog_port == 5514
13
13
  assert cfg.listener.syslog_source_path == "/dev/udp"
14
14
  assert cfg.defaults.ingest_threshold == 2
15
+ assert cfg.defaults.recurrence_threshold == 2
15
16
  assert cfg.defaults.interesting_limit == 5
17
+ assert cfg.candidate_attention.recurrence_count_normalizer == 10
18
+ assert cfg.candidate_attention.decision_band_watch == 0.25
16
19
  assert cfg.limits.replay_raw_max_ids == 32
17
20
 
18
21
 
22
+ def test_runtime_config_allows_threshold_and_attention_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
23
+ monkeypatch.setenv("BRAINSTEM_INGEST_THRESHOLD", "7")
24
+ monkeypatch.setenv("BRAINSTEM_RECURRENCE_THRESHOLD", "9")
25
+ monkeypatch.setenv("BRAINSTEM_CANDIDATE_RECURRENCE_NORMALIZER", "5")
26
+ monkeypatch.setenv("BRAINSTEM_CANDIDATE_RECOVERY", "0.11")
27
+ cfg = get_runtime_config()
28
+ assert cfg.defaults.ingest_threshold == 7
29
+ assert cfg.defaults.recurrence_threshold == 9
30
+ assert cfg.candidate_attention.recurrence_count_normalizer == 5
31
+ assert cfg.candidate_attention.recovery_signal_weight == 0.11
32
+
33
+
19
34
  def test_resolve_default_db_path_uses_env_override(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
20
35
  custom_db = tmp_path / "override.sqlite3"
21
36
  monkeypatch.setenv("BRAINSTEM_DB_PATH", str(custom_db))
@@ -1,4 +1,6 @@
1
- from brainstem.fingerprint import normalize_message, fingerprint_event
1
+ from brainstem.connectors.logicmonitor import map_logicmonitor_payload_to_raw_envelope
2
+ from brainstem.fingerprint import fingerprint_event, normalize_message
3
+ from brainstem.ingest import canonicalize_raw_input_envelope, parse_file_line, parse_syslog_line
2
4
  from brainstem.models import Event
3
5
 
4
6
 
@@ -20,3 +22,51 @@ def test_fingerprint_event_builds_signature() -> None:
20
22
  sig = fingerprint_event(event)
21
23
  assert sig.signature_key.startswith("failure|sshd|") or sig.signature_key.startswith("auth|sshd|")
22
24
  assert sig.normalized_pattern
25
+
26
+
27
+ def test_event_family_for_connectivity_syslog_event() -> None:
28
+ event = parse_syslog_line(
29
+ "Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered",
30
+ tenant_id="client-a",
31
+ source_path="/var/log/syslog",
32
+ )
33
+ signature = fingerprint_event(event)
34
+ assert signature.event_family == "connectivity"
35
+
36
+
37
+ def test_event_family_for_resource_file_event() -> None:
38
+ event = parse_file_line(
39
+ "disk pressure warning on /var reached high watermark",
40
+ tenant_id="client-a",
41
+ source_path="/tmp/sample.log",
42
+ )
43
+ signature = fingerprint_event(event)
44
+ assert signature.event_family == "resource"
45
+
46
+
47
+ def test_event_family_for_logicmonitor_connectivity_event() -> None:
48
+ raw = map_logicmonitor_payload_to_raw_envelope(
49
+ {
50
+ "resource_id": 123,
51
+ "resource_name": "edge-fw-01",
52
+ "message_raw": "IPsec tunnel dropped and recovered",
53
+ "severity": "warning",
54
+ "timestamp": "2026-03-22T00:00:00Z",
55
+ "metadata": {"datasource": "IPSec Tunnel"},
56
+ },
57
+ tenant_id="client-a",
58
+ source_path="/logicmonitor/ingest",
59
+ )
60
+ event = canonicalize_raw_input_envelope(raw)
61
+ signature = fingerprint_event(event)
62
+ assert signature.event_family == "connectivity"
63
+
64
+
65
+ def test_event_family_falls_back_to_generic_when_no_clear_signal() -> None:
66
+ event = parse_file_line(
67
+ "Routine status check completed without issue",
68
+ tenant_id="client-a",
69
+ source_path="/tmp/sample.log",
70
+ )
71
+ signature = fingerprint_event(event)
72
+ assert signature.event_family == "generic"
@@ -1,4 +1,5 @@
1
- from brainstem.connectors.logicmonitor import map_logicmonitor_event
1
+ from brainstem.connectors.logicmonitor import map_logicmonitor_event, map_logicmonitor_payload_to_raw_envelope
2
+ from brainstem.ingest import canonicalize_raw_input_envelope
2
3
 
3
4
 
4
5
  def test_map_logicmonitor_event_normalizes_payload() -> None:
@@ -20,3 +21,55 @@ def test_map_logicmonitor_event_normalizes_payload() -> None:
20
21
  assert event.source_type == 'logicmonitor'
21
22
  assert event.host == 'edge-fw-01'
22
23
  assert event.metadata['resource_id'] == 123
24
+
25
+
26
+ def test_map_logicmonitor_payload_to_raw_envelope_preserves_shape() -> None:
27
+ raw = map_logicmonitor_payload_to_raw_envelope(
28
+ {
29
+ 'resource_id': 123,
30
+ 'resource_name': 'edge-fw-01',
31
+ 'severity': 'warning',
32
+ 'timestamp': '2026-03-22T00:00:00Z',
33
+ 'message_raw': 'VPN tunnel dropped and recovered',
34
+ 'metadata': {
35
+ 'datasource': 'IPSec Tunnel',
36
+ 'instance_name': 'site-b',
37
+ },
38
+ },
39
+ tenant_id='client-a',
40
+ source_path='/logicmonitor/ingest',
41
+ )
42
+
43
+ assert raw.source_type == 'logicmonitor'
44
+ assert raw.source_path == '/logicmonitor/ingest'
45
+ assert raw.host == 'edge-fw-01'
46
+ assert raw.service == 'IPSec Tunnel'
47
+ assert raw.severity == 'warning'
48
+ assert raw.message_raw == 'VPN tunnel dropped and recovered'
49
+ assert raw.metadata['alert_id'] is None
50
+ assert raw.timestamp == '2026-03-22T00:00:00Z'
51
+
52
+
53
+ def test_canonicalization_from_logicmonitor_raw_envelope_remains_in_ingest_flow() -> None:
54
+ raw = map_logicmonitor_payload_to_raw_envelope(
55
+ {
56
+ 'resource_id': 123,
57
+ 'resource_name': 'edge-fw-01',
58
+ 'severity': 'warning',
59
+ 'timestamp': '2026-03-22T00:00:00Z',
60
+ 'message_raw': 'VPN tunnel dropped and recovered',
61
+ 'metadata': {
62
+ 'datasource': 'IPSec Tunnel',
63
+ },
64
+ },
65
+ tenant_id='client-a',
66
+ source_path='/logicmonitor/ingest',
67
+ )
68
+
69
+ event = canonicalize_raw_input_envelope(raw)
70
+ assert event.source_type == 'logicmonitor'
71
+ assert event.host == 'edge-fw-01'
72
+ assert event.service == 'IPSec Tunnel'
73
+ assert event.ingest_metadata['canonicalization_source'] == 'logicmonitor'
74
+ assert event.ingest_metadata['raw_input_seen'] is True
75
+ assert event.message_normalized == 'vpn tunnel dropped and recovered'
@@ -1,3 +1,5 @@
1
+ import pytest
2
+
1
3
  from brainstem.ingest import ingest_syslog_lines, signatures_for_events
2
4
  from brainstem.recurrence import build_recurrence_candidates, digest_items
3
5
 
@@ -16,3 +18,15 @@ def test_build_recurrence_candidates_emits_candidate_for_repeated_signature() ->
16
18
  assert digest[0]["attention_explanation"]["dominant_signals"]
17
19
  assert "summary" in digest[0]["attention_explanation"]
18
20
  assert digest[0]['decision_band'] in {'watch', 'review', 'urgent_human_review', 'promote_to_incident_memory'}
21
+
22
+
23
+ def test_build_recurrence_candidates_uses_default_threshold_from_config(monkeypatch: pytest.MonkeyPatch) -> None:
24
+ lines = [
25
+ "Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered",
26
+ "Mar 22 00:00:03 fw-01 charon: VPN tunnel dropped and recovered",
27
+ ]
28
+ monkeypatch.setenv("BRAINSTEM_RECURRENCE_THRESHOLD", "3")
29
+ events = ingest_syslog_lines(lines, tenant_id="client-a", source_path="/var/log/syslog")
30
+ signatures = signatures_for_events(events)
31
+ candidates = build_recurrence_candidates(events, signatures)
32
+ assert candidates == []
@@ -6,7 +6,10 @@ from brainstem.models import RawInputEnvelope
6
6
  from brainstem.recurrence import build_recurrence_candidates
7
7
  from brainstem.storage import (
8
8
  extract_source_raw_envelope_ids,
9
+ create_sqlite_snapshot,
10
+ clear_storage_tables,
9
11
  get_raw_envelope_by_id,
12
+ get_storage_counts,
10
13
  get_ingest_stats,
11
14
  init_db,
12
15
  list_candidates,
@@ -18,6 +21,7 @@ from brainstem.storage import (
18
21
  list_recent_raw_envelopes,
19
22
  store_raw_envelopes,
20
23
  set_raw_envelope_status,
24
+ resolve_maintenance_tables,
21
25
  store_signatures,
22
26
  )
23
27
 
@@ -392,3 +396,76 @@ def test_get_raw_envelope_by_id(tmp_path: Path) -> None:
392
396
  assert row["id"] == raw_id
393
397
  assert row["canonicalization_status"] == "parse_failed"
394
398
  assert row["failure_reason"] == "empty message"
399
+
400
+
401
+ def test_storage_counts_report_expected_rows_for_ingested_payload(tmp_path: Path) -> None:
402
+ db_path = tmp_path / 'brainstem.sqlite3'
403
+ raw_events = parse_syslog_envelopes(
404
+ [
405
+ "Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered",
406
+ "Mar 22 00:00:02 fw-01 charon: VPN tunnel dropped and recovered",
407
+ ],
408
+ tenant_id="client-a",
409
+ source_path="/var/log/syslog",
410
+ )
411
+ result = run_ingest_pipeline(raw_events, threshold=2, db_path=str(db_path))
412
+ assert result.raw_envelope_ids
413
+ assert result.events
414
+ assert result.signatures
415
+ assert result.candidates
416
+
417
+ counts = get_storage_counts(str(db_path))
418
+ expected_signature_rows = len({sig.signature_key for sig in result.signatures})
419
+ assert counts["raw_envelopes"] == len(result.raw_envelope_ids)
420
+ assert counts["events"] == len(result.events)
421
+ assert counts["signatures"] == expected_signature_rows
422
+ assert counts["candidates"] == len(result.candidates)
423
+
424
+
425
+ def test_resolve_and_clear_storage_tables_are_explicit() -> None:
426
+ assert resolve_maintenance_tables(["all"]) == ["events", "raw_envelopes", "signatures", "candidates"]
427
+ assert resolve_maintenance_tables(["raw_envelopes", "events"]) == ["raw_envelopes", "events"]
428
+ assert resolve_maintenance_tables(None) == ["events", "raw_envelopes", "signatures", "candidates"]
429
+
430
+
431
+ def test_clear_storage_tables_only_clears_requested_tables(tmp_path: Path) -> None:
432
+ db_path = tmp_path / 'brainstem.sqlite3'
433
+ raw_events = parse_syslog_envelopes(
434
+ [
435
+ "Mar 22 00:00:01 fw-01 charon: VPN tunnel dropped and recovered",
436
+ "Mar 22 00:00:02 fw-01 charon: VPN tunnel dropped and recovered",
437
+ ],
438
+ tenant_id="client-a",
439
+ source_path="/var/log/syslog",
440
+ )
441
+ result = run_ingest_pipeline(raw_events, threshold=2, db_path=str(db_path))
442
+ removed = clear_storage_tables(str(db_path), tables=["raw_envelopes", "events"])
443
+ expected_signature_rows = len({sig.signature_key for sig in result.signatures})
444
+ assert removed["raw_envelopes"] == len(result.raw_envelope_ids)
445
+ assert removed["events"] == len(result.events)
446
+
447
+ counts_after = get_storage_counts(str(db_path))
448
+ assert counts_after["raw_envelopes"] == 0
449
+ assert counts_after["events"] == 0
450
+ assert counts_after["signatures"] == expected_signature_rows
451
+ assert counts_after["candidates"] == len(result.candidates)
452
+
453
+
454
+ def test_create_sqlite_snapshot_returns_metadata_and_copies_file(tmp_path: Path) -> None:
455
+ db_path = tmp_path / "snapshot.sqlite3"
456
+ init_db(str(db_path))
457
+
458
+ first = create_sqlite_snapshot(str(db_path))
459
+ first_path = Path(first["snapshot_path"])
460
+ assert first["source_path"] == str(db_path)
461
+ assert first_path.exists()
462
+ assert first_path.is_file()
463
+ assert first_path != db_path
464
+ assert first["size"] == first_path.stat().st_size
465
+ assert first["size"] > 0
466
+ assert first["created_at"].endswith("Z")
467
+
468
+ second = create_sqlite_snapshot(str(db_path))
469
+ second_path = Path(second["snapshot_path"])
470
+ assert second_path.exists()
471
+ assert second["snapshot_path"] != first["snapshot_path"]