diff --git a/config/milo/activity/policies/backendtlspolicy-policy.yaml b/config/milo/activity/policies/backendtlspolicy-policy.yaml index 2fa19917..c7dd0790 100644 --- a/config/milo/activity/policies/backendtlspolicy-policy.yaml +++ b/config/milo/activity/policies/backendtlspolicy-policy.yaml @@ -19,30 +19,30 @@ spec: auditRules: # BackendTLSPolicy creation with spec available - name: create - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created backend TLS policy {{ link(audit.responseObject.metadata.name, audit.objectRef) }}" # BackendTLSPolicy creation fallback (no spec) - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('a backend TLS policy', audit.objectRef) }}" # BackendTLSPolicy deletion with responseObject.spec available (response contains the deleted resource) - name: delete - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec)" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted backend TLS policy {{ audit.objectRef.name }}" # BackendTLSPolicy deletion fallback (no spec on response) - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted a backend TLS policy" # BackendTLSPolicy update with spec available - excludes status subresource - name: update - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated backend TLS policy {{ link(audit.objectRef.name, audit.objectRef) }}" # BackendTLSPolicy update fallback (no spec) - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('a backend TLS policy', audit.objectRef) }}" diff --git a/config/milo/activity/policies/connector-policy.yaml b/config/milo/activity/policies/connector-policy.yaml index 74efefb3..702c8503 100644 --- a/config/milo/activity/policies/connector-policy.yaml +++ b/config/milo/activity/policies/connector-policy.yaml @@ -19,30 +19,30 @@ spec: auditRules: # Connector creation with spec available - name: create - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created connector {{ link(audit.responseObject.metadata.name, audit.objectRef) }}" # Connector creation fallback (no spec) - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('a connector', audit.objectRef) }}" # Connector deletion with responseObject.spec available (response contains the deleted resource) - name: delete - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec)" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted connector {{ audit.objectRef.name }}" # Connector deletion fallback (no spec on response) - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted a connector" # Connector update with spec available - excludes status subresource - name: update - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated connector {{ link(audit.objectRef.name, audit.objectRef) }}" # Connector update fallback (no spec) - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('a connector', audit.objectRef) }}" diff --git a/config/milo/activity/policies/connectoradvertisement-policy.yaml b/config/milo/activity/policies/connectoradvertisement-policy.yaml index 3006f273..2e86a260 100644 --- a/config/milo/activity/policies/connectoradvertisement-policy.yaml +++ b/config/milo/activity/policies/connectoradvertisement-policy.yaml @@ -20,30 +20,30 @@ spec: auditRules: # ConnectorAdvertisement creation with spec available - name: create - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created connector advertisement {{ link(audit.responseObject.metadata.name, audit.objectRef) }}" # ConnectorAdvertisement creation fallback (no spec) - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('a connector advertisement', audit.objectRef) }}" # ConnectorAdvertisement deletion with responseObject.spec available (response contains the deleted resource) - name: delete - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec)" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted connector advertisement {{ audit.objectRef.name }}" # ConnectorAdvertisement deletion fallback (no spec on response) - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted a connector advertisement" # ConnectorAdvertisement update with spec available - excludes status subresource - name: update - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated connector advertisement {{ link(audit.objectRef.name, audit.objectRef) }}" # ConnectorAdvertisement update fallback (no spec) - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('a connector advertisement', audit.objectRef) }}" diff --git a/config/milo/activity/policies/domain-policy.yaml b/config/milo/activity/policies/domain-policy.yaml index a734a2de..0cf7ea5f 100644 --- a/config/milo/activity/policies/domain-policy.yaml +++ b/config/milo/activity/policies/domain-policy.yaml @@ -20,30 +20,30 @@ spec: auditRules: # Domain creation with spec.domainName available - use domain name as display text - name: create - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created domain {{ link(audit.responseObject.spec.domainName, audit.objectRef) }}" # Domain creation fallback (no spec) - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('a domain', audit.objectRef) }}" # Domain deletion with responseObject.spec available (response contains the deleted resource) - name: delete - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec)" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted domain {{ audit.responseObject.spec.domainName }}" # Domain deletion fallback (no spec on response) - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted a domain" # Domain update with spec available - excludes status subresource - name: update - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated domain {{ link(audit.responseObject.spec.domainName, audit.objectRef) }}" # Domain update fallback (no spec) - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('a domain', audit.objectRef) }}" diff --git a/config/milo/activity/policies/gateway-policy.yaml b/config/milo/activity/policies/gateway-policy.yaml index a10bc5cc..779ff47a 100644 --- a/config/milo/activity/policies/gateway-policy.yaml +++ b/config/milo/activity/policies/gateway-policy.yaml @@ -19,30 +19,30 @@ spec: auditRules: # Gateway creation with spec available - name: create - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created gateway {{ link(audit.responseObject.metadata.name, audit.objectRef) }}" # Gateway creation fallback (no spec) - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('a gateway', audit.objectRef) }}" # Gateway deletion with responseObject.spec available (response contains the deleted resource) - name: delete - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec)" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted gateway {{ audit.objectRef.name }}" # Gateway deletion fallback (no spec on response) - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted a gateway" # Gateway update with spec available - excludes status subresource - name: update - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated gateway {{ link(audit.objectRef.name, audit.objectRef) }}" # Gateway update fallback (no spec) - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('a gateway', audit.objectRef) }}" diff --git a/config/milo/activity/policies/httpproxy-policy.yaml b/config/milo/activity/policies/httpproxy-policy.yaml index 6f1bae67..ef54f9fd 100644 --- a/config/milo/activity/policies/httpproxy-policy.yaml +++ b/config/milo/activity/policies/httpproxy-policy.yaml @@ -20,99 +20,99 @@ spec: auditRules: - name: create-annotated-backend - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/display-value' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/display-value'] != '' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/display-value' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/display-value'] != '' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created load balancer {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }} pointing to {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-value'] }}" - name: create-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created load balancer {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: create-name-backend - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.objectRef.name) && has(audit.requestObject.spec.rules) && size(audit.requestObject.spec.rules) > 0 && has(audit.requestObject.spec.rules[0].backends) && size(audit.requestObject.spec.rules[0].backends) > 0 && has(audit.requestObject.spec.rules[0].backends[0].endpoint) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.objectRef.name) && has(audit.requestObject.spec.rules) && size(audit.requestObject.spec.rules) > 0 && has(audit.requestObject.spec.rules[0].backends) && size(audit.requestObject.spec.rules[0].backends) > 0 && has(audit.requestObject.spec.rules[0].backends[0].endpoint) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created load balancer {{ link(audit.objectRef.name, audit.objectRef) }} pointing to {{ audit.requestObject.spec.rules[0].backends[0].endpoint }}" - name: create-name - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.objectRef.name) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.objectRef.name) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created load balancer {{ link(audit.objectRef.name, audit.objectRef) }}" - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('a load balancer', audit.objectRef) }}" - name: delete-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted load balancer {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'] }}" - name: delete-name - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && has(audit.objectRef.name)" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && has(audit.objectRef.name) && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted load balancer {{ audit.objectRef.name }}" - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted a load balancer" - name: update-hostname-added - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'hostname' && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'hostname' && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} added a custom hostname {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-name'] }} to {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-hostname-removed - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'hostname' && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'hostname' && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} removed hostname {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-name'] }} from {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'] }}" - name: update-backend - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'backend' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'backend' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} changed the origin of {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }} to {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-value'] }}" - name: update-force-https-enabled - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'force-https' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'force-https' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled Force HTTPS on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-force-https-disabled - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'force-https' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'force-https' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} disabled Force HTTPS on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-display-name - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'display-name' && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'display-name' && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} renamed {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-name'] }} to {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/activity-value'], audit.objectRef) }}" - name: update-host-header-added - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'host-header' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'host-header' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} added a host header {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-value'] }} to {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-host-header-removed - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'host-header' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'host-header' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} removed the host header {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-name'] }} from {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'] }}" - name: update-host-header - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'host-header' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'host-header' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} changed the host header on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }} to {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-value'] }}" - name: update-rule - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'rule' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'rule' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated routing on load balancer {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-health-check-enabled - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'health-check' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'added' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'health-check' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled passive health checks on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-health-check-disabled - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'health-check' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-change' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-change'] == 'removed' && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'health-check' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} disabled passive health checks on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-health-check - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'health-check' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'health-check' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated passive health checks on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated load balancer {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-name - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.objectRef.name) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.objectRef.name) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated load balancer {{ link(audit.objectRef.name, audit.objectRef) }}" - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('a load balancer', audit.objectRef) }}" eventRules: diff --git a/config/milo/activity/policies/httproute-policy.yaml b/config/milo/activity/policies/httproute-policy.yaml index 3046fdfb..0af5f771 100644 --- a/config/milo/activity/policies/httproute-policy.yaml +++ b/config/milo/activity/policies/httproute-policy.yaml @@ -19,30 +19,30 @@ spec: auditRules: # HTTPRoute creation with spec.hostnames available - use first hostname as display text - name: create - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.hostnames) && size(audit.requestObject.spec.hostnames) > 0 && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.hostnames) && size(audit.requestObject.spec.hostnames) > 0 && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created HTTP route {{ link(string(audit.requestObject.spec.hostnames[0]), audit.objectRef) }}" # HTTPRoute creation fallback (no spec or no hostnames) - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} created {{ link('an HTTP route', audit.objectRef) }}" # HTTPRoute deletion with responseObject.spec.hostnames available - name: delete - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseObject.spec.hostnames) && size(audit.responseObject.spec.hostnames) > 0" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.spec) && has(audit.responseObject.spec.hostnames) && size(audit.responseObject.spec.hostnames) > 0 && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted HTTP route {{ string(audit.responseObject.spec.hostnames[0]) }}" # HTTPRoute deletion fallback (no spec on response) - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} deleted an HTTP route" # HTTPRoute update with spec.hostnames available - excludes status subresource - name: update - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.requestObject.spec.hostnames) && size(audit.requestObject.spec.hostnames) > 0 && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.requestObject.spec.hostnames) && size(audit.requestObject.spec.hostnames) > 0 && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated HTTP route {{ link(string(audit.requestObject.spec.hostnames[0]), audit.objectRef) }}" # HTTPRoute update fallback (no spec or no hostnames) - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('an HTTP route', audit.objectRef) }}" diff --git a/config/milo/activity/policies/securitypolicy-policy.yaml b/config/milo/activity/policies/securitypolicy-policy.yaml index 6566ef01..80ff0d0f 100644 --- a/config/milo/activity/policies/securitypolicy-policy.yaml +++ b/config/milo/activity/policies/securitypolicy-policy.yaml @@ -17,29 +17,29 @@ spec: auditRules: - name: create-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.basicAuth) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.basicAuth) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled basic authentication on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: create-basic-auth - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.basicAuth) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.basicAuth) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled basic authentication on {{ link(audit.objectRef.name, audit.objectRef) }}" - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled {{ link('basic authentication', audit.objectRef) }}" - name: delete-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} removed basic authentication from {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'] }}" - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} removed basic authentication" - name: update-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated basic authentication on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated basic authentication on {{ link(audit.objectRef.name, audit.objectRef) }}" diff --git a/config/milo/activity/policies/trafficprotectionpolicy-policy.yaml b/config/milo/activity/policies/trafficprotectionpolicy-policy.yaml index c82ea933..b486ee10 100644 --- a/config/milo/activity/policies/trafficprotectionpolicy-policy.yaml +++ b/config/milo/activity/policies/trafficprotectionpolicy-policy.yaml @@ -20,47 +20,47 @@ spec: auditRules: - name: create-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/display-value' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/display-value' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled traffic protection on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }} in {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-value'] }} mode" - name: create-mode - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.mode) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.requestObject.spec) && has(audit.requestObject.spec.mode) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled traffic protection {{ link(audit.objectRef.name, audit.objectRef) }} in {{ audit.requestObject.spec.mode }} mode" - name: create-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'create' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} enabled {{ link('traffic protection', audit.objectRef) }}" - name: delete-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} removed traffic protection from {{ audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'] }}" - name: delete-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete'" + match: "!audit.user.username.startsWith('system:') && audit.verb == 'delete' && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} removed traffic protection" - name: update-mode - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'mode' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'mode' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && 'networking.datumapis.com/activity-value' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} changed traffic protection on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }} from {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-value'] }}" - name: update-sampling - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'sampling' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'sampling' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} changed traffic protection sampling on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }} to {{ audit.responseObject.metadata.annotations['networking.datumapis.com/activity-value'] }}" - name: update-exclusions - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'exclusions' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/activity-field' in audit.responseObject.metadata.annotations && audit.responseObject.metadata.annotations['networking.datumapis.com/activity-field'] == 'exclusions' && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated traffic protection exclusions on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-annotated - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseObject) && type(audit.responseObject) == map && has(audit.responseObject.metadata) && has(audit.responseObject.metadata.annotations) && 'networking.datumapis.com/display-name' in audit.responseObject.metadata.annotations && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated traffic protection on {{ link(audit.responseObject.metadata.annotations['networking.datumapis.com/display-name'], audit.objectRef) }}" - name: update-mode-spec - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.requestObject.spec.mode) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map && has(audit.requestObject.spec)) && has(audit.requestObject.spec.mode) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated traffic protection {{ link(audit.objectRef.name, audit.objectRef) }} to {{ audit.requestObject.spec.mode }} mode" - name: update-fallback - match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300" + match: "!audit.user.username.startsWith('system:') && audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && (type(audit.requestObject) == map ? has(audit.requestObject.spec) : type(audit.requestObject) == list && audit.requestObject.exists(patch, patch.op in ['add', 'remove', 'replace', 'copy', 'move'] && (patch.path == '' || patch.path == '/spec' || patch.path.startsWith('/spec/') || (patch.op == 'move' && (patch.from == '/spec' || patch.from.startsWith('/spec/')))))) && has(audit.responseStatus.code) && audit.responseStatus.code >= 200 && audit.responseStatus.code < 300 && !(has(audit.requestURI) && audit.requestURI.contains('dryRun='))" summary: "{{ actor }} updated {{ link('traffic protection', audit.objectRef) }}" eventRules: diff --git a/internal/activitypolicy/securitypolicy_policy_test.go b/internal/activitypolicy/securitypolicy_policy_test.go index 8d6df7d1..a6155a8f 100644 --- a/internal/activitypolicy/securitypolicy_policy_test.go +++ b/internal/activitypolicy/securitypolicy_policy_test.go @@ -35,8 +35,9 @@ func TestSecurityPolicyPolicy_Fixtures(t *testing.T) { name: "delete annotated", wantRule: "delete-annotated", audit: map[string]any{ - "user": map[string]any{"username": "alice@example.com"}, - "verb": "delete", + "user": map[string]any{"username": "alice@example.com"}, + "verb": "delete", + "responseStatus": map[string]any{"code": 200}, "responseObject": map[string]any{"metadata": map[string]any{"annotations": map[string]any{ "networking.datumapis.com/display-name": "alb", }}}, diff --git a/internal/activitypolicy/tpp_policy_test.go b/internal/activitypolicy/tpp_policy_test.go index c6801d06..b46e66cc 100644 --- a/internal/activitypolicy/tpp_policy_test.go +++ b/internal/activitypolicy/tpp_policy_test.go @@ -52,8 +52,9 @@ func TestTPPPolicy_Fixtures(t *testing.T) { name: "delete annotated", wantRule: "delete-annotated", audit: map[string]any{ - "user": map[string]any{"username": "alice@example.com"}, - "verb": "delete", + "user": map[string]any{"username": "alice@example.com"}, + "verb": "delete", + "responseStatus": map[string]any{"code": 200}, "responseObject": map[string]any{"metadata": map[string]any{"annotations": map[string]any{ "networking.datumapis.com/display-name": "alb", }}}, diff --git a/test/activitypolicy/policies_test.go b/test/activitypolicy/policies_test.go index 9ecbc198..2cbeaf3f 100644 --- a/test/activitypolicy/policies_test.go +++ b/test/activitypolicy/policies_test.go @@ -12,13 +12,14 @@ import ( ) // These tests guard the ActivityPolicy audit rules under -// config/milo/activity/policies against a single defect with two symptoms: -// create/update rules that fire on FAILED (non-2xx) requests. On a rejected -// request the audit responseObject is a metav1.Status, so a summary that -// dereferences audit.responseObject. throws and the event is lost to the -// DLQ (DLQSlowLeak); the same rule also emits a false "created"/"updated" -// activity for an attempt that never succeeded. The fix gates every create and -// update rule's match on audit.responseStatus.code in [200,300). +// config/milo/activity/policies against write rules that fire on requests that +// changed nothing. On a rejected request the audit responseObject is a +// metav1.Status, so a summary that dereferences audit.responseObject. +// throws and the event is lost to the DLQ (DLQSlowLeak); the same rule also +// emits a false "created"/"updated"/"deleted" activity for an attempt that +// never succeeded. A dry run (?dryRun=All) succeeds but persists nothing. Every +// create, update and delete rule's match therefore gates on +// audit.responseStatus.code in [200,300) and on the request not being a dry run. const policiesGlob = "../../config/milo/activity/policies/*-policy.yaml" @@ -64,13 +65,17 @@ func verbOf(match string) string { } func gatesOn2xx(match string) bool { - return strings.Contains(match, "audit.responseStatus.code >= 200") && + return strings.Contains(match, "has(audit.responseStatus.code) && audit.responseStatus.code >= 200") && strings.Contains(match, "audit.responseStatus.code < 300") } +func skipsDryRun(match string) bool { + return strings.Contains(match, "audit.requestURI.contains('dryRun=')") +} + func newEnv(t *testing.T) *cel.Env { t.Helper() - env, err := cel.NewEnv(cel.Variable("audit", cel.DynType)) + env, err := cel.NewEnv(cel.Variable("audit", cel.MapType(cel.StringType, cel.DynType))) if err != nil { t.Fatalf("cel env: %v", err) } @@ -96,7 +101,7 @@ func evalMatch(t *testing.T, env *cel.Env, match string, audit map[string]any) b if err != nil { t.Fatalf("program %q: %v", match, err) } - out, _, err := prg.Eval(map[string]any{"audit": audit}) + out, _, err := prg.Eval(map[string]any{"audit": withDefaults(audit)}) if err != nil { t.Fatalf("eval %q: %v", match, err) } @@ -112,6 +117,17 @@ func evalMatch(t *testing.T, env *cel.Env, match string, audit map[string]any) b // carries a metav1.Status (no metadata.name, no spec), as the API server sends // on a rejected write. func auditEvent(verb string, code int) map[string]any { + return auditEventURI(verb, code, objectURI) +} + +const objectURI = "/apis/networking.datumapis.com/v1alpha/namespaces/default/objects/obj-1" + +// dryRunEvent is a successful request made with ?dryRun=All. +func dryRunEvent(verb string) map[string]any { + return auditEventURI(verb, successCodeFor(verb), objectURI+"?dryRun=All&fieldManager=kubectl-client-side-apply") +} + +func auditEventURI(verb string, code int, uri string) map[string]any { var responseObject map[string]any if code >= 200 && code < 300 { responseObject = map[string]any{ @@ -147,6 +163,7 @@ func auditEvent(verb string, code int) map[string]any { "responseObject": responseObject, "objectRef": map[string]any{"name": "obj-1"}, "responseStatus": map[string]any{"code": code}, + "requestURI": uri, } } @@ -164,12 +181,12 @@ func successCodeFor(verb string) int { return 200 } -// Structural guard: every create/update rule must gate on a 2xx response. -func TestCreateUpdateRulesGateOn2xx(t *testing.T) { +// Structural guard: every write rule must gate on a 2xx response and skip dry runs. +func TestWriteRulesGateOnOutcome(t *testing.T) { for _, pol := range loadPolicies(t) { for _, r := range pol.Spec.AuditRules { v := verbOf(r.Match) - if v != "create" && v != "update" { + if v == "other" { continue } t.Run(pol.Name+"/"+r.Name, func(t *testing.T) { @@ -177,26 +194,35 @@ func TestCreateUpdateRulesGateOn2xx(t *testing.T) { t.Errorf("%s rule %q (%s) is not gated on a 2xx response:\n %s", pol.Name, r.Name, v, r.Match) } + if !skipsDryRun(r.Match) { + t.Errorf("%s rule %q (%s) does not skip dry-run requests:\n %s", + pol.Name, r.Name, v, r.Match) + } }) } } } -// Semantic guard: create/update rules must NOT match a failed request, and MUST -// still match the successful one — the two properties that fix the DLQ leak and -// the false-activity emission together. -func TestCreateUpdateRulesFireOnlyOnSuccess(t *testing.T) { +// Semantic guard: write rules must NOT match a failed or dry-run request, and +// MUST still match the successful one — the properties that fix the DLQ leak +// and the false-activity emission together. +func TestWriteRulesFireOnlyOnSuccess(t *testing.T) { env := newEnv(t) for _, pol := range loadPolicies(t) { for _, r := range pol.Spec.AuditRules { v := verbOf(r.Match) - if v != "create" && v != "update" { + if v == "other" { continue } t.Run(pol.Name+"/"+r.Name, func(t *testing.T) { - if got := evalMatch(t, env, r.Match, auditEvent(v, failCodeFor(v))); got { - t.Errorf("%s rule %q matched a failed %s (code %d); it would DLQ / emit a false activity", - pol.Name, r.Name, v, failCodeFor(v)) + for _, code := range []int{failCodeFor(v), 404, 422, 500} { + if got := evalMatch(t, env, r.Match, auditEvent(v, code)); got { + t.Errorf("%s rule %q matched a failed %s (code %d); it would DLQ / emit a false activity", + pol.Name, r.Name, v, code) + } + } + if got := evalMatch(t, env, r.Match, dryRunEvent(v)); got { + t.Errorf("%s rule %q matched a dry-run %s; nothing was persisted", pol.Name, r.Name, v) } if got := evalMatch(t, env, r.Match, auditEvent(v, successCodeFor(v))); !got { if strings.Contains(r.Match, "metadata.annotations") { @@ -235,3 +261,18 @@ func TestAllMatchesCompile(t *testing.T) { } } } + +// withDefaults mirrors the processor's BuildAuditVars, which sets absent +// top-level objects to empty maps before evaluating a rule. +func withDefaults(audit map[string]any) map[string]any { + out := make(map[string]any, len(audit)) + for k, v := range audit { + out[k] = v + } + for _, field := range []string{"objectRef", "user", "responseStatus", "responseObject", "requestObject"} { + if _, ok := out[field]; !ok { + out[field] = map[string]any{} + } + } + return out +}