diff --git a/README.md b/README.md index 830c8b3..ef47257 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/configs/richer_sample.yaml b/configs/richer_sample.yaml new file mode 100644 index 0000000..387d79b --- /dev/null +++ b/configs/richer_sample.yaml @@ -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 diff --git a/data/processed/richer_sample/alerts.csv b/data/processed/richer_sample/alerts.csv new file mode 100644 index 0000000..661c64b --- /dev/null +++ b/data/processed/richer_sample/alerts.csv @@ -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 diff --git a/data/processed/richer_sample/alerts_timeline.png b/data/processed/richer_sample/alerts_timeline.png new file mode 100644 index 0000000..74ca96e Binary files /dev/null and b/data/processed/richer_sample/alerts_timeline.png differ diff --git a/data/processed/richer_sample/error_rate_timeline.png b/data/processed/richer_sample/error_rate_timeline.png new file mode 100644 index 0000000..4a63ef2 Binary files /dev/null and b/data/processed/richer_sample/error_rate_timeline.png differ diff --git a/data/processed/richer_sample/event_count_timeline.png b/data/processed/richer_sample/event_count_timeline.png new file mode 100644 index 0000000..17b3d51 Binary files /dev/null and b/data/processed/richer_sample/event_count_timeline.png differ diff --git a/data/processed/richer_sample/features.csv b/data/processed/richer_sample/features.csv new file mode 100644 index 0000000..8d4e05c --- /dev/null +++ b/data/processed/richer_sample/features.csv @@ -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 diff --git a/data/raw/richer_sample_events.jsonl b/data/raw/richer_sample_events.jsonl new file mode 100644 index 0000000..d1d2b86 --- /dev/null +++ b/data/raw/richer_sample_events.jsonl @@ -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"} diff --git a/tests/test_pipeline_e2e.py b/tests/test_pipeline_e2e.py index de595fe..d36792e 100644 --- a/tests/test_pipeline_e2e.py +++ b/tests/test_pipeline_e2e.py @@ -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" @@ -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