Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions config/milo/iam/resources/auditlogfacetsqueries.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: iam.miloapis.com/v1alpha1
kind: ProtectedResource
metadata:
name: activity.miloapis.com-auditlogfacetsqueries
spec:
serviceRef:
name: "activity.miloapis.com"
kind: AuditLogFacetsQuery
plural: auditlogfacetsqueries
singular: auditlogfacetsquery
permissions:
- create
parentResources:
- apiGroup: resourcemanager.miloapis.com
kind: Organization
- apiGroup: resourcemanager.miloapis.com
kind: Project
- apiGroup: iam.miloapis.com
kind: User
1 change: 1 addition & 0 deletions config/milo/iam/resources/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ kind: Component

resources:
- auditlogqueries.yaml
- auditlogfacetsqueries.yaml
1 change: 1 addition & 0 deletions config/milo/iam/roles/audit-log-querier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ spec:
launchStage: Beta
includedPermissions:
- activity.miloapis.com/auditlogqueries.create
- activity.miloapis.com/auditlogfacetsqueries.create
28 changes: 28 additions & 0 deletions internal/cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ func TestCELFilterWorkflow(t *testing.T) {
wantArgCount: 2,
wantErr: false,
},
{
name: "NOT operator - simple negation",
filter: "!(verb == 'get')",
wantSQL: "NOT (verb = {arg1})",
wantArgCount: 1,
wantErr: false,
},
{
name: "NOT operator - negate IN expression",
filter: "!(verb in ['get', 'list', 'watch'])",
wantSQL: "NOT (verb IN [{arg1}, {arg2}, {arg3}])",
wantArgCount: 3,
wantErr: false,
},
{
name: "NOT operator - combined with AND",
filter: "!(verb == 'get') && objectRef.namespace == 'production'",
wantSQL: "(NOT (verb = {arg1}) AND namespace = {arg2})",
wantArgCount: 2,
wantErr: false,
},
{
name: "NOT operator - negate string method",
filter: "!user.username.startsWith('system:')",
wantSQL: "NOT (startsWith(user, {arg1}))",
wantArgCount: 1,
wantErr: false,
},
}

for _, tt := range tests {
Expand Down
10 changes: 9 additions & 1 deletion internal/cel/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var tracer = otel.Tracer("activity-cel-filter")
// Note: stageTimestamp is intentionally NOT available for filtering as it should
// only be used for internal pipeline delay calculations, not for querying events.
//
// Supports standard CEL operators (==, &&, ||, in) and string methods
// Supports standard CEL operators (==, !=, <, >, <=, >=, &&, ||, !, in) and string methods
// (startsWith, endsWith, contains).
func Environment() (*cel.Env, error) {
objectRefType := cel.MapType(cel.StringType, cel.DynType)
Expand Down Expand Up @@ -268,6 +268,14 @@ func (c *sqlConverter) convertExpr(e *expr.Expr) (string, error) {

func (c *sqlConverter) convertCallExpr(call *expr.Expr_Call, e *expr.Expr) (string, error) {
switch call.Function {
case "!_":
// Handle logical NOT: !expr -> NOT (expr)
arg, err := c.convertExpr(call.Args[0])
if err != nil {
return "", err
}
return fmt.Sprintf("NOT (%s)", arg), nil

case "_==_":
left, err := c.convertExpr(call.Args[0])
if err != nil {
Expand Down
11 changes: 9 additions & 2 deletions pkg/apis/activity/v1alpha1/types_auditlogfacetsquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ type AuditLogFacetsQuerySpec struct {
// Filter narrows the audit logs before computing facets using CEL.
// This allows you to get facet values for a subset of audit logs.
//
// Example: "verb in ['create', 'update', 'delete']" to get facets only for write operations.
//
// Available Fields:
// verb - API action: get, list, create, update, patch, delete, watch
// user.username - who made the request (user or service account)
Expand All @@ -60,6 +58,15 @@ type AuditLogFacetsQuerySpec struct {
// objectRef.apiGroup - API group of the resource
// objectRef.name - specific resource name
//
// Operators: ==, !=, <, >, <=, >=, &&, ||, !, in
// String Functions: startsWith(), endsWith(), contains()
//
// Examples:
// "verb in ['create', 'update', 'delete']" - Facets for write operations only
// "!(verb in ['get', 'list', 'watch'])" - Exclude read-only operations
// "!user.username.startsWith('system:')" - Exclude system users
// "objectRef.namespace == 'production'" - Facets for production namespace
//
// +optional
Filter string `json:"filter,omitempty"`

Expand Down
4 changes: 3 additions & 1 deletion pkg/apis/activity/v1alpha1/types_auditlogquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,17 @@ type AuditLogQuerySpec struct {
// objectRef.resource - resource type (pods, deployments, secrets, configmaps, etc.)
// objectRef.name - specific resource name
//
// Operators: ==, !=, <, >, <=, >=, &&, ||, in
// Operators: ==, !=, <, >, <=, >=, &&, ||, !, in
// String Functions: startsWith(), endsWith(), contains()
//
// Common Patterns:
// "verb == 'delete'" - All deletions
// "objectRef.namespace == 'production'" - Activity in production namespace
// "verb in ['create', 'update', 'delete', 'patch']" - All write operations
// "!(verb in ['get', 'list', 'watch'])" - Exclude read-only operations
// "responseStatus.code >= 400" - Failed requests
// "user.username.startsWith('system:serviceaccount:')" - Service account activity
// "!user.username.startsWith('system:')" - Exclude system users
// "user.uid == '550e8400-e29b-41d4-a716-446655440000'" - Specific user by UID
// "objectRef.resource == 'secrets'" - Secret access
// "verb == 'delete' && objectRef.namespace == 'production'" - Production deletions
Expand Down