Update to META-Data Only
All checks were successful
release-tag / release-image (push) Successful in 3m4s

This commit is contained in:
2026-07-18 17:16:10 +02:00
parent f1d88e8fb3
commit 10541fb22d
6 changed files with 584 additions and 321 deletions

View File

@@ -1,3 +1,71 @@
# SIEM Backend Metadata-first Variante
## Warum die Datenbank bisher wächst
Die ursprüngliche Implementierung speichert jedes Windows-Event zweimal: als breite normalisierte Zeile in `event_logs` und zusätzlich als vollständiges XML in `event_log_raw`. Außerdem enthielt die Partitionswartung einen Tabellennamenfehler (`event_logs_raw` statt `event_log_raw`). Der Wartungslauf brach deshalb ab, bevor alte Partitionen gelöscht wurden. Auch `event_count_buckets` und `ueba_context_buckets` waren nicht partitioniert und konnten unbegrenzt wachsen.
## Neues Speichermodell
- `event_logs`: kurzlebige Hot-Daten für Regeln, die eine genaue Reihenfolge einzelner Events benötigen. Standard: 72 Stunden.
- `event_occurrences`: langfristige Metadaten-Aggregate pro Zeit-Bucket, Host, Channel, Event-ID, Benutzer, IP und Workstation. Enthält `cnt`, `first_event_ts` und `last_event_ts`. Standard: 180 Tage.
- `event_catalog`: kleine, dauerhafte Landkarte des ersten und letzten Auftretens jeder Event-ID pro Host und Channel.
- `event_log_raw`: optionales Raw-XML. Standardmäßig deaktiviert; bei Aktivierung nur 24 Stunden Aufbewahrung.
- `event_count_buckets` und `ueba_context_buckets`: jetzt partitioniert und automatisch bereinigt.
Für einen User-Lockout (typischerweise Security Event 4740) kann die Anwendung damit direkt beantworten: wann er zuerst/zuletzt im Bucket auftrat, auf welchem Host, für welchen Benutzer und wie oft. Das vollständige XML ist dafür nicht erforderlich. `CallerComputerName` wird dabei als Gerät/Workstation übernommen.
## Bevorzugtes Metadaten-Ingest
Bestehende Collector dürfen weiterhin `msg` mit XML senden. Neue oder angepasste Collector können stattdessen direkt ein `meta`-Objekt senden; dann muss das Backend das XML weder speichern noch parsen:
```json
[
{
"host": "DC01.example.local",
"channel": "Security",
"id": 4740,
"source": "Microsoft-Windows-Security-Auditing",
"ts": "2026-07-18T12:34:56Z",
"meta": {
"target_user": "alice",
"subject_user": "DC01$",
"device": "CLIENT-42",
"provider": "Microsoft-Windows-Security-Auditing"
}
}
]
```
Mindestens `msg` oder `meta` ist erforderlich. Für Regeln, die auf `msg`/Volltext prüfen, muss Raw-XML weiterhin vom Collector geliefert werden; im reinen Metadatenmodus sollten solche Regeln auf strukturierte Felder umgestellt werden.
## Wichtige Einstellungen
```env
STORE_RAW_XML=false
METADATA_BUCKET=1m
EVENT_RETENTION=72h
RAW_RETENTION=24h
METADATA_RETENTION=4320h
PARTITION_MAINTENANCE_ENABLED=true
```
`METADATA_BUCKET=1m` ist ein guter Ausgangspunkt. Bei sehr hohen Raten kann auf `5m` erhöht werden; dadurch sinkt die Zeilenzahl weiter, die zeitliche Auflösung wird aber gröber.
## Bestehende Installation migrieren
1. Datenbank sichern und Ingest vorübergehend stoppen.
2. `deploy/mariadb/migrations/002-metadata-first.sql` in einem Wartungsfenster ausführen. Das Script wärmt `event_catalog` aus den kleineren Baseline-Buckets vor, damit bekannte Event-IDs nicht als neu alarmiert werden. Die beiden `ALTER TABLE ... PARTITION BY`-Operationen können große Tabellen neu aufbauen und sperren.
3. Neue Umgebungsvariablen aus `dot_env` übernehmen.
4. Backend aktualisieren und starten.
5. Im Log kontrollieren, dass `partition maintenance completed` erscheint. Die frühere Meldung zur nicht vorhandenen Tabelle `event_logs_raw` darf nicht mehr auftreten.
6. Nach Ablauf von `EVENT_RETENTION` prüfen, ob alte `event_logs`-Partitionen verschwinden. Raw-XML kann nach erfolgreicher Validierung separat gelöscht oder durch die kurze `RAW_RETENTION` automatisch entfernt werden. `event_occurrences` wird nicht rückwirkend aus der alten Volltabelle aufgebaut; die langfristige Metadatenhistorie beginnt mit dem neuen Backend.
## Datenbankwahl
MariaDB bleibt für dieses Modell sinnvoll: Die Abfragen sind überwiegend zeitbasierte Filter, gruppierte Zähler und kleine relationale Dimensionen. Ein Wechsel zu ClickHouse oder OpenSearch lohnt sich erst, wenn trotz Aggregation sehr hohe Eventraten, ad-hoc Volltextsuchen oder jahrelange Rohdatenhaltung erforderlich sind. Für die hier beschriebene Metadatenanforderung wäre ein sofortiger Engine-Wechsel zusätzlicher Betriebsaufwand ohne zwingenden Nutzen.
---
# SIEM-lite Admin-Handbuch: Anlernphase, manuelle Bewertung und Lerneffekt
Stand: 2026-04-27

