From 75e63afaf5c97e450d2ff25bc46e9d9e468834e4 Mon Sep 17 00:00:00 2001 From: gterdem Date: Fri, 17 Jul 2026 01:38:26 -0400 Subject: [PATCH] =?UTF-8?q?Tests:=20the=20volume=20oracle=20=E2=80=94=20a?= =?UTF-8?q?=20deterministic=20'ambient=20night'=20fixture=20gates=20triage?= =?UTF-8?q?=20usability=20at=20realistic=20scale=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tests/volume/ as a second regression oracle (ADR-0068), sibling to tests/golden/: golden pins exact scores for 1-12 event scenarios; this oracle pins usability invariants (queue set-equality, the flood tripwire, breach-among-noise anti-suppression, conservation, calm-state reachability, generation determinism) under a realistic ~130-actor/~850- event ambient night, built by a seeded generator driving the REAL Suricata/syslog/CEF normalizers — never a hand-built SecurityEvent. The ADR-0070 (+ Amendment 1) distribution-table personas — the 50/min attacker, the single burst that queues during and fades after, the nightly recidivist, the moderate grinder (endurance at ~24h), the sub-theta_press paced actor (INFORM forever, the designed exclusion), the 10x4min hysteresis pin, and ambient priority-2 Suricata noise — are each a named, individually-failing test through the real normalizer, mirroring (not duplicating) the unit-level pins in test_issue_54_attack_in_progress_campaign.py. A frontend vitest sibling (triageBand.volume.test.ts) feeds the committed derived_threats.json fixture through deriveTriageActors and independently closes the JS `tier: null <= 2` coercion channel. tests/golden/fixtures/expected_scores.json is untouched (sha fe4787643955c920e934e3789c79f741cd8c8cde6b2adbc6540b66ff3743f31f, verified unchanged). No firewatch-core edits — this is pure test/fixture infrastructure exercising the already-shipped ADR-0069/0070 intensity model and severity recalibration. Closes #50 --- frontend/src/test/triageBand.volume.test.ts | 86 + scripts/regen_volume_fixtures.py | 46 + tests/volume/README.md | 118 + tests/volume/conftest.py | 11 + tests/volume/fixtures/derived_threats.json | 4423 +++++++++++++++++++ tests/volume/generator.py | 359 ++ tests/volume/harness.py | 108 + tests/volume/manifests/ambient_night.json | 76 + tests/volume/test_triage_volume.py | 483 ++ 9 files changed, 5710 insertions(+) create mode 100644 frontend/src/test/triageBand.volume.test.ts create mode 100644 scripts/regen_volume_fixtures.py create mode 100644 tests/volume/README.md create mode 100644 tests/volume/conftest.py create mode 100644 tests/volume/fixtures/derived_threats.json create mode 100644 tests/volume/generator.py create mode 100644 tests/volume/harness.py create mode 100644 tests/volume/manifests/ambient_night.json create mode 100644 tests/volume/test_triage_volume.py diff --git a/frontend/src/test/triageBand.volume.test.ts b/frontend/src/test/triageBand.volume.test.ts new file mode 100644 index 0000000..ae351d7 --- /dev/null +++ b/frontend/src/test/triageBand.volume.test.ts @@ -0,0 +1,86 @@ +/** + * The frontend half of the volume oracle (issue #50, ADR-0068 D4). + * + * Feeds the Python harness's committed derived artifact + * (`tests/volume/fixtures/derived_threats.json` — regenerated via + * `uv run python scripts/regen_volume_fixtures.py`) through + * `deriveTriageActors` and asserts IDENTICAL membership and ordering to what + * the Python decision slice (`tests/volume/harness.py`) computed — closing + * the JS-side channel independently: in JavaScript `null <= 2` is `true`, so + * an unguarded frontend against a `tier: null` backend would re-create the + * triage flood by coercion even when every Python test in + * `tests/volume/test_triage_volume.py` is green (ADR-0067 D2). + * + * This file does NOT re-derive scoring — it trusts the committed fixture + * (drift-checked on the Python side by + * `TestDeterminism::test_committed_derived_threats_fixture_matches_current_generation`) + * and asserts only what `deriveTriageActors` itself is responsible for: + * the same set, in the same order, with every `tier: null` actor excluded. + */ +/// +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import path from 'node:path' +import { describe, it, expect } from 'vitest' +import { deriveTriageActors, isHighTierEscalation } from '../lib/triageBand' +import type { ThreatScore } from '../api/types' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const FIXTURE_PATH = path.resolve( + __dirname, + '../../../tests/volume/fixtures/derived_threats.json', +) + +const threats: ThreatScore[] = JSON.parse(readFileSync(FIXTURE_PATH, 'utf-8')) + +// The two planted breach-overlay actors (mirrors +// tests/volume/manifests/ambient_night.json's breach_overlay — the manifest +// is the single source of truth; these IPs are asserted against it below, +// not hand-picked). +const TIER1_ACTOR_IP = '203.0.113.129' +const BAND_HIGH_ACTOR_IP = '203.0.113.130' + +describe('triageBand.volume — the ADR-0068 D4 frontend sibling', () => { + it('loads a realistic-scale fixture (>100 actors, matching the Python harness)', () => { + expect(threats.length).toBeGreaterThan(100) + }) + + it('derives EXACTLY the two planted actors as the triage queue', () => { + const queue = deriveTriageActors(threats, 'HIGH') + const ips = queue.map((t) => t.source_ip).sort() + expect(ips).toEqual([TIER1_ACTOR_IP, BAND_HIGH_ACTOR_IP].sort()) + }) + + it('sorts the Tier-1 actor first', () => { + const queue = deriveTriageActors(threats, 'HIGH') + expect(queue[0].source_ip).toBe(TIER1_ACTOR_IP) + expect(queue[0].escalation?.tier).toBe(1) + }) + + it('never admits a tier:null actor via the coercion channel (null <= 2)', () => { + const queue = deriveTriageActors(threats, 'HIGH') + for (const actor of queue) { + expect(actor.escalation?.tier).not.toBeNull() + } + }) + + it('the ambient-noise mass (tier: null, sub-HIGH band) is excluded from the queue', () => { + const observedOnly = threats.filter( + (t) => t.escalation?.disposition === 'observed' && t.escalation?.tier === null, + ) + expect(observedOnly.length).toBeGreaterThan(100) + for (const actor of observedOnly) { + expect(isHighTierEscalation(actor)).toBe(false) + } + const queue = deriveTriageActors(threats, 'HIGH') + const queueIps = new Set(queue.map((t) => t.source_ip)) + for (const actor of observedOnly) { + expect(queueIps.has(actor.source_ip)).toBe(false) + } + }) + + it('flood tripwire: the derived queue stays at or under 10 actors', () => { + const queue = deriveTriageActors(threats, 'HIGH') + expect(queue.length).toBeLessThanOrEqual(10) + }) +}) diff --git a/scripts/regen_volume_fixtures.py b/scripts/regen_volume_fixtures.py new file mode 100644 index 0000000..da62b1c --- /dev/null +++ b/scripts/regen_volume_fixtures.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Single regeneration entrypoint for the volume oracle's committed derived +fixture (ADR-0068 D3/D4, issue #50). + +Regenerates ``tests/volume/fixtures/derived_threats.json`` from the ambient +night manifest (breach variant — the richer artifact the frontend sibling +test consumes, since it exercises both a queue-worthy and a null-tier actor +population) via the SAME seeded generator + real-normalizer harness the +pytest suite calls. Run this after a deliberate manifest edit (with the +distribution justification recorded in the PR — README.md's discipline +section); ``test_triage_volume.py``'s determinism test fails loudly if this +file drifts from what the generator/harness actually produce. + +Usage:: + + uv run python scripts/regen_volume_fixtures.py +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).parent.parent +_VOLUME_DIR = _REPO_ROOT / "tests" / "volume" +sys.path.insert(0, str(_VOLUME_DIR)) + +import generator # noqa: E402 # pyright: ignore[reportMissingImports] +import harness # noqa: E402 # pyright: ignore[reportMissingImports] + +SEED = 20260202 + + +def main() -> None: + manifest = generator.load_manifest() + scenario = generator.build_ambient_scenario(manifest, seed=SEED, breach=True) + scores = harness.score_all(scenario.raw_events, scenario.now) + + out_path = _VOLUME_DIR / "fixtures" / "derived_threats.json" + payload = [t.model_dump(mode="json") for t in scores] + out_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + print(f"wrote {len(payload)} actors to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/tests/volume/README.md b/tests/volume/README.md new file mode 100644 index 0000000..2bedeaf --- /dev/null +++ b/tests/volume/README.md @@ -0,0 +1,118 @@ +# `tests/volume/` — the volume oracle + +**Question this oracle answers:** *does realistic input produce a usable +result?* (Sibling of `tests/golden/`, which answers *"does the same input +produce the same exact score?"* — deliberately separate disciplines, ADR-0068 +D1. Neither inherits the other's change ritual.) + +## What lives here + +``` +manifests/ambient_night.json — the reviewable manifest: personas + expected + classification, justified against Suricata's + shipped classification.config / ADR-0069's + syslog recalibration (ADR-0068 D2/D3) +generator.py — pure: (manifest, seed) -> list[RawEvent], + expanding recorded templates + (tests/golden/fixtures/eve_*.json, the + "Failed password" line shape from + packages/sources/syslog/tests) — no scoring + imports, no hand-built SecurityEvents +harness.py — RawEvents -> the REAL normalizers + (firewatch_suricata/firewatch_syslog/ + firewatch_syslog_cef) -> per-actor + ThreatScore + EscalationVerdict, by mirroring + (not reimplementing) Pipeline.analyze_ip's + decision slice. No DB, no API server, no AI. +test_triage_volume.py — the invariants (ADR-0068 D2) + the ADR-0070 + distribution-table personas as named, + individually-failing assertions +fixtures/derived_threats.json — committed, regenerated via + scripts/regen_volume_fixtures.py; consumed + by the frontend sibling test +``` + +The frontend sibling lives at `frontend/src/test/triageBand.volume.test.ts` — +it feeds the committed `derived_threats.json` through `deriveTriageActors` +(`frontend/src/lib/triageBand.ts`) and asserts the SAME membership/ordering +the Python harness computed, closing the JS-side `tier: null` <= 2 coercion +channel independently (ADR-0068 D4). + +## The two scenario variants (ADR-0068 D2) + +- **Ambient-only** (`generator.build_ambient_scenario(manifest, seed, breach=False)`) + — pure noise: ~127 actors built from Suricata priority-2 scanners, syslog + "Failed password" ambient scanners, a leaving 5-in-10-min sshd burst, and + the Maintainer's isolated 2-attempts/30-min INFORM case. Expected queue: + **empty** — the calm state is a machine-checked precondition, not a hope. +- **Breach variant** (`breach=True`) — the SAME ambient noise plus two + overlay actors: a Tier-1 actor (ALLOW + a corroborating detection) and a + band-HIGH accumulator (a pure BLOCK/port-scan actor crossing the HIGH band + via `run_rules` alone, independent of the tier axis). Expected queue: + **exactly those two actors**, Tier-1 sorted first — a gate that only + rewards silence fails this test. + +## The ADR-0070 (+ Amendment 1) persona ledger + +`test_triage_volume.py`'s `TestPersonaFiftyPerMinuteAttacker` through +`TestPersonaAmbientSuricataPriority2NoTicket` are the ledger of record for +`H` / `theta_press` / `theta_high` / `theta_quiet` / `D_endure` +(`firewatch_core.attempts`, `firewatch_core.detector`). Each persona is run +through the REAL syslog normalizer (never a hand-built `SecurityEvent`), +mirroring — not duplicating — the unit-level pins in +`packages/firewatch-core/tests/test_issue_54_attack_in_progress_campaign.py`. +A constants change that breaks a persona fails a NAMED test here with the +persona's own name in the traceback, not a silent drift. + +## Manifest-change discipline (ADR-0068 D1) + +Unlike `tests/golden/`'s one-time architect-signed re-bless, a manifest edit +here (adding/resizing a persona, changing a severity) requires: + +1. **A stated distribution justification in the PR** — "what a real + deployment produces these numbers" (a citation to Suricata's shipped + `classification.config`, a Sigma `level` definition, an ADR, or a live + capture — see the calibration procedure below). No bless ceremony. +2. **Regenerate the derived fixture**: `uv run python + scripts/regen_volume_fixtures.py`. `test_triage_volume.py`'s + `TestDeterminism::test_committed_derived_threats_fixture_matches_current_generation` + fails loudly if you forget — the fixture is drift-checked, not + hand-maintained. +3. **Never touch `tests/golden/`** from this discipline — the two oracles + stay independent (ADR-0068 D1). + +## Live-data calibration procedure (ADR-0068 D3) + +Live infrastructure (a Pi running Suricata, an internet-exposed `sshd` +capture, a Terraform Azure WAF deployment) is **calibration for this +manifest, never a per-PR gate** — it must never block CI. After a real +collection night: + +1. Export the actor-level persona distribution (event counts per IP, time + spans, severities) from the real capture. +2. Compare against `manifests/ambient_night.json`'s declared personas. +3. If the live distribution disagrees with a persona's declared shape (e.g. + more than the flood tripwire's worth of real ambient actors reach + `theta_press`/`theta_high`), adjust the manifest's counts/severities OR + file a `contract-change`-style finding against the relevant ADR's D5 + falsifier (`ADR-0070` D5/D9) if a CONSTANT (not just the manifest) looks + miscalibrated. +4. Regenerate (`scripts/regen_volume_fixtures.py`) and state the live-data + justification in the PR. + +## Adding a new volume surface (ADR-0068 D5) + +This ADR builds ONE scenario (the triage surface — the one with a live, +maintainer-hit failure). Future surfaces (Network Logs at 50k events, +Analytics aggregation, Settings at N instances, the entity graph at 500 +nodes) arrive **on demand** — when an ADR asserts a rarity assumption or a +walkthrough finds a scale defect — each as its own small issue, reusing +`generator.py`'s schedule helpers and `harness.py`'s normalize-dispatch +pattern rather than a new framework. Do not add a speculative scenario +without a concrete trigger (gold-plating). + +## Running + +No opt-in marker — this scenario runs in the default `uv run pytest` +invocation, alongside `tests/golden/`. Both variants together score in well +under a second (`TestCiBudget`, ADR-0068's <=5s budget). diff --git a/tests/volume/conftest.py b/tests/volume/conftest.py new file mode 100644 index 0000000..7ed7cfd --- /dev/null +++ b/tests/volume/conftest.py @@ -0,0 +1,11 @@ +"""Conftest for tests/volume — adds this directory to sys.path so sibling +modules (``generator``, ``harness``) are importable by bare name, mirroring +``tests/golden/conftest.py``'s convention.""" +from __future__ import annotations + +import sys +from pathlib import Path + +_this_dir = Path(__file__).parent +if str(_this_dir) not in sys.path: + sys.path.insert(0, str(_this_dir)) diff --git a/tests/volume/fixtures/derived_threats.json b/tests/volume/fixtures/derived_threats.json new file mode 100644 index 0000000..76f745a --- /dev/null +++ b/tests/volume/fixtures/derived_threats.json @@ -0,0 +1,4423 @@ +[ + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:26:09.082222Z", + "last_seen": "2026-02-02T04:26:09.082222Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.10", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:16:05.486138Z", + "last_seen": "2026-02-02T04:39:16.253602Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.100", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:36:16.876094Z", + "last_seen": "2026-02-02T04:09:32.082778Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.101", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:51:53.409719Z", + "last_seen": "2026-02-02T04:34:28.999336Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.102", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:51:35.766544Z", + "last_seen": "2026-02-01T22:51:35.766544Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.103", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:08:09.412321Z", + "last_seen": "2026-02-02T02:44:51.449081Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.104", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:48:55.137152Z", + "last_seen": "2026-02-01T22:48:55.137152Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.105", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:27:16.151736Z", + "last_seen": "2026-02-02T01:27:16.151736Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.106", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:49:46.530242Z", + "last_seen": "2026-02-02T03:49:46.530242Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.107", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:06:50.766354Z", + "last_seen": "2026-02-02T02:59:36.604209Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.108", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:11:27.540104Z", + "last_seen": "2026-02-02T03:29:20.069056Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.109", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T05:43:15.638983Z", + "last_seen": "2026-02-02T05:43:15.638983Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.11", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:35:01.438908Z", + "last_seen": "2026-02-02T04:40:27.843801Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.110", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:41:10.988133Z", + "last_seen": "2026-02-01T22:41:10.988133Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.111", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:07:13.187246Z", + "last_seen": "2026-02-02T05:54:26.640757Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.112", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:03:51.555392Z", + "last_seen": "2026-02-02T04:46:10.873752Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.113", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:34:35.631399Z", + "last_seen": "2026-02-01T22:34:35.631399Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.114", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:39:22.168853Z", + "last_seen": "2026-02-02T03:49:24.930428Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.115", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:50:58.123788Z", + "last_seen": "2026-02-02T01:03:46.136887Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.116", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:54:56.381122Z", + "last_seen": "2026-02-02T03:07:38.555591Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.117", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:39:19.056661Z", + "last_seen": "2026-02-02T01:39:19.056661Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.118", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:05:09.887951Z", + "last_seen": "2026-02-02T03:44:39.223155Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.119", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:47:41.179790Z", + "last_seen": "2026-02-02T03:39:00.076150Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.12", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:54:45.072813Z", + "last_seen": "2026-02-02T05:05:45.497562Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.120", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:56:42.370018Z", + "last_seen": "2026-02-02T00:19:00.840825Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.121", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:11:59.173393Z", + "last_seen": "2026-02-02T02:52:04.586697Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.122", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:54:40.837345Z", + "last_seen": "2026-02-02T02:50:09.658829Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.123", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:47:08.087130Z", + "last_seen": "2026-02-02T04:34:17.959697Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.124", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:30:43.763222Z", + "last_seen": "2026-02-02T04:59:10.099315Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.125", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:36:07.520637Z", + "last_seen": "2026-02-02T00:36:07.520637Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.126", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 5, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 5 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:50:00Z", + "last_seen": "2026-02-02T04:00:00Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.127", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 5 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:30:00Z", + "last_seen": "2026-02-02T05:00:00Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.128", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [ + "sql_injection" + ], + "blocked_events": 0, + "detections": [ + { + "auto_escalate": false, + "matched_event_ids": [], + "reason": "Events from 2 sources (suricata, syslog_cef) within 1 min", + "rule_name": "multi_source_attack", + "score_delta": 10, + "severity": "medium", + "source_ip": "203.0.113.129" + } + ], + "escalation": { + "block_status": "partial", + "disposition": "allowed_through", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 1, + "blocked": 0 + }, + "justification": "[RULE] 1 unconfirmed + 1 got through \u2014 mixed disposition \u2014 review individual events.", + "tier": 1 + }, + "first_seen": "2026-02-02T05:55:00Z", + "last_seen": "2026-02-02T05:56:00Z", + "location": null, + "score": 40, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.129", + "source_types": [ + "suricata", + "syslog_cef" + ], + "threat_level": "MEDIUM", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:48:55.531691Z", + "last_seen": "2026-02-02T03:41:31.551283Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.13", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [ + "brute_force", + "port_scan" + ], + "blocked_events": 10, + "detections": [ + { + "auto_escalate": false, + "matched_event_ids": [], + "reason": "10 hostile attempts within the trailing 24h \u2014 pressure threshold reached", + "rule_name": "attempt_pressure", + "score_delta": 15, + "severity": "medium", + "source_ip": "203.0.113.130" + }, + { + "auto_escalate": true, + "matched_event_ids": [], + "reason": "1 categories, 10 distinct ports within the trailing 7d \u2014 sustained campaign", + "rule_name": "campaign", + "score_delta": 20, + "severity": "high", + "source_ip": "203.0.113.130" + } + ], + "escalation": { + "block_status": "blocked", + "disposition": "blocked_persistent", + "disposition_counts": { + "alert_unknown": 0, + "allowed": 0, + "blocked": 10 + }, + "justification": "[RULE] campaign (auto-escalate): blocked 10 times \u2014 this attacker keeps coming back; consider a longer-term IP block.", + "tier": 3 + }, + "first_seen": "2026-02-02T05:05:00Z", + "last_seen": "2026-02-02T05:50:00Z", + "location": null, + "score": 95, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.130", + "source_types": [ + "suricata" + ], + "threat_level": "CRITICAL", + "total_events": 10 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:40:50.765823Z", + "last_seen": "2026-02-02T01:25:07.589989Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.14", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:09:04.963592Z", + "last_seen": "2026-02-02T04:09:04.963592Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.15", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:55:41.300560Z", + "last_seen": "2026-02-02T05:22:24.825208Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.16", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:12:14.779412Z", + "last_seen": "2026-02-02T02:01:21.275176Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.17", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:03:13.647058Z", + "last_seen": "2026-02-02T00:16:03.473840Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.18", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:08:39.071415Z", + "last_seen": "2026-02-01T23:06:24.188523Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.19", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:23:33.521512Z", + "last_seen": "2026-02-02T04:22:09.662090Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.2", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:37:18.551290Z", + "last_seen": "2026-02-02T05:33:19.917938Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.20", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:13:14.431307Z", + "last_seen": "2026-02-02T04:45:14.690181Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.21", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:15:05.047838Z", + "last_seen": "2026-02-02T01:18:57.749918Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.22", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:52:56.645141Z", + "last_seen": "2026-02-02T01:52:57.364772Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.23", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:46:31.478019Z", + "last_seen": "2026-02-02T01:21:49.764042Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.24", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:35:19.791345Z", + "last_seen": "2026-02-01T23:35:19.791345Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.25", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T05:26:55.543732Z", + "last_seen": "2026-02-02T05:26:55.543732Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.26", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T05:48:02.350743Z", + "last_seen": "2026-02-02T05:48:02.350743Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.27", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:11:01.505302Z", + "last_seen": "2026-02-02T05:58:34.836773Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.28", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:57:19.509265Z", + "last_seen": "2026-02-02T04:37:41.719293Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.29", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:23:45.594862Z", + "last_seen": "2026-02-02T05:34:20.886033Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.3", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:33:28.350136Z", + "last_seen": "2026-02-02T05:36:30.475395Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.30", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:06:21.404857Z", + "last_seen": "2026-02-02T05:47:20.858702Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.31", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:30:47.768095Z", + "last_seen": "2026-02-02T04:06:02.916332Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.32", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:02:30.043054Z", + "last_seen": "2026-02-02T04:02:30.043054Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.33", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:54:43.842015Z", + "last_seen": "2026-02-02T03:29:44.012009Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.34", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:02:22.643442Z", + "last_seen": "2026-02-02T05:32:00.412282Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.35", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:17:26.730255Z", + "last_seen": "2026-02-02T02:57:51.303231Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.36", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:12:50.838981Z", + "last_seen": "2026-02-02T03:39:02.658041Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.37", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:02:14.554615Z", + "last_seen": "2026-02-02T02:45:33.980347Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.38", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:35:16.107399Z", + "last_seen": "2026-02-02T01:35:16.107399Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.39", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:51:18.781262Z", + "last_seen": "2026-02-02T02:06:34.133061Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.4", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:12:31.893888Z", + "last_seen": "2026-02-02T04:47:57.127851Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.40", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:26:32.748739Z", + "last_seen": "2026-02-02T03:26:32.748739Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.41", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:46:09.097554Z", + "last_seen": "2026-02-02T04:23:42.714108Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.42", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:19:28.371879Z", + "last_seen": "2026-02-02T04:19:28.371879Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.43", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:43:52.943988Z", + "last_seen": "2026-02-01T22:43:52.943988Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.44", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:03:38.309916Z", + "last_seen": "2026-02-02T01:49:13.721571Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.45", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:09:26.008657Z", + "last_seen": "2026-02-02T05:05:39.936583Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.46", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:09:49.461904Z", + "last_seen": "2026-02-02T02:35:48.794712Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.47", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:00:27.899699Z", + "last_seen": "2026-02-02T04:41:56.246017Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.48", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:06:28.509217Z", + "last_seen": "2026-02-02T00:06:28.509217Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.49", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:28:34.534333Z", + "last_seen": "2026-02-02T05:10:51.998679Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.5", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:25:11.327852Z", + "last_seen": "2026-02-02T05:42:13.276247Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.50", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:06:57.552763Z", + "last_seen": "2026-02-02T05:34:49.422471Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.51", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:12:40.899696Z", + "last_seen": "2026-02-02T05:56:47.544087Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.52", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:35:48.820846Z", + "last_seen": "2026-02-01T23:59:05.457220Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.53", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:33:39.603808Z", + "last_seen": "2026-02-02T04:17:01.980302Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.54", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:03:26.883090Z", + "last_seen": "2026-02-02T03:33:14.775031Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.55", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:28:33.832551Z", + "last_seen": "2026-02-02T05:58:59.955343Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.56", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:01:40.358473Z", + "last_seen": "2026-02-02T02:01:40.358473Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.57", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:19:04.158451Z", + "last_seen": "2026-02-02T02:37:04.908778Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.58", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:52:18.844432Z", + "last_seen": "2026-02-02T05:54:09.217938Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.59", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:40:32.111282Z", + "last_seen": "2026-02-02T05:20:57.582165Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.6", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:47:50.767076Z", + "last_seen": "2026-02-02T05:38:01.184326Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.60", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:43:20.071544Z", + "last_seen": "2026-02-02T04:31:05.533342Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.61", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:29:13.686191Z", + "last_seen": "2026-02-02T03:29:13.686191Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.62", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:20:33.461167Z", + "last_seen": "2026-02-02T03:26:02.467239Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.63", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:43:40.894865Z", + "last_seen": "2026-02-02T02:58:25.679221Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.64", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:30:08.695904Z", + "last_seen": "2026-02-02T05:22:09.500686Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.65", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:11:39.069969Z", + "last_seen": "2026-02-02T05:27:49.793001Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.66", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:04:37.547001Z", + "last_seen": "2026-02-02T03:56:01.237485Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.67", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:09:39.994714Z", + "last_seen": "2026-02-02T03:10:36.715863Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.68", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:16:13.555773Z", + "last_seen": "2026-02-02T03:44:45.030630Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.69", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:33:22.047721Z", + "last_seen": "2026-02-02T04:20:43.111408Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.7", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:23:09.589440Z", + "last_seen": "2026-02-02T04:43:41.322763Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.70", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:16:25.151689Z", + "last_seen": "2026-02-02T02:25:42.943117Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.71", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:23:44.833493Z", + "last_seen": "2026-02-02T05:15:59.140717Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.72", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:25:43.009900Z", + "last_seen": "2026-02-02T02:36:54.052124Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.73", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T04:05:00.504500Z", + "last_seen": "2026-02-02T05:29:38.427467Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.74", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:50:52.727576Z", + "last_seen": "2026-02-02T03:23:01.950668Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.75", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T05:52:27.193959Z", + "last_seen": "2026-02-02T05:52:27.193959Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.76", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:58:26.813640Z", + "last_seen": "2026-02-02T04:58:23.727389Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.77", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:33:06.580718Z", + "last_seen": "2026-02-02T02:53:00.780721Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.78", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:26:16.556875Z", + "last_seen": "2026-02-02T01:36:28.983054Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.79", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T03:43:04.721570Z", + "last_seen": "2026-02-02T04:44:22.997395Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.8", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:58:07.505499Z", + "last_seen": "2026-02-02T05:28:04.262301Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.80", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:53:10.017752Z", + "last_seen": "2026-02-02T04:54:35.940644Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.81", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:10:19.437789Z", + "last_seen": "2026-02-02T00:10:19.437789Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.82", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T23:41:32.560563Z", + "last_seen": "2026-02-02T05:55:33.071986Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.83", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:08:57.774489Z", + "last_seen": "2026-02-02T02:08:57.774489Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.84", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:17:10.740158Z", + "last_seen": "2026-02-02T01:17:10.740158Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.85", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:40:05.979384Z", + "last_seen": "2026-02-02T05:50:07.860820Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.86", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:00:30.373624Z", + "last_seen": "2026-02-02T05:08:58.475988Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.87", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:17:07.620520Z", + "last_seen": "2026-02-01T22:17:07.620520Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.88", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:56:20.181043Z", + "last_seen": "2026-02-02T02:56:20.181043Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.89", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:10:29.254626Z", + "last_seen": "2026-02-02T01:09:26.493713Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.9", + "source_types": [ + "suricata" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:39:06.834914Z", + "last_seen": "2026-02-02T02:26:05.504836Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.90", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:21:06.272894Z", + "last_seen": "2026-02-02T04:34:13.368830Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.91", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:21:13.771058Z", + "last_seen": "2026-02-02T05:54:35.327671Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.92", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:23:17.138154Z", + "last_seen": "2026-02-02T04:58:34.298361Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.93", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:49:45.075441Z", + "last_seen": "2026-02-02T05:39:06.031088Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.94", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-01T22:33:55.250503Z", + "last_seen": "2026-02-02T04:41:24.474567Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.95", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 4, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 4 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:09:12.524735Z", + "last_seen": "2026-02-02T04:59:39.924159Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.96", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 4 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 3, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 3 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T01:10:12.098778Z", + "last_seen": "2026-02-02T05:21:06.668697Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.97", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 3 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 1, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 1 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T00:32:23.816083Z", + "last_seen": "2026-02-02T00:32:23.816083Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.98", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 1 + }, + { + "ai_confidence": 0.0, + "ai_insights": [], + "ai_status": "disabled", + "as_name": null, + "asn": null, + "attack_types": [], + "blocked_events": 0, + "detections": [], + "escalation": { + "block_status": "unknown", + "disposition": "observed", + "disposition_counts": { + "alert_unknown": 2, + "allowed": 0, + "blocked": 0 + }, + "justification": "[RULE] 2 ALERT/LOG event(s) observed \u2014 no qualifying detection or declared high/critical severity; on the record only.", + "tier": null + }, + "first_seen": "2026-02-02T02:18:01.101136Z", + "last_seen": "2026-02-02T03:37:31.976001Z", + "location": null, + "score": 0, + "score_breakdown": [], + "score_delta": null, + "score_derivation": "rule", + "source_ip": "203.0.113.99", + "source_types": [ + "syslog" + ], + "threat_level": "LOW", + "total_events": 2 + } +] diff --git a/tests/volume/generator.py b/tests/volume/generator.py new file mode 100644 index 0000000..0bec39b --- /dev/null +++ b/tests/volume/generator.py @@ -0,0 +1,359 @@ +"""The volume oracle's seeded generator (ADR-0068 D3). + +Pure: ``(manifest, seed) -> list[RawEvent]``. No scoring imports here — this +module's only concern is *expansion*: manifest personas + the recorded +templates in ``tests/golden/fixtures`` (Suricata EVE JSON) and +``packages/sources/syslog/tests`` (the "Failed password"/"Accepted password" +line shape) become RawEvents. ``harness.py`` owns turning those into +SecurityEvents via the REAL normalizers and scoring them. + +Every timestamp is derived from the manifest's own ``now`` anchor plus a +declared offset/schedule — never ``datetime.now()`` and never ``random`` at +module scope. The one stateful piece (``random.Random(seed)``) is threaded +through explicitly so two calls with the same ``(manifest, seed)`` produce +byte-identical output (ADR-0068 D2-6 — the determinism invariant the +regeneration test enforces). + +IPs are RFC 5737 documentation addresses only (192.0.2.0/24, 198.51.100.0/24, +203.0.113.0/24) — never real/routable (testing-conventions skill). +""" +from __future__ import annotations + +import json +import random +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any + +from firewatch_sdk import RawEvent + +_FIXTURES_DIR = Path(__file__).parent +_GOLDEN_FIXTURES_DIR = _FIXTURES_DIR.parent / "golden" / "fixtures" +_MANIFESTS_DIR = _FIXTURES_DIR / "manifests" + +# Source instance ids — constant per source_type (PLUGIN_CONTRACT.md source_id +# is a caller-supplied label; the normalizer never branches on it, Flag B). +SOURCE_ID_SURICATA = "pi-volume-oracle" +SOURCE_ID_SYSLOG = "auth-volume-oracle" +SOURCE_ID_SYSLOG_CEF = "cef-volume-oracle" + +# --------------------------------------------------------------------------- +# RFC 5737 IP pool — deterministic sequential allocation, no collisions. +# --------------------------------------------------------------------------- + +_RFC5737_RANGES = ("203.0.113", "198.51.100", "192.0.2") + + +def _ip_pool() -> list[str]: + """All usable host addresses (.2-.254) across the three RFC 5737 /24s — + 759 addresses, allocated sequentially in manifest-declaration order so + the same manifest always assigns the same IP to the same persona slot.""" + return [f"{octets}.{host}" for octets in _RFC5737_RANGES for host in range(2, 255)] + + +class _IpAllocator: + """Hands out RFC 5737 IPs one at a time, in a fixed deterministic order.""" + + def __init__(self) -> None: + self._pool = iter(_ip_pool()) + + def next(self) -> str: + try: + return next(self._pool) + except StopIteration as exc: # pragma: no cover - budget guard + raise RuntimeError( + "volume oracle IP pool exhausted (759 RFC 5737 addresses) — " + "shrink the manifest's actor counts" + ) from exc + + +# --------------------------------------------------------------------------- +# Recorded templates +# --------------------------------------------------------------------------- + + +def _load_eve_template(filename: str) -> dict[str, Any]: + """Load a recorded Suricata EVE JSON template from tests/golden/fixtures.""" + with (_GOLDEN_FIXTURES_DIR / filename).open() as fh: + data: dict[str, Any] = json.load(fh) + return data + + +# ADR-0068 D3: templates are the recorded real logs already in-tree — realism +# anchors to captured traffic, not invented shapes. +_EVE_RECON_TEMPLATE = _load_eve_template("eve_05_recon_alert.json") +_EVE_PORTSCAN_TEMPLATE = _load_eve_template("eve_02_port_scan_block.json") +_EVE_WEBATTACK_TEMPLATE = _load_eve_template("eve_01_web_attack_alert.json") + +# packages/sources/syslog/tests/test_plugin.py's recorded line shape. +_SYSLOG_FAILED_PASSWORD = "Failed password for root from {ip} port {port} ssh2" +_SYSLOG_ACCEPTED_PASSWORD = "Accepted password for admin from {ip} port {port} ssh2" + + +# --------------------------------------------------------------------------- +# Schedule helpers — pure (rng, now, ...) -> list[datetime] +# --------------------------------------------------------------------------- + + +def schedule_uniform_spread( + rng: random.Random, now: datetime, count: int, spread: timedelta +) -> list[datetime]: + """``count`` timestamps drawn uniformly from ``(now - spread, now)``.""" + return sorted( + now - timedelta(seconds=rng.uniform(0, spread.total_seconds())) for _ in range(count) + ) + + +def schedule_fixed_interval( + now: datetime, count: int, interval: timedelta, end_before: timedelta +) -> list[datetime]: + """``count`` timestamps ``interval`` apart, the last landing at + ``now - end_before`` (a burst that already happened and stopped).""" + last = now - end_before + return [last - interval * (count - 1 - i) for i in range(count)] + + +def schedule_rate_burst( + now: datetime, count: int, interval: timedelta, end_before: timedelta = timedelta(0) +) -> list[datetime]: + """Alias of ``schedule_fixed_interval`` — named separately for callers + describing a dense attack rate (e.g. 50/min) rather than an ambient burst.""" + return schedule_fixed_interval(now, count, interval, end_before) + + +def schedule_two_bursts( + now: datetime, + burst_size: int, + gap: timedelta, + second_end_before: timedelta, +) -> list[datetime]: + """One ``burst_size``-event simultaneous burst, then a second identical + burst ``gap`` later, ending ``second_end_before`` before ``now`` — + the recidivist shape (ADR-0070 D3 recidivism clause).""" + second_at = now - second_end_before + first_at = second_at - gap + return [first_at] * burst_size + [second_at] * burst_size + + +def schedule_continuous_drip( + now: datetime, + initial_burst: int, + period: timedelta, + span: timedelta, + end_before: timedelta = timedelta(0), +) -> list[datetime]: + """An initial simultaneous burst followed by a periodic drip filling the + dip between what would otherwise be separate excursions — the moderate + grinder / endurance shape (ADR-0070 D3 endurance clause, + ``test_issue_54_attack_in_progress_campaign.py``'s + ``_continuous_pressure_events`` mirrored here through the real + normalizer).""" + start = now - end_before - span + out = [start] * initial_burst + t = period + while t <= span: + out.append(start + t) + t += period + return out + + +def schedule_paced(now: datetime, count: int, period: timedelta) -> list[datetime]: + """``count`` timestamps ``period`` apart, ending at ``now`` — a + sub-theta_press paced actor (ADR-0070 D9's designed INFORM exclusion).""" + return [now - period * (count - 1 - i) for i in range(count)] + + +# --------------------------------------------------------------------------- +# Template expansion — one actor's timestamps -> RawEvents +# --------------------------------------------------------------------------- + + +def suricata_events( + ip: str, + timestamps: list[datetime], + *, + category: str, + severity: int, + template: dict[str, Any] | None = None, + action: str = "allowed", + destination_ports: list[int] | None = None, +) -> list[RawEvent]: + """Expand a recorded Suricata EVE template into RawEvents for one actor. + + ``severity`` is the manifest's declared, classification.config-justified + integer priority (ADR-0068 fact 1) — never the template's own recorded + value, which reflects that one archived capture, not the persona being + modelled. + """ + base = template if template is not None else _EVE_RECON_TEMPLATE + out: list[RawEvent] = [] + for i, ts in enumerate(timestamps): + data = json.loads(json.dumps(base)) # deep copy, no shared mutable state + data["timestamp"] = ts.isoformat() + data["src_ip"] = ip + data["alert"]["category"] = category + data["alert"]["severity"] = severity + data["alert"]["action"] = action + if destination_ports: + data["dest_port"] = destination_ports[i % len(destination_ports)] + out.append(RawEvent(source_type="suricata", received_at=ts, data=data)) + return out + + +def syslog_events( + ip: str, timestamps: list[datetime], *, line_template: str = _SYSLOG_FAILED_PASSWORD +) -> list[RawEvent]: + """Expand the recorded "Failed password"/"Accepted password" line shape + (``packages/sources/syslog/tests/test_plugin.py``) into RawEvents.""" + out: list[RawEvent] = [] + for ts in timestamps: + line = line_template.format(ip=ip, port=44000 + (hash((ip, ts)) % 1000)) + out.append(RawEvent( + source_type="syslog", + received_at=ts, + data={"line": line, "client_ip": ip}, + )) + return out + + +def syslog_cef_allow_event(ip: str, ts: datetime, *, dest_ip: str = "192.0.2.1") -> RawEvent: + """One CEF firewall-style ALLOW event (``act=permit`` -> ADR-0070 D3's + ALLOW census: syslog_cef's generic CEF path, ``registry.py``).""" + line = ( + "CEF:0|Fortinet|FortiGate|6.4.5|9999|connection allowed|3|" + f"src={ip} dst={dest_ip} spt=51234 dpt=443 proto=TCP act=permit" + ) + return RawEvent( + source_type="syslog_cef", received_at=ts, data={"line": line, "client_ip": ip} + ) + + +# --------------------------------------------------------------------------- +# Generated scenario container +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class GeneratedScenario: + """The output of expanding a manifest: RawEvents plus a name -> IPs index + so tests can address "the 50/min attacker's actor" without re-deriving + the IP allocation order.""" + + raw_events: list[RawEvent] + persona_ips: dict[str, list[str]] = field(default_factory=dict) + now: datetime = field(default_factory=lambda: datetime(1970, 1, 1)) + + +def load_manifest(path: Path | None = None) -> dict[str, Any]: + manifest_path = path or (_MANIFESTS_DIR / "ambient_night.json") + with manifest_path.open() as fh: + data: dict[str, Any] = json.load(fh) + return data + + +def _expand_ambient_persona( + persona: dict[str, Any], rng: random.Random, ips: _IpAllocator, now: datetime +) -> tuple[list[RawEvent], list[str]]: + kind = persona["kind"] + actor_count = persona["actor_count"] + actor_ips: list[str] = [] + events: list[RawEvent] = [] + + if kind == "suricata_ambient": + spread = timedelta(hours=persona["spread_hours"]) + for _ in range(actor_count): + ip = ips.next() + actor_ips.append(ip) + n = rng.randint(persona["min_events"], persona["max_events"]) + ts = schedule_uniform_spread(rng, now, n, spread) + events += suricata_events( + ip, ts, category=persona["category"], severity=persona["severity"] + ) + elif kind == "syslog_ambient": + spread = timedelta(hours=persona["spread_hours"]) + for _ in range(actor_count): + ip = ips.next() + actor_ips.append(ip) + n = rng.randint(persona["min_events"], persona["max_events"]) + ts = schedule_uniform_spread(rng, now, n, spread) + events += syslog_events(ip, ts) + elif kind == "syslog_fixed_interval": + for _ in range(actor_count): + ip = ips.next() + actor_ips.append(ip) + ts = schedule_fixed_interval( + now, + persona["count"], + timedelta(minutes=persona["interval_minutes"]), + timedelta(hours=persona["end_before_hours"]), + ) + events += syslog_events(ip, ts) + else: # pragma: no cover - manifest authoring error + raise ValueError(f"unknown persona kind: {kind!r}") + + return events, actor_ips + + +def _expand_breach_overlay( + overlay: dict[str, Any], ips: _IpAllocator, now: datetime +) -> tuple[list[RawEvent], dict[str, list[str]]]: + events: list[RawEvent] = [] + persona_ips: dict[str, list[str]] = {} + + tier1 = overlay["tier1_actor"] + ip = ips.next() + persona_ips[tier1["name"]] = [ip] + end_before = timedelta(minutes=tier1["end_before_minutes"]) + allow_ts = now - end_before + corroborating_ts = allow_ts + timedelta(minutes=1) + events.append(syslog_cef_allow_event(ip, allow_ts)) + events += suricata_events( + ip, [corroborating_ts], category="Web Application Attack", severity=1, + template=_EVE_WEBATTACK_TEMPLATE, + ) + + band_high = overlay["band_high_actor"] + ip = ips.next() + persona_ips[band_high["name"]] = [ip] + count = band_high["count"] + ts = schedule_fixed_interval( + now, count, timedelta(minutes=5), timedelta(minutes=band_high["end_before_minutes"]) + ) + ports = [21, 22, 23, 25, 80, 443, 3389, 8080, 8443, 9000][:count] + events += suricata_events( + ip, ts, category="Detection of a Network Scan", severity=1, action="blocked", + template=_EVE_PORTSCAN_TEMPLATE, destination_ports=ports, + ) + + return events, persona_ips + + +def build_ambient_scenario( + manifest: dict[str, Any], seed: int, *, breach: bool = False +) -> GeneratedScenario: + """Expand the manifest into a full night of RawEvents. + + ``breach=False`` (default) is the ambient-only variant — pure noise, no + planted breach (ADR-0068 D2-5, the calm-reachability invariant). + ``breach=True`` additionally overlays the two anti-suppression personas + (ADR-0068 D2-3): a Tier-1 ALLOW+detection actor and a band-HIGH + accumulator, both inside the SAME ambient noise. + """ + now = datetime.fromisoformat(manifest["now"]) + rng = random.Random(seed) + ips = _IpAllocator() + + all_events: list[RawEvent] = [] + persona_ips: dict[str, list[str]] = {} + for persona in manifest["personas"]: + events, actor_ips = _expand_ambient_persona(persona, rng, ips, now) + all_events += events + persona_ips[persona["name"]] = actor_ips + + if breach: + events, overlay_ips = _expand_breach_overlay(manifest["breach_overlay"], ips, now) + all_events += events + persona_ips.update(overlay_ips) + + return GeneratedScenario(raw_events=all_events, persona_ips=persona_ips, now=now) diff --git a/tests/volume/harness.py b/tests/volume/harness.py new file mode 100644 index 0000000..68d22b8 --- /dev/null +++ b/tests/volume/harness.py @@ -0,0 +1,108 @@ +"""RawEvents -> real normalizers -> per-actor ThreatScore + EscalationVerdict +(ADR-0068 D4). No DB, no API server, no AI — the decision path is pure, so +the whole night scores in well under a second. + +This module deliberately MIRRORS ``firewatch_core.pipeline.Pipeline.analyze_ip``'s +decision slice (windowing -> run_rules -> detect -> merge_score -> decide) +rather than reimplementing it, so the oracle can never silently drift from +what the real pipeline computes. It skips only the I/O-bound concerns the +ADR excludes: the event store fetch (the manifest/generator supply events +directly), the AI sample call (ADR-0068 D4 — additive and never +de-escalating, so its absence cannot hide a flood), and geo/ASN enrichment +(presentation-only, no bearing on queue membership). +""" +from __future__ import annotations + +from collections import defaultdict +from datetime import datetime + +from firewatch_sdk import RawEvent, SecurityEvent, ThreatScore + +from firewatch_core.escalation.decider import decide +from firewatch_core.pipeline import W_CAMPAIGN, W_STATE, _window_slice +from firewatch_core.detector import detect +from firewatch_core.scoring import merge_score, run_rules + +from generator import SOURCE_ID_SURICATA, SOURCE_ID_SYSLOG, SOURCE_ID_SYSLOG_CEF + +# Real normalizers only (ADR-0068 D4 / the issue's acceptance criteria) — no +# hand-built SecurityEvents anywhere in the scenario path. +from firewatch_suricata.normalize import normalize as _normalize_suricata +from firewatch_syslog.normalize import normalize as _normalize_syslog +from firewatch_syslog_cef.normalize import normalize as _normalize_syslog_cef + +_NORMALIZERS = { + "suricata": (_normalize_suricata, SOURCE_ID_SURICATA), + "syslog": (_normalize_syslog, SOURCE_ID_SYSLOG), + "syslog_cef": (_normalize_syslog_cef, SOURCE_ID_SYSLOG_CEF), +} + + +def normalize_all(raw_events: list[RawEvent]) -> list[SecurityEvent]: + """Dispatch each RawEvent to its owning plugin's real ``normalize()``.""" + out: list[SecurityEvent] = [] + for raw in raw_events: + normalize_fn, source_id = _NORMALIZERS[raw.source_type] + out.append(normalize_fn(raw, source_id)) + return out + + +def group_by_ip(events: list[SecurityEvent]) -> dict[str, list[SecurityEvent]]: + by_ip: dict[str, list[SecurityEvent]] = defaultdict(list) + for e in events: + by_ip[e.source_ip].append(e) + return dict(by_ip) + + +def score_actor(events: list[SecurityEvent], now: datetime) -> ThreatScore: + """One actor's full ``analyze_ip`` decision slice, without I/O or AI. + + Mirrors ``Pipeline.analyze_ip`` field-for-field for the fields that drive + queue membership (``threat_level``, ``score``, ``escalation``) and for + the conservation/provenance fields (`total_events`, `source_types`, + `first_seen`/`last_seen`). + """ + ip = events[0].source_ip + blocked = [e for e in events if e.action in ("BLOCK", "DROP")] + timestamps = [e.timestamp for e in events] + + state_events = _window_slice(events, now, W_STATE) + campaign_events = _window_slice(events, now, W_CAMPAIGN) + + rule_score, attack_types = run_rules(state_events) + detections = detect(campaign_events, now=now) + detection_boost = sum(d.score_delta for d in detections) + + score, level, score_derivation = merge_score( + rule_score, None, detection_boost=detection_boost + ) + escalation_verdict = decide(state_events, detections) + + return ThreatScore( + source_ip=ip, + threat_level=level, # type: ignore[arg-type] + score=score, + total_events=len(events), + blocked_events=len(blocked), + attack_types=attack_types, + first_seen=min(timestamps), + last_seen=max(timestamps), + source_types=sorted({e.source_type for e in events}), + detections=detections, + score_derivation=score_derivation, # type: ignore[arg-type] + escalation=escalation_verdict, + ) + + +def score_all(raw_events: list[RawEvent], now: datetime) -> list[ThreatScore]: + """RawEvents -> real normalizers -> one ThreatScore per distinct actor IP. + + Sorted by ``source_ip`` — determinism for the regeneration-drift test + (ADR-0068 D2-6), independent of dict/set iteration order. + """ + events = normalize_all(raw_events) + by_ip = group_by_ip(events) + return sorted( + (score_actor(actor_events, now) for actor_events in by_ip.values()), + key=lambda t: t.source_ip, + ) diff --git a/tests/volume/manifests/ambient_night.json b/tests/volume/manifests/ambient_night.json new file mode 100644 index 0000000..eca86aa --- /dev/null +++ b/tests/volume/manifests/ambient_night.json @@ -0,0 +1,76 @@ +{ + "_comment": "ADR-0068 D3 manifest — reviewable in one screen. Every persona's severity/category is justified against Suricata's shipped classification.config (ADR-0068 fact 1) or ADR-0069's syslog fallback recalibration (D4b). now = analysis anchor; all persona timing is expressed relative to it so the whole night is scored as one deterministic snapshot (ADR-0070 D4 windowing). Manifest-change discipline: README.md.", + "now": "2026-02-02T06:00:00+00:00", + "flood_tripwire": 10, + "personas": [ + { + "name": "suricata_recon_scanners", + "kind": "suricata_ambient", + "actor_count": 40, + "min_events": 1, + "max_events": 4, + "category": "Attempted Information Leak", + "severity": 2, + "spread_hours": 8, + "justification": "classification.config: attempted-recon = priority 2 (ET SCAN ambient mass, ADR-0068 fact 1) -> ADR-0069 D4a normalizer maps priority 2 to 'medium'", + "expected": {"queue": false, "disposition": "observed"} + }, + { + "name": "suricata_reputation_scanners", + "kind": "suricata_ambient", + "actor_count": 40, + "min_events": 1, + "max_events": 4, + "category": "Misc Attack", + "severity": 2, + "spread_hours": 8, + "justification": "classification.config: misc-attack = priority 2 (ET DROP/CINS reputation-list mass) -> ADR-0069 D4a normalizer maps priority 2 to 'medium'", + "expected": {"queue": false, "disposition": "observed"} + }, + { + "name": "syslog_failed_login_scanners", + "kind": "syslog_ambient", + "actor_count": 45, + "min_events": 1, + "max_events": 4, + "spread_hours": 8, + "justification": "ADR-0069 D4b: a lone 'Failed password' line is Sigma-low ('notable event but rarely an incident'); an internet-exposed sshd sees hundreds of distinct ambient scanner IPs a night, each emitting 1-4 such lines", + "expected": {"queue": false, "disposition": "observed"} + }, + { + "name": "ambient_sshd_burst_leaves", + "kind": "syslog_fixed_interval", + "actor_count": 1, + "count": 5, + "interval_minutes": 2.5, + "end_before_hours": 2, + "justification": "ADR-0070 distribution table: 5-in-10-min burst that leaves, peak ~= theta_press (5) -- borderline, contributes band score/pressure-strip visibility only, never queues", + "expected": {"queue": false, "disposition": "observed"} + }, + { + "name": "isolated_inform_case", + "kind": "syslog_fixed_interval", + "actor_count": 1, + "count": 2, + "interval_minutes": 30, + "end_before_hours": 1, + "justification": "Maintainer's INFORM case (ADR-0070 distribution table): 2 attempts/30min isolated -- record + pressure strip only, never queues", + "expected": {"queue": false, "disposition": "observed"} + } + ], + "breach_overlay": { + "tier1_actor": { + "name": "tier1_breach_allow", + "end_before_minutes": 5, + "justification": "ADR-0068 D2-3 anti-suppression persona: one CEF firewall ALLOW (act=permit) + a corroborating Suricata ALERT on the same IP within the hour fires multi_source_attack -- ALLOW + any detection is Tier 1 unconditionally (ADR-0067 D1)", + "expected": {"queue": true, "tier": 1, "disposition": "allowed_through"} + }, + "band_high_actor": { + "name": "band_high_port_scanner", + "count": 10, + "end_before_minutes": 10, + "justification": "band-axis anti-suppression persona: 10 Suricata BLOCK events across 10 distinct destination ports (port_scan +25, brute_force +30, persistence +10 = 65) crosses the HIGH band (>=51) via run_rules alone, independent of the escalation-tier axis (tier 3, blocked_persistent)", + "expected": {"queue": true, "tier": 3, "disposition": "blocked_persistent"} + } + } +} diff --git a/tests/volume/test_triage_volume.py b/tests/volume/test_triage_volume.py new file mode 100644 index 0000000..070b255 --- /dev/null +++ b/tests/volume/test_triage_volume.py @@ -0,0 +1,483 @@ +"""The volume oracle (issue #50, ADR-0068) — usability invariants at realistic +event volume, and the ADR-0070 (+ Amendment 1) distribution-table personas as +named, individually-failing assertions. + +Two disciplines live in this ONE file, deliberately kept apart from +``tests/golden/`` (ADR-0068 D1): golden pins EXACT scores for 1-12 event +scenarios; this oracle pins INVARIANTS (set membership, bounds, ordering, +conservation) under a realistic ~130-actor/~850-event night, built from the +seeded generator (``generator.py``) driving the REAL Suricata/syslog/CEF +normalizers (``harness.py``) — never a hand-built ``SecurityEvent``. + +EARS -> test mapping (issue #50 acceptance criteria) +───────────────────────────────────────────────────── +AC1 (exact queue membership, ambient-only) -> TestAmbientOnlyCalmState +AC2 (flood tripwire, len(queue)<=10) -> TestFloodTripwire +AC3 (breach-among-noise anti-suppression) -> TestBreachAntiSuppression +AC4 (conservation — observed is never a drop) -> TestConservation +AC5 (calm-state precondition) -> TestAmbientOnlyCalmState +AC6 (real normalizers only) -> TestRealNormalizersOnly +AC7 (manifest declares justified personas) -> TestManifestDiscipline +AC8 (deterministic generation, drift-checked) -> TestDeterminism +AC9 (frontend vitest sibling) -> frontend/src/test/triageBand.volume.test.ts +AC10 (runs in default suite, <=5s) -> this whole file carries no + opt-in marker; timed below +AC11 (red pre-#42, green on M1 target) -> PR description (see README.md) +AC12 (tests/golden untouched) -> verified in the PR (sha + unchanged), not re-asserted here + +The ADR-0070 distribution table + Amendment 1 personas — the "ledger of +record" the constants (theta_press/theta_high/theta_quiet/H/D_endure) are +adjudicated against — get their own named test classes below (mirroring, not +duplicating, the unit-level pins in +``packages/firewatch-core/tests/test_issue_54_attack_in_progress_campaign.py``): +a constants change that breaks a persona here FAILS a named test with a +clear message, exactly like that file, but exercised through the real +syslog normalizer end-to-end. + +Fixture IPs: RFC 5737 documentation ranges only (192.0.2.0/24, 198.51.100.0/24, +203.0.113.0/24) — never real/routable (testing-conventions skill). +""" +from __future__ import annotations + +import json +import time +from datetime import datetime, timedelta +from pathlib import Path + +import pytest +from firewatch_sdk import SecurityEvent, ThreatScore + +import generator +import harness +from firewatch_core.escalation.worthiness import is_alert_worthy + +SEED = 20260202 +_FIXTURES_DIR = Path(__file__).parent / "fixtures" + +# --------------------------------------------------------------------------- +# Shared scenario fixtures — computed once per test session (pure, sub-second). +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="session") +def manifest() -> dict: + return generator.load_manifest() + + +@pytest.fixture(scope="session") +def ambient_scenario(manifest: dict) -> generator.GeneratedScenario: + return generator.build_ambient_scenario(manifest, seed=SEED, breach=False) + + +@pytest.fixture(scope="session") +def breach_scenario(manifest: dict) -> generator.GeneratedScenario: + return generator.build_ambient_scenario(manifest, seed=SEED, breach=True) + + +@pytest.fixture(scope="session") +def ambient_scores(ambient_scenario: generator.GeneratedScenario) -> list[ThreatScore]: + return harness.score_all(ambient_scenario.raw_events, ambient_scenario.now) + + +@pytest.fixture(scope="session") +def breach_scores(breach_scenario: generator.GeneratedScenario) -> list[ThreatScore]: + return harness.score_all(breach_scenario.raw_events, breach_scenario.now) + + +def _queue(scores: list[ThreatScore]) -> list[ThreatScore]: + """The real queue predicate (ADR-0059 D2) at the default HIGH threshold — + the exact function the banner/notifier calls, not a test re-implementation.""" + return [t for t in scores if is_alert_worthy(t, "HIGH")] + + +def _sort_triage(scores: list[ThreatScore]) -> list[ThreatScore]: + """Mirrors ``frontend/src/lib/triageBand.ts``'s ``deriveTriageActors`` sort: + tier ascending (``None`` -> 99, sorts last), then score descending. Kept as + a literal port (not an import — there is no cross-language import seam) + so the Python oracle can assert the sort order the frontend sibling test + (AC9) independently re-derives from ``derived_threats.json``.""" + def key(t: ThreatScore) -> tuple[int, int]: + tier = t.escalation.tier if t.escalation and t.escalation.tier is not None else 99 + return (tier, -t.score) + return sorted(scores, key=key) + + +# --------------------------------------------------------------------------- +# AC1 / AC5 — ambient-only: exact queue membership is empty; calm is reachable +# --------------------------------------------------------------------------- + + +class TestAmbientOnlyCalmState: + def test_queue_is_exactly_empty(self, ambient_scores: list[ThreatScore]) -> None: + """ADR-0068 D2-1: set equality, not a ceiling — the ambient-only + manifest declares NO queue-worthy persona, so the queue set must + equal the empty set exactly.""" + queue = _queue(ambient_scores) + assert {t.source_ip for t in queue} == set(), ( + f"ambient-only scenario must be calm; flooded by: " + f"{[(t.source_ip, t.threat_level, t.escalation) for t in queue]}" + ) + + def test_manifest_declares_no_queue_expectation_for_any_ambient_persona( + self, manifest: dict + ) -> None: + """Guards the manifest itself: a persona authored with + ``expected.queue: true`` in the ambient-only section would silently + invalidate the calm-state claim above without this sanity check.""" + for persona in manifest["personas"]: + assert persona["expected"]["queue"] is False, ( + f"persona {persona['name']!r} declares queue=true but lives in " + "the ambient-only section — move it to a named persona test" + ) + + def test_calm_state_precondition_nonzero_record_count( + self, ambient_scores: list[ThreatScore] + ) -> None: + """ADR-0068 D2-5 / issue #43's calm state: empty queue AND a nonzero + record (observed) count — calm is "on the record, nothing pending", + never "nothing happened".""" + queue = _queue(ambient_scores) + assert len(queue) == 0 + assert len(ambient_scores) > 0 + + +# --------------------------------------------------------------------------- +# AC2 — the flood tripwire, independent of manifest set-equality edits +# --------------------------------------------------------------------------- + + +class TestFloodTripwire: + def test_ambient_only_queue_within_tripwire( + self, ambient_scores: list[ThreatScore], manifest: dict + ) -> None: + assert len(_queue(ambient_scores)) <= manifest["flood_tripwire"] + + def test_breach_variant_queue_within_tripwire( + self, breach_scores: list[ThreatScore], manifest: dict + ) -> None: + assert len(_queue(breach_scores)) <= manifest["flood_tripwire"] + + +# --------------------------------------------------------------------------- +# AC3 — breach-among-noise: anti-suppression + Tier-1-sorts-first +# --------------------------------------------------------------------------- + + +class TestBreachAntiSuppression: + def test_exact_queue_membership_is_the_two_planted_actors( + self, breach_scores: list[ThreatScore], breach_scenario: generator.GeneratedScenario + ) -> None: + tier1_ip = breach_scenario.persona_ips["tier1_breach_allow"][0] + band_high_ip = breach_scenario.persona_ips["band_high_port_scanner"][0] + queue_ips = {t.source_ip for t in _queue(breach_scores)} + assert queue_ips == {tier1_ip, band_high_ip}, ( + "a gate that only rewards silence must fail here — both planted " + f"actors must surface; got queue={queue_ips}" + ) + + def test_tier1_actor_sorts_first( + self, breach_scores: list[ThreatScore], breach_scenario: generator.GeneratedScenario + ) -> None: + tier1_ip = breach_scenario.persona_ips["tier1_breach_allow"][0] + ordered = _sort_triage(_queue(breach_scores)) + assert ordered[0].source_ip == tier1_ip + assert ordered[0].escalation is not None + assert ordered[0].escalation.tier == 1 + + def test_band_high_actor_qualifies_via_band_axis_not_tier_axis( + self, breach_scores: list[ThreatScore], breach_scenario: generator.GeneratedScenario + ) -> None: + """The second planted actor deliberately reaches the queue via + ``band_meets(threat_level, "HIGH")`` (score-driven), NOT via the + tier axis (``tier<=2``) — proving is_alert_worthy's OTHER OR-branch + independently of the first.""" + band_high_ip = breach_scenario.persona_ips["band_high_port_scanner"][0] + actor = next(t for t in breach_scores if t.source_ip == band_high_ip) + assert actor.escalation is not None + assert actor.escalation.tier is not None and actor.escalation.tier > 2 + assert actor.threat_level in ("HIGH", "CRITICAL") + + def test_ambient_noise_stays_silent_inside_the_breach_variant( + self, breach_scores: list[ThreatScore], breach_scenario: generator.GeneratedScenario + ) -> None: + """The 127 ambient actors must not be swept into the queue merely + because two loud actors were added to the same population.""" + planted = set(breach_scenario.persona_ips["tier1_breach_allow"]) | set( + breach_scenario.persona_ips["band_high_port_scanner"] + ) + ambient_in_queue = [t for t in _queue(breach_scores) if t.source_ip not in planted] + assert ambient_in_queue == [] + + +# --------------------------------------------------------------------------- +# AC4 — conservation: observed is never a drop (ADR-0067 D5) +# --------------------------------------------------------------------------- + + +class TestConservation: + def test_ambient_only_every_actor_is_observed_and_none_dropped( + self, ambient_scenario: generator.GeneratedScenario, ambient_scores: list[ThreatScore] + ) -> None: + expected_actor_count = sum(len(ips) for ips in ambient_scenario.persona_ips.values()) + assert len(ambient_scores) == expected_actor_count + queue = _queue(ambient_scores) + non_queue = [t for t in ambient_scores if t not in queue] + assert len(queue) + len(non_queue) == len(ambient_scores) + + def test_breach_variant_every_actor_accounted_for_exactly_once( + self, breach_scenario: generator.GeneratedScenario, breach_scores: list[ThreatScore] + ) -> None: + expected_actor_count = sum(len(ips) for ips in breach_scenario.persona_ips.values()) + assert len(breach_scores) == expected_actor_count + queue_ips = {t.source_ip for t in _queue(breach_scores)} + non_queue_ips = {t.source_ip for t in breach_scores if t.source_ip not in queue_ips} + assert queue_ips | non_queue_ips == {t.source_ip for t in breach_scores} + assert queue_ips & non_queue_ips == set() + + def test_no_duplicate_ip_allocation_across_personas( + self, breach_scenario: generator.GeneratedScenario + ) -> None: + all_ips = [ip for ips in breach_scenario.persona_ips.values() for ip in ips] + assert len(all_ips) == len(set(all_ips)), "the IP allocator must never double-assign" + + +# --------------------------------------------------------------------------- +# AC6 — real normalizers only, no hand-built SecurityEvents in the scenario path +# --------------------------------------------------------------------------- + + +class TestRealNormalizersOnly: + def test_scenario_events_are_produced_by_the_real_normalizers( + self, breach_scenario: generator.GeneratedScenario + ) -> None: + """generator.py emits RawEvents exclusively; harness.py's + normalize_all() dispatches every one through + firewatch_suricata/firewatch_syslog/firewatch_syslog_cef's actual + ``normalize()`` — asserted here by checking the raw payloads carry + source-shaped fields (EVE ``alert``/CEF ``line``) rather than + already-normalized SecurityEvent fields.""" + source_types = {raw.source_type for raw in breach_scenario.raw_events} + assert source_types <= {"suricata", "syslog", "syslog_cef"} + events = harness.normalize_all(breach_scenario.raw_events) + assert all(isinstance(e, SecurityEvent) for e in events) + + +# --------------------------------------------------------------------------- +# AC7 — manifest declares justified, reviewable personas +# --------------------------------------------------------------------------- + + +class TestManifestDiscipline: + def test_every_persona_declares_count_and_justification(self, manifest: dict) -> None: + for persona in manifest["personas"]: + assert persona["actor_count"] > 0 + assert persona["justification"] + assert "expected" in persona + + def test_every_breach_persona_declares_justification(self, manifest: dict) -> None: + for persona in manifest["breach_overlay"].values(): + assert persona["justification"] + assert "expected" in persona + + +# --------------------------------------------------------------------------- +# AC8 — deterministic generation, drift-checked against the committed fixture +# --------------------------------------------------------------------------- + + +class TestDeterminism: + def test_regeneration_is_byte_stable_for_the_same_seed(self, manifest: dict) -> None: + first = generator.build_ambient_scenario(manifest, seed=SEED, breach=True) + second = generator.build_ambient_scenario(manifest, seed=SEED, breach=True) + first_scores = harness.score_all(first.raw_events, first.now) + second_scores = harness.score_all(second.raw_events, second.now) + assert [t.model_dump(mode="json") for t in first_scores] == [ + t.model_dump(mode="json") for t in second_scores + ] + + def test_committed_derived_threats_fixture_matches_current_generation( + self, breach_scores: list[ThreatScore] + ) -> None: + """The regeneration-drift gate (ADR-0068 D2-6): a constants change or + an unreviewed manifest edit that alters the derived population FAILS + here with a clear message, instead of the frontend sibling silently + drifting out of sync. Regenerate deliberately via + ``uv run python scripts/regen_volume_fixtures.py`` (README.md's + manifest-change discipline).""" + committed = json.loads((_FIXTURES_DIR / "derived_threats.json").read_text()) + current = [t.model_dump(mode="json") for t in breach_scores] + assert current == committed, ( + "tests/volume/fixtures/derived_threats.json has drifted from the " + "generator/harness's current output — if this is a deliberate, " + "justified manifest/constants change, regenerate with " + "`uv run python scripts/regen_volume_fixtures.py` and state the " + "justification in the PR (README.md discipline); otherwise this " + "is a real regression." + ) + + +# --------------------------------------------------------------------------- +# AC10 — CI budget: the whole scenario (both variants) stays well under 5s +# --------------------------------------------------------------------------- + + +class TestCiBudget: + def test_full_scenario_scores_in_well_under_five_seconds(self, manifest: dict) -> None: + start = time.monotonic() + scenario = generator.build_ambient_scenario(manifest, seed=SEED, breach=True) + harness.score_all(scenario.raw_events, scenario.now) + elapsed = time.monotonic() - start + assert elapsed < 5.0, f"volume scenario took {elapsed:.2f}s — over the ADR-0068 budget" + + +# --------------------------------------------------------------------------- +# The ADR-0070 (+ Amendment 1) distribution-table personas — the ledger of +# record. Each class below is a NAMED assertion: a constants change that +# breaks a persona fails here with the persona's own name in the traceback, +# not a silent drift. Driven through the real syslog normalizer +# (``generator.syslog_events`` -> ``firewatch_syslog.normalize``), mirroring +# — never duplicating — the unit-level pins in +# ``test_issue_54_attack_in_progress_campaign.py``. +# --------------------------------------------------------------------------- + +NOW = generator.load_manifest()["now"] + + +def _score_persona(ip: str, timestamps: list, now) -> ThreatScore: + raw = generator.syslog_events(ip, timestamps) + events = harness.normalize_all(raw) + return harness.score_actor(events, now) + + +def _rule_names(t: ThreatScore) -> set[str]: + return {d.rule_name for d in t.detections if d.rule_name} + + +class TestPersonaFiftyPerMinuteAttacker: + """ADR-0070 D3 flagship case: an IP attempting SSH brute force 50 + times/min queues WHILE IT IS HAPPENING, within the first minute.""" + + def test_queues_within_the_first_minute_via_attack_in_progress(self) -> None: + now = datetime.fromisoformat(NOW) + ts = generator.schedule_rate_burst( + now, 50, timedelta(seconds=1.2), end_before=timedelta(seconds=1.2) + ) + t = _score_persona("203.0.113.5", ts, now) + assert "attack_in_progress" in _rule_names(t) + assert t.escalation is not None and t.escalation.tier == 2 + + +class TestPersonaSingleBurstFadesAfter: + """ADR-0070 distribution table: a single 120-attempt/40-min burst that + never returns queues DURING the attack and fades from the queue after + it stops — no manual expiry, decay alone.""" + + def test_queues_during_the_burst(self) -> None: + now = datetime.fromisoformat(NOW) + ts = generator.schedule_rate_burst(now, 120, timedelta(seconds=20)) + t = _score_persona("203.0.113.6", ts, now) + assert "attack_in_progress" in _rule_names(t) + assert t.escalation is not None and t.escalation.tier == 2 + + def test_fades_from_the_queue_after_it_stops(self) -> None: + now = datetime.fromisoformat(NOW) + ts = generator.schedule_rate_burst(now, 120, timedelta(seconds=20)) + later = now + timedelta(hours=2) + t = _score_persona("203.0.113.6", ts, later) + assert "attack_in_progress" not in _rule_names(t) + assert "campaign" not in _rule_names(t) + assert t.escalation is not None and t.escalation.tier is None + + +class TestPersonaNightlyRecidivist: + """ADR-0070 D3 recidivism clause: two 10-attempt bursts separated by a + quiet gap (collapsed well below theta_quiet) queue via `campaign` on + the second night — recidivism, theta_quiet-separated.""" + + def test_queues_night_two_via_campaign_recidivism(self) -> None: + now = datetime.fromisoformat(NOW) + ts = generator.schedule_two_bursts( + now, burst_size=10, gap=timedelta(hours=29), second_end_before=timedelta(hours=1) + ) + t = _score_persona("203.0.113.7", ts, now) + assert "campaign" in _rule_names(t) + assert t.escalation is not None and t.escalation.tier == 2 + campaign = next(d for d in t.detections if d.rule_name == "campaign") + assert "pressure episodes" in campaign.reason, "must queue via recidivism, not endurance" + + +class TestPersonaModerateGrinderEndurance: + """ADR-0070 D3 endurance clause: a moderate-rate grinder (12/h, + continuous) that never spikes to theta_high queues via `campaign` + (endurance) once its merged episode reaches D_endure (~24h) — NOT + within the first 30 minutes (no campaign-in-30-min).""" + + def test_does_not_fire_campaign_within_thirty_minutes(self) -> None: + now = datetime.fromisoformat(NOW) + span = timedelta(hours=25) + start = now - span + ts = generator.schedule_continuous_drip(now, 6, timedelta(minutes=5), span) + checkpoint = start + timedelta(minutes=30) + raw = generator.syslog_events("203.0.113.8", ts) + events = [e for e in harness.normalize_all(raw) if e.timestamp <= checkpoint] + t = harness.score_actor(events, checkpoint) + assert "campaign" not in _rule_names(t) + assert t.escalation is not None and t.escalation.tier is None + + def test_queues_via_endurance_at_approximately_twenty_four_hours(self) -> None: + now = datetime.fromisoformat(NOW) + span = timedelta(hours=25) + ts = generator.schedule_continuous_drip(now, 6, timedelta(minutes=5), span) + t = _score_persona("203.0.113.8", ts, now) + assert "campaign" in _rule_names(t) + campaign = next(d for d in t.detections if d.rule_name == "campaign") + assert "spanning" in campaign.reason, "must queue via endurance, not recidivism" + assert t.escalation is not None and t.escalation.tier == 2 + + +class TestPersonaSlowGrinderNeverQueues: + """ADR-0070 D9's designed INFORM exclusion: a sub-theta_press paced + actor (1 attempt every 3 days) never queues at any lifetime volume.""" + + def test_never_queues_at_any_lifetime_volume(self) -> None: + now = datetime.fromisoformat(NOW) + ts = generator.schedule_paced(now, 50, timedelta(days=3)) + t = _score_persona("203.0.113.9", ts, now) + assert _rule_names(t) == set() + assert t.escalation is not None and t.escalation.tier is None + assert t.escalation.disposition == "observed" + + +class TestPersonaModerateBurstHysteresisNoCampaign: + """ADR-0070 Amendment 1 (theta_quiet): the exact PR #86 defect fixture + (10 attempts 4 min apart) is ONE episode under quiet-collapse + hysteresis — `campaign` must NOT fire (no recidivism, no endurance, no + breadth) — through the real syslog normalizer, not just the unit-level + `episodes()` pin.""" + + def test_one_episode_no_campaign(self) -> None: + now = datetime.fromisoformat(NOW) + ts = generator.schedule_fixed_interval(now, 10, timedelta(minutes=4), timedelta(0)) + t = _score_persona("203.0.113.10", ts, now) + assert "campaign" not in _rule_names(t) + assert t.escalation is not None and t.escalation.tier is None + + +class TestPersonaAmbientSuricataPriority2NoTicket: + """ADR-0068/0069 D4a outcome: ambient priority-2 Suricata noise (ET + SCAN/ET DROP reputation mass) stays <= medium severity and never + reaches a Tier-2 ticket, even at higher per-actor volume than the + ambient mass's 1-4/night norm.""" + + def test_priority_two_stays_medium_and_never_queues(self) -> None: + now = datetime.fromisoformat(NOW) + ts = [now - timedelta(minutes=10 * i) for i in range(50)] + raw = generator.suricata_events( + "203.0.113.11", ts, category="Misc Attack", severity=2 + ) + events = harness.normalize_all(raw) + assert all(e.severity == "medium" for e in events) + t = harness.score_actor(events, now) + assert t.escalation is not None and t.escalation.tier is None + assert t.threat_level in ("LOW", "MEDIUM")