Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ Small prototypes for telemetry analytics, monitoring, and detection-oriented sig
python -m telemetry_window_demo.cli run --config configs/default.yaml
```

The sample config reads `data/raw/sample_events.jsonl` and regenerates outputs in `data/processed/`.
The sample config reads `data/raw/sample_events.jsonl` and regenerates outputs in `data/processed/`.

For a richer scenario pack that is easier to walk through in demos:

```bash
python -m telemetry_window_demo.cli run --config configs/richer_sample.yaml
```

That scenario pack reads `data/raw/richer_sample_events.jsonl` and writes outputs to `data/processed/richer_sample/`.
It currently produces `28` normalized events, `24` windows, and `8` alerts.

## Current behavior

Expand Down Expand Up @@ -50,6 +59,8 @@ With the bundled sample data, the default run currently produces:
- `12` alerts after applying a `60` second cooldown

The default config suppresses repeated alerts by cooldown key. The key is `rule_name` plus an entity scope when the rule input includes `entity`, `source`, `target`, or `host`; otherwise it falls back to `rule_name` alone. Different cooldown keys can still alert on the same window.

The richer scenario pack uses a longer `120` second cooldown so the output stays compact enough to inspect as four phases: normal background activity, a login-failure burst, a high-risk configuration change with follow-on policy denials, and a rare malware-alert repeat sequence.

## Outputs

Expand Down
47 changes: 47 additions & 0 deletions configs/richer_sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
input_path: data/raw/richer_sample_events.jsonl
output_dir: data/processed/richer_sample

time:
timestamp_col: timestamp
window_size_seconds: 60
step_size_seconds: 10

features:
count_event_types:
- login_fail
- login_success
- config_change
- malware_alert
- policy_denied
error_statuses:
- fail
- blocked
severity_levels:
- high
- critical

rules:
cooldown_seconds: 120
high_error_rate:
threshold: 0.30
severity: medium
login_fail_burst:
threshold: 8
severity: high
high_severity_spike:
threshold: 3
severity: high
persistent_high_error:
threshold: 0.25
consecutive_windows: 2
severity: medium
source_spread_spike:
absolute_threshold: 10
multiplier: 1.3
severity: medium
rare_event_repeat:
threshold: 2
event_types:
- malware_alert
- policy_denied
severity: high
9 changes: 9 additions & 0 deletions data/processed/richer_sample/alerts.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
alert_time,rule_name,severity,window_start,window_end,message
2026-03-11T09:01:10Z,high_error_rate,medium,2026-03-11T09:00:10Z,2026-03-11T09:01:10Z,error_rate 0.33 exceeded 0.30
2026-03-11T09:01:20Z,persistent_high_error,medium,2026-03-11T09:00:20Z,2026-03-11T09:01:20Z,error_rate stayed above 0.25 for 2 windows
2026-03-11T09:01:40Z,login_fail_burst,high,2026-03-11T09:00:40Z,2026-03-11T09:01:40Z,"login_fail_count reached 9, threshold is 8"
2026-03-11T09:02:30Z,high_severity_spike,high,2026-03-11T09:01:30Z,2026-03-11T09:02:30Z,high_severity_count reached 4
2026-03-11T09:02:40Z,rare_event_repeat_policy_denied,high,2026-03-11T09:01:40Z,2026-03-11T09:02:40Z,policy_denied repeated 3 times in one window
2026-03-11T09:03:10Z,high_error_rate,medium,2026-03-11T09:02:10Z,2026-03-11T09:03:10Z,error_rate 0.43 exceeded 0.30
2026-03-11T09:03:20Z,persistent_high_error,medium,2026-03-11T09:02:20Z,2026-03-11T09:03:20Z,error_rate stayed above 0.25 for 2 windows
2026-03-11T09:03:40Z,rare_event_repeat_malware_alert,high,2026-03-11T09:02:40Z,2026-03-11T09:03:40Z,malware_alert repeated 3 times in one window
Binary file added data/processed/richer_sample/alerts_timeline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions data/processed/richer_sample/features.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
window_start,window_end,event_count,error_count,error_rate,unique_sources,unique_targets,high_severity_count,login_fail_count,login_success_count,config_change_count,malware_alert_count,policy_denied_count
2026-03-11T09:00:00Z,2026-03-11T09:01:00Z,6,0,0.0,5,3,0,0,2,0,0,0
2026-03-11T09:00:10Z,2026-03-11T09:01:10Z,6,2,0.3333333333333333,6,3,0,2,1,0,0,0
2026-03-11T09:00:20Z,2026-03-11T09:01:20Z,7,4,0.5714285714285714,7,2,0,4,1,0,0,0
2026-03-11T09:00:30Z,2026-03-11T09:01:30Z,9,7,0.7777777777777778,9,2,0,7,1,0,0,0
2026-03-11T09:00:40Z,2026-03-11T09:01:40Z,10,9,0.9,10,2,0,9,0,0,0,0
2026-03-11T09:00:50Z,2026-03-11T09:01:50Z,10,9,0.9,9,1,0,9,1,0,0,0
2026-03-11T09:01:00Z,2026-03-11T09:02:00Z,10,9,0.9,9,1,0,9,1,0,0,0
2026-03-11T09:01:10Z,2026-03-11T09:02:10Z,8,7,0.875,8,1,0,7,1,0,0,0
2026-03-11T09:01:20Z,2026-03-11T09:02:20Z,8,5,0.625,7,3,2,5,1,2,0,0
2026-03-11T09:01:30Z,2026-03-11T09:02:30Z,7,3,0.42857142857142855,5,4,4,2,1,3,0,1
2026-03-11T09:01:40Z,2026-03-11T09:02:40Z,7,3,0.42857142857142855,3,4,6,0,1,3,0,3
2026-03-11T09:01:50Z,2026-03-11T09:02:50Z,6,3,0.5,2,3,6,0,0,3,0,3
2026-03-11T09:02:00Z,2026-03-11T09:03:00Z,7,3,0.42857142857142855,3,4,6,0,0,3,0,3
2026-03-11T09:02:10Z,2026-03-11T09:03:10Z,7,3,0.42857142857142855,3,4,6,0,0,3,0,3
2026-03-11T09:02:20Z,2026-03-11T09:03:20Z,5,3,0.6,3,4,4,0,0,1,0,3
2026-03-11T09:02:30Z,2026-03-11T09:03:30Z,4,3,0.75,3,4,3,0,0,0,1,2
2026-03-11T09:02:40Z,2026-03-11T09:03:40Z,4,3,0.75,2,2,3,0,0,0,3,0
2026-03-11T09:02:50Z,2026-03-11T09:03:50Z,5,3,0.6,3,3,3,0,1,0,3,0
2026-03-11T09:03:00Z,2026-03-11T09:04:00Z,5,3,0.6,3,3,3,0,1,0,3,0
2026-03-11T09:03:10Z,2026-03-11T09:04:10Z,5,3,0.6,3,3,3,0,1,0,3,0
2026-03-11T09:03:20Z,2026-03-11T09:04:20Z,5,3,0.6,3,3,3,0,1,0,3,0
2026-03-11T09:03:30Z,2026-03-11T09:04:30Z,4,2,0.5,3,3,2,0,1,0,2,0
2026-03-11T09:03:40Z,2026-03-11T09:04:40Z,2,0,0.0,2,2,0,0,1,0,0,0
2026-03-11T09:03:50Z,2026-03-11T09:04:50Z,1,0,0.0,1,1,0,0,0,0,0,0
28 changes: 28 additions & 0 deletions data/raw/richer_sample_events.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{"timestamp":"2026-03-11T09:00:02Z","event_type":"login_success","source":"user_a","target":"auth_service","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:00:08Z","event_type":"token_refresh","source":"user_a","target":"auth_service","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:00:14Z","event_type":"health_check","source":"monitor","target":"api_gateway","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:00:22Z","event_type":"file_download","source":"user_b","target":"storage_api","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:00:31Z","event_type":"login_success","source":"user_c","target":"auth_service","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:00:42Z","event_type":"data_export","source":"svc_backup","target":"storage_api","status":"ok","severity":"medium"}
{"timestamp":"2026-03-11T09:01:05Z","event_type":"login_fail","source":"user_d","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:08Z","event_type":"login_fail","source":"user_e","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:12Z","event_type":"login_fail","source":"user_f","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:16Z","event_type":"login_fail","source":"user_g","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:20Z","event_type":"login_fail","source":"user_h","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:24Z","event_type":"login_fail","source":"user_i","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:28Z","event_type":"login_fail","source":"user_j","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:31Z","event_type":"login_fail","source":"user_k","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:36Z","event_type":"login_fail","source":"user_l","target":"auth_service","status":"fail","severity":"medium"}
{"timestamp":"2026-03-11T09:01:44Z","event_type":"login_success","source":"user_d","target":"auth_service","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:02:12Z","event_type":"config_change","source":"admin_root","target":"iam_policy","status":"ok","severity":"critical"}
{"timestamp":"2026-03-11T09:02:18Z","event_type":"config_change","source":"admin_root","target":"vpn_gateway","status":"ok","severity":"critical"}
{"timestamp":"2026-03-11T09:02:24Z","event_type":"config_change","source":"admin_root","target":"db_firewall","status":"ok","severity":"critical"}
{"timestamp":"2026-03-11T09:02:29Z","event_type":"policy_denied","source":"policy_engine","target":"db_firewall","status":"blocked","severity":"high"}
{"timestamp":"2026-03-11T09:02:34Z","event_type":"policy_denied","source":"policy_engine","target":"vpn_gateway","status":"blocked","severity":"high"}
{"timestamp":"2026-03-11T09:02:39Z","event_type":"policy_denied","source":"policy_engine","target":"iam_policy","status":"blocked","severity":"high"}
{"timestamp":"2026-03-11T09:02:52Z","event_type":"service_restart","source":"ops_1","target":"api_gateway","status":"ok","severity":"medium"}
{"timestamp":"2026-03-11T09:03:26Z","event_type":"malware_alert","source":"sensor_7","target":"host_77","status":"blocked","severity":"high"}
{"timestamp":"2026-03-11T09:03:31Z","event_type":"malware_alert","source":"sensor_7","target":"host_77","status":"blocked","severity":"high"}
{"timestamp":"2026-03-11T09:03:36Z","event_type":"malware_alert","source":"sensor_7","target":"host_77","status":"blocked","severity":"high"}
{"timestamp":"2026-03-11T09:03:42Z","event_type":"login_success","source":"user_m","target":"auth_service","status":"ok","severity":"low"}
{"timestamp":"2026-03-11T09:03:55Z","event_type":"health_check","source":"monitor","target":"api_gateway","status":"ok","severity":"low"}
44 changes: 43 additions & 1 deletion tests/test_pipeline_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from telemetry_window_demo.io import load_alert_table, load_config, load_feature_table


def test_default_pipeline_reproduces_sample_outputs(tmp_path, capsys) -> None:
def test_default_pipeline_reproduces_sample_outputs(tmp_path, capsys) -> None:
repo_root = Path(__file__).resolve().parents[1]
config_path = repo_root / "configs" / "default.yaml"
expected_output_dir = repo_root / "data" / "processed"
Expand Down Expand Up @@ -48,3 +48,45 @@ def test_default_pipeline_reproduces_sample_outputs(tmp_path, capsys) -> None:
stdout = capsys.readouterr().out
assert "[OK] Loaded 41 events" in stdout
assert "[OK] Triggered 12 alerts" in stdout


def test_richer_sample_pipeline_reproduces_sample_outputs(tmp_path, capsys) -> None:
repo_root = Path(__file__).resolve().parents[1]
config_path = repo_root / "configs" / "richer_sample.yaml"
expected_output_dir = repo_root / "data" / "processed" / "richer_sample"
generated_output_dir = tmp_path / "richer_sample"

config = load_config(config_path)
config["input_path"] = str(
(repo_root / "data" / "raw" / "richer_sample_events.jsonl").resolve()
)
config["output_dir"] = str(generated_output_dir.resolve())

temp_config_path = tmp_path / "richer_sample.yaml"
temp_config_path.write_text(
yaml.safe_dump(config, sort_keys=False),
encoding="utf-8",
)

run_command(Namespace(config=str(temp_config_path)))

generated_features = load_feature_table(generated_output_dir / "features.csv")
generated_alerts = load_alert_table(generated_output_dir / "alerts.csv")
expected_features = load_feature_table(expected_output_dir / "features.csv")
expected_alerts = load_alert_table(expected_output_dir / "alerts.csv")

assert len(generated_features) == 24
assert len(generated_alerts) == 8
pd.testing.assert_frame_equal(generated_features, expected_features)
pd.testing.assert_frame_equal(generated_alerts, expected_alerts)

for file_name in (
"event_count_timeline.png",
"error_rate_timeline.png",
"alerts_timeline.png",
):
assert (generated_output_dir / file_name).exists()

stdout = capsys.readouterr().out
assert "[OK] Loaded 28 events" in stdout
assert "[OK] Triggered 8 alerts" in stdout
Loading