View File

@@ -62,6 +62,17 @@ services:
NEW_SOURCE_IP_LOOKBACK: ${NEW_SOURCE_IP_LOOKBACK}
NEW_SOURCE_IP_WINDOW: ${NEW_SOURCE_IP_WINDOW}
DETECTIONS_LIMIT: ${DETECTIONS_LIMIT}
STORE_RAW_XML: ${STORE_RAW_XML}
METADATA_BUCKET: ${METADATA_BUCKET}
EVENT_RETENTION: ${EVENT_RETENTION}
RAW_RETENTION: ${RAW_RETENTION}
METADATA_RETENTION: ${METADATA_RETENTION}
PARTITION_MAINTENANCE_ENABLED: ${PARTITION_MAINTENANCE_ENABLED}
PARTITION_MAINTENANCE_INTERVAL: ${PARTITION_MAINTENANCE_INTERVAL}
PARTITION_INTERVAL: ${PARTITION_INTERVAL}
PARTITION_AHEAD: ${PARTITION_AHEAD}
PARTITION_BEHIND: ${PARTITION_BEHIND}
PARTITION_RETENTION: ${PARTITION_RETENTION}
TZ: ${TZ}
depends_on:
mariadb:

View File

@@ -22,6 +22,8 @@ SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS ensure_siem_partitions;
DROP TABLE IF EXISTS event_catalog;
DROP TABLE IF EXISTS event_occurrences;
DROP TABLE IF EXISTS event_count_buckets;
DROP TABLE IF EXISTS ueba_context_buckets;
DROP TABLE IF EXISTS host_risk_scores;
@@ -181,6 +183,63 @@ PARTITION BY RANGE COLUMNS(ts) (
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
-- ---------------------------------------------------------------------
-- Kleine Event-Landkarte: erstes/letztes Auftreten pro Host/Channel/EventID.
-- Verhindert teure NOT EXISTS-Scans über die Hot-Event-Tabelle.
-- ---------------------------------------------------------------------
CREATE TABLE event_catalog (
hostname VARCHAR(191) NOT NULL,
channel_name VARCHAR(128) NOT NULL,
event_id INT UNSIGNED NOT NULL,
first_seen DATETIME(6) NOT NULL,
last_seen DATETIME(6) NOT NULL,
total_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (hostname, channel_name, event_id),
KEY idx_event_catalog_first_seen (first_seen),
KEY idx_event_catalog_last_seen (last_seen)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- Langfristige, kompakte Event-Metadaten
-- Eine Zeile pro Zeit-Bucket und Dimensionskombination statt einer Zeile pro XML.
-- ---------------------------------------------------------------------
CREATE TABLE event_occurrences (
bucket_start DATETIME(6) NOT NULL,
bucket_end DATETIME(6) NOT NULL,
dimension_key BINARY(16) NOT NULL,
hostname VARCHAR(191) NOT NULL,
channel_name VARCHAR(128) NOT NULL,
event_id INT UNSIGNED NOT NULL,
provider_name VARCHAR(191) NOT NULL DEFAULT '',
target_user VARCHAR(191) NOT NULL DEFAULT '',
subject_user VARCHAR(191) NOT NULL DEFAULT '',
src_ip VARCHAR(64) NOT NULL DEFAULT '',
workstation VARCHAR(191) NOT NULL DEFAULT '',
logon_type VARCHAR(32) NOT NULL DEFAULT '',
status_text VARCHAR(128) NOT NULL DEFAULT '',
failure_reason VARCHAR(255) NOT NULL DEFAULT '',
cnt BIGINT UNSIGNED NOT NULL DEFAULT 0,
first_event_ts DATETIME(6) NOT NULL,
last_event_ts DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (bucket_start, dimension_key),
KEY idx_occurrences_time_host_event (bucket_start, hostname, channel_name, event_id),
KEY idx_occurrences_host_event_time (hostname, channel_name, event_id, bucket_start),
KEY idx_occurrences_target_user_time (target_user, bucket_start),
KEY idx_occurrences_subject_user_time (subject_user, bucket_start),
KEY idx_occurrences_src_ip_time (src_ip, bucket_start)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
PARTITION BY RANGE COLUMNS(bucket_start) (
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
-- ---------------------------------------------------------------------
-- Detection-Regeln
-- ---------------------------------------------------------------------
@@ -361,7 +420,10 @@ CREATE TABLE event_count_buckets (
bucket_start,
bucket_end
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
PARTITION BY RANGE COLUMNS(bucket_start) (
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
-- ---------------------------------------------------------------------
-- Baseline Stats
@@ -518,7 +580,10 @@ CREATE TABLE ueba_context_buckets (
workstation,
bucket_start
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
PARTITION BY RANGE COLUMNS(bucket_start) (
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
-- ---------------------------------------------------------------------
-- Privileged Users

18
dot_env
View File

@@ -50,4 +50,20 @@ BASELINE_SUPPRESS_FOR=1h
#BASELINE_MIN_SAMPLES=84
#BASELINE_MEDIUM_Z=3.0
#BASELINE_HIGH_Z=5.0
#BASELINE_MIN_COUNT=20
#BASELINE_MIN_COUNT=20
# Metadata-first storage
# Raw XML is disabled by default. Enable only for short forensic retention.
STORE_RAW_XML=false
METADATA_BUCKET=1m
EVENT_RETENTION=72h
RAW_RETENTION=24h
METADATA_RETENTION=4320h
# Partition maintenance
PARTITION_MAINTENANCE_ENABLED=true
PARTITION_MAINTENANCE_INTERVAL=15m
PARTITION_INTERVAL=3h
PARTITION_AHEAD=24h
PARTITION_BEHIND=6h
# Compatibility fallback; table-specific retention values above take precedence.
PARTITION_RETENTION=720h

627
main.go

File diff suppressed because it is too large Load Diff

View File

@@ -1,101 +1,11 @@
CREATE DATABASE IF NOT EXISTS eventcollector
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE eventcollector;
CREATE TABLE IF NOT EXISTS agents (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
hostname VARCHAR(255) NOT NULL,
api_key_hash CHAR(64) NOT NULL,
first_seen DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
last_seen DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
last_ip VARCHAR(64) NOT NULL DEFAULT '',
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY ux_agents_hostname (hostname),
KEY ix_agents_last_seen (last_seen)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS event_logs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
agent_id BIGINT UNSIGNED NOT NULL,
hostname VARCHAR(255) NOT NULL,
channel_name VARCHAR(128) NOT NULL,
event_id INT UNSIGNED NOT NULL,
source VARCHAR(255) NOT NULL,
ts DATETIME(6) NOT NULL,
received_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
msg LONGTEXT NOT NULL,
msg_sha256 CHAR(64) NOT NULL,
PRIMARY KEY (id),
KEY ix_event_logs_ts (ts),
KEY ix_event_logs_received_at (received_at),
KEY ix_event_logs_agent_ts (agent_id, ts),
KEY ix_event_logs_eventid_ts (event_id, ts),
KEY ix_event_logs_hostname_ts (hostname, ts),
KEY ix_event_logs_channel_event_ts (channel_name, event_id, ts),
CONSTRAINT fk_event_logs_agent
FOREIGN KEY (agent_id) REFERENCES agents(id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS detections (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
rule_name VARCHAR(128) NOT NULL,
severity VARCHAR(32) NOT NULL,
hostname VARCHAR(255) NOT NULL,
channel_name VARCHAR(128) NOT NULL DEFAULT '',
event_id INT UNSIGNED NOT NULL DEFAULT 0,
score DOUBLE NOT NULL DEFAULT 0,
window_start DATETIME(6) NOT NULL,
window_end DATETIME(6) NOT NULL,
summary VARCHAR(512) NOT NULL,
details_json JSON NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY ux_detection_dedupe (rule_name, hostname, channel_name, event_id, window_start, window_end),
KEY ix_detections_created (created_at),
KEY ix_detections_rule_host_time (rule_name, hostname, created_at),
KEY ix_detections_severity_time (severity, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
USE eventcollector;
INSERT INTO agents (hostname, api_key_hash)
VALUES
('client01.domain.local', SHA2('SUPER-LANGER-AGENT-KEY-01', 256)),
('client02.domain.local', SHA2('SUPER-LANGER-AGENT-KEY-02', 256));
#V2
ALTER TABLE event_logs
ADD COLUMN computer VARCHAR(255) NOT NULL DEFAULT '' AFTER source,
ADD COLUMN provider_name VARCHAR(255) NOT NULL DEFAULT '' AFTER computer,
ADD COLUMN level_value INT UNSIGNED NOT NULL DEFAULT 0 AFTER provider_name,
ADD COLUMN task_value INT UNSIGNED NOT NULL DEFAULT 0 AFTER level_value,
ADD COLUMN opcode_value INT UNSIGNED NOT NULL DEFAULT 0 AFTER task_value,
ADD COLUMN keywords VARCHAR(255) NOT NULL DEFAULT '' AFTER opcode_value,
ADD COLUMN target_user VARCHAR(255) NOT NULL DEFAULT '' AFTER keywords,
ADD COLUMN target_domain VARCHAR(255) NOT NULL DEFAULT '' AFTER target_user,
ADD COLUMN subject_user VARCHAR(255) NOT NULL DEFAULT '' AFTER target_domain,
ADD COLUMN subject_domain VARCHAR(255) NOT NULL DEFAULT '' AFTER subject_user,
ADD COLUMN workstation VARCHAR(255) NOT NULL DEFAULT '' AFTER subject_domain,
ADD COLUMN src_ip VARCHAR(64) NOT NULL DEFAULT '' AFTER workstation,
ADD COLUMN src_port VARCHAR(32) NOT NULL DEFAULT '' AFTER src_ip,
ADD COLUMN logon_type VARCHAR(32) NOT NULL DEFAULT '' AFTER src_port,
ADD COLUMN process_name VARCHAR(512) NOT NULL DEFAULT '' AFTER logon_type,
ADD COLUMN authentication_package VARCHAR(128) NOT NULL DEFAULT '' AFTER process_name,
ADD COLUMN logon_process VARCHAR(128) NOT NULL DEFAULT '' AFTER authentication_package,
ADD COLUMN status_text VARCHAR(64) NOT NULL DEFAULT '' AFTER logon_process,
ADD COLUMN sub_status_text VARCHAR(64) NOT NULL DEFAULT '' AFTER status_text,
ADD COLUMN failure_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER sub_status_text;
ALTER TABLE event_logs
ADD KEY ix_event_logs_target_user_ts (target_user, ts),
ADD KEY ix_event_logs_src_ip_ts (src_ip, ts),
ADD KEY ix_event_logs_target_user_src_ip_ts (target_user, src_ip, ts),
ADD KEY ix_event_logs_eventid_srcip_ts (event_id, src_ip, ts),
ADD KEY ix_event_logs_eventid_targetuser_ts (event_id, target_user, ts),
ADD KEY ix_event_logs_eventid_logontype_ts (event_id, logon_type, ts);
-- Canonical schema notice
--
-- New empty database:
-- deploy/mariadb/init/001-schema.sql
--
-- Existing database:
-- deploy/mariadb/migrations/002-metadata-first.sql
--
-- The former schema.sql was an obsolete, non-idempotent development schema and
-- has been retained as schema.legacy.sql only for historical reference. Do not
-- run schema.legacy.sql against a production database.