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
80 changes: 80 additions & 0 deletions plugins/alerts/azure_alert_volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

// Fabricated AKS audit records run through the offline Azure parser model
// (azureParse) and the pinned SDK CEL. They pin the de-duplication keys that
// keep the admission webhook rule from alerting on every reconciliation patch
// the AKS control plane makes. The EventProcessor playground separately runs
// the real parser and alert plugins.
import (
"encoding/json"
"reflect"
"testing"

"github.com/threatwinds/go-sdk/plugins"
"github.com/tidwall/gjson"
)

func azureVolumeAudit(t *testing.T, user, verb, stage, resource, name string, code int) string {
t.Helper()
audit, err := json.Marshal(map[string]any{
"kind": "Event", "apiVersion": "audit.k8s.io/v1", "stage": stage, "verb": verb,
"user": map[string]any{"username": user}, "sourceIPs": []string{"198.51.100.4"},
"responseStatus": map[string]any{"code": code},
"objectRef": map[string]any{"resource": resource, "name": name, "apiGroup": "admissionregistration.k8s.io"},
"requestURI": "/apis/admissionregistration.k8s.io/v1/" + resource + "/" + name + "?fieldManager=example",
})
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(map[string]any{
"time": "2026-09-29T10:00:00Z", "tenantId": "directory-test", "category": "kube-audit-admin",
"resourceId": "/SUBSCRIPTIONS/00000000-0000-4000-8000-000000000000/RESOURCEGROUPS/RG-TEST/PROVIDERS/MICROSOFT.CONTAINERSERVICE/MANAGEDCLUSTERS/AKS-TEST",
"operationName": "Microsoft.ContainerService/managedClusters/diagnosticLogs/Read",
"properties": map[string]any{"log": string(audit)},
})
if err != nil {
t.Fatal(err)
}
return string(raw)
}

func TestAzureKubernetesWebhookAlertVolume(t *testing.T) {
cfg, cache := azureConfig(t), plugins.NewCELCache("azure-alert-volume")
r := azureRules(t)["azure_kubernetes_admission_controller"]
if r == nil {
t.Fatal("missing azure_kubernetes_admission_controller")
}
want := []string{"dataSource", "lastEvent.log.azureScope", "adversary.user"}
if 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)
}
for _, tc := range []struct {
name string
raw string
want bool
}{
{"control plane patch", azureVolumeAudit(t, "aksService", "patch", "ResponseComplete", "validatingwebhookconfigurations", "aks-node-validating-webhook", 200), true},
{"administrator create", azureVolumeAudit(t, "admin@example.test", "create", "ResponseComplete", "mutatingwebhookconfigurations", "inject-sidecar", 201), true},
{"read", azureVolumeAudit(t, "aksService", "get", "ResponseComplete", "validatingwebhookconfigurations", "aks-node-validating-webhook", 200), false},
{"request received", azureVolumeAudit(t, "admin@example.test", "create", "RequestReceived", "mutatingwebhookconfigurations", "inject-sidecar", 200), false},
{"denied", azureVolumeAudit(t, "admin@example.test", "patch", "ResponseComplete", "mutatingwebhookconfigurations", "inject-sidecar", 403), false},
{"other resource", azureVolumeAudit(t, "admin@example.test", "patch", "ResponseComplete", "configmaps", "settings", 200), false},
} {
t.Run(tc.name, func(t *testing.T) {
out := azureParse(t, cfg, tc.raw, "EventHub (test)", cache)
got, err := cache.Eval(r.Where, out)
if err != nil || got != tc.want {
t.Fatalf("where got %v (%v), want %v for %s", got, err, tc.want, out)
}
if !tc.want {
return
}
// adversary: origin, so adversary.user is the event's origin.user.
for key, path := range map[string]string{"dataSource": "dataSource", "lastEvent.log.azureScope": "log.azureScope", "adversary.user": "origin.user"} {
if v := gjson.Get(out, path); v.Type != gjson.String || v.String() == "" {
t.Fatalf("de-duplication key %s (%s) does not resolve to text in %s", key, path, out)
}
}
})
}
}
9 changes: 4 additions & 5 deletions rules/cloud/azure/azure_kubernetes_admission_controller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ references:
- https://attack.mitre.org/techniques/T1078/004/
description: 'Detects creation or modification of MutatingAdmissionWebhook or ValidatingAdmissionWebhook configurations in Azure
Kubernetes Service. Attackers use admission controllers to inject malicious containers or modify workload specifications.
The AKS control plane (aksService) patches its own webhooks every few minutes, so one alert is raised per cluster and
identity; repeats are suppressed for seven days.


Next Steps:
Expand All @@ -37,10 +39,7 @@ where: 'equalsIgnoreCase("log.azureKind","kubernetes") && equalsIgnoreCase("log.
"update", "patch"])

'
groupBy:
- lastEvent.dataSource
- lastEvent.log.azureScopeType
deduplicateBy:
- dataSource
- lastEvent.log.azureScope
- lastEvent.log.azureOperation
- adversary.user
- adversary.ip
Loading