From e98b25595a1b783d87a7359c0c85377e51ab805c Mon Sep 17 00:00:00 2001 From: Ricardo Valdes Date: Tue, 29 Sep 2026 19:07:34 -0400 Subject: [PATCH] fix(linux): one logging-service alert per host "Audit or Logging Service Disabled" grouped by origin.host and origin.user, which an alert never carries (the alert holds the event's origin side as adversary). Grouping keys that do not resolve are skipped, so every matching record opened a new top-level alert. The rule now raises one alert per host (dataSource) and drops repeats for seven days. Its condition is unchanged. linux_alert_volume_test.go pins the key and checks that it resolves on fabricated journald records. Co-Authored-By: Claude Opus 5.5 --- plugins/alerts/linux_alert_volume_test.go | 58 +++++++++++++++++++ .../debian_family/auditd_syslog_disabling.yml | 9 ++- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 plugins/alerts/linux_alert_volume_test.go diff --git a/plugins/alerts/linux_alert_volume_test.go b/plugins/alerts/linux_alert_volume_test.go new file mode 100644 index 000000000..4de14c7c1 --- /dev/null +++ b/plugins/alerts/linux_alert_volume_test.go @@ -0,0 +1,58 @@ +package main + +// Fabricated journald records run through the Linux raw model and the pinned +// SDK CEL. They pin the de-duplication key that keeps "Audit or Logging Service +// Disabled" from opening a new alert for every matching record: its groupBy +// named origin.* fields, which an alert never carries. The EventProcessor +// playground separately runs the real parser and alert plugins. +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "github.com/tidwall/gjson" + "google.golang.org/protobuf/encoding/protojson" +) + +func TestLinuxLoggingServiceAlertVolume(t *testing.T) { + blob, err := utils.ReadPbYaml("../../rules/linux/debian_family/auditd_syslog_disabling.yml") + if err != nil { + t.Fatal(err) + } + r := new(plugins.Rule) + if err = protojson.Unmarshal(blob, r); err != nil { + t.Fatal(err) + } + r.Normalize() + if want := []string{"dataSource"}; len(r.GroupBy) != 0 || !reflect.DeepEqual(r.DeduplicateBy, want) { + t.Fatalf("grouping got groupBy %v deduplicateBy %v, want deduplicateBy %v", r.GroupBy, r.DeduplicateBy, want) + } + cache := plugins.NewCELCache("linux-alert-volume") + for _, tc := range []struct { + message string + want bool + }{ + {"systemctl stop auditd", true}, + {"systemctl disable rsyslog.service", true}, + {"systemctl mask systemd-journald.service", true}, + {"systemctl restart rsyslog.service", false}, + {"Started Session 42 of User reviewer.", false}, + } { + t.Run(tc.message, func(t *testing.T) { + raw, err := json.Marshal(map[string]any{"MESSAGE": tc.message, "SYSLOG_IDENTIFIER": "sudo", "_HOSTNAME": "web01"}) + if err != nil { + t.Fatal(err) + } + event := linuxActionResultNormalize(t, string(raw)) + got, err := cache.Eval(r.Where, event) + if err != nil || got != tc.want { + t.Fatalf("where got %v (%v), want %v for %s", got, err, tc.want, event) + } + if got && gjson.Get(event, "dataSource").String() == "" { + t.Fatalf("de-duplication key dataSource does not resolve in %s", event) + } + }) + } +} diff --git a/rules/linux/debian_family/auditd_syslog_disabling.yml b/rules/linux/debian_family/auditd_syslog_disabling.yml index b1dc69f1d..c371e5552 100644 --- a/rules/linux/debian_family/auditd_syslog_disabling.yml +++ b/rules/linux/debian_family/auditd_syslog_disabling.yml @@ -1,4 +1,4 @@ -# Rule version v1.0.0 +# Rule version v1.1.0 dataTypes: - linux @@ -13,7 +13,7 @@ adversary: origin references: - https://attack.mitre.org/techniques/T1562/001/ description: | - Detects attempts to stop or disable audit and logging services (auditd, rsyslog, syslog-ng, journald) which attackers do to prevent their activities from being recorded. + Detects attempts to stop or disable audit and logging services (auditd, rsyslog, syslog-ng, journald) which attackers do to prevent their activities from being recorded. One alert is raised per host; repeats are suppressed for seven days. Next Steps: 1. Immediately investigate the host where logging was disabled @@ -30,6 +30,5 @@ where: | contains("log.message", "syslog-ng") || contains("log.message", "journald") || contains("log.message", "syslog")) && !(contains("log.message", "restart") || contains("log.message", "reload")) -groupBy: - - origin.host - - origin.user +deduplicateBy: + - dataSource