diff --git a/armotypes/runtimeincidents.go b/armotypes/runtimeincidents.go index 329d2ab..4f2eeaa 100644 --- a/armotypes/runtimeincidents.go +++ b/armotypes/runtimeincidents.go @@ -2,6 +2,8 @@ package armotypes import ( "encoding/json" + "fmt" + "slices" "time" "github.com/armosec/armoapi-go/armotypes/cdr" @@ -270,6 +272,52 @@ type RuntimeIncidentExceptionPolicy struct { SeverityScore int `json:"severityScore"` } +type RuntimeIncidentsOvertime struct { + Date string `json:"date"` + CountsByStatus map[string]int64 `json:"countsByStatus,omitempty"` + // Legacy fields for backward compatibility - will be removed after FE will be in production + Count int64 `json:"count,omitempty"` + NewCount int64 `json:"newCount,omitempty"` + DismissedCount int64 `json:"dismissedCount,omitempty"` +} + +type IncidentStatusChange struct { + IncidentGUID string `json:"incidentGUID"` + Status string `json:"status"` + PreviousStatus string `json:"previousStatus,omitempty"` + ChangedBy string `json:"changedBy,omitempty"` + CustomerGUID string `json:"customerGUID"` + ClusterName string `json:"clusterName"` +} + +func (c *IncidentStatusChange) Validate() error { + if c.IncidentGUID == "" { + return fmt.Errorf("incidentGUID is required") + } + if c.Status == "" { + return fmt.Errorf("status is required") + } + if c.CustomerGUID == "" { + return fmt.Errorf("customerGUID is required") + } + if c.ClusterName == "" { + return fmt.Errorf("clusterName is required") + } + + validStatuses := []string{"Open", "Investigating", "Resolved", "Dismissed"} + if !slices.Contains(validStatuses, c.Status) { + return fmt.Errorf("invalid status '%s', must be one of: %v", c.Status, validStatuses) + } + if c.PreviousStatus != "" && !slices.Contains(validStatuses, c.PreviousStatus) { + return fmt.Errorf("invalid previous_status '%s', must be one of: %v", c.PreviousStatus, validStatuses) + } + if c.Status == c.PreviousStatus && c.PreviousStatus != "" { + return fmt.Errorf("status and previousStatus are the same (%s) - no change to record", c.Status) + } + + return nil +} + // FindProcessByPID searches for a process by PID in the process tree func (pt *ProcessTree) FindProcessByPID(pid uint32) *Process { return findProcessRecursive(&pt.ProcessTree, pid) diff --git a/armotypes/runtimeincidents_test.go b/armotypes/runtimeincidents_test.go index 35355c9..b1eb059 100644 --- a/armotypes/runtimeincidents_test.go +++ b/armotypes/runtimeincidents_test.go @@ -54,6 +54,124 @@ func TestAdmissionAlertJSON(t *testing.T) { assert.Equal(t, alert, alert2) } +func TestIncidentStatusChangeValidate(t *testing.T) { + tests := []struct { + name string + change IncidentStatusChange + expectError bool + errorMsg string + }{ + { + name: "valid change", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "Open", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: false, + }, + { + name: "valid transition", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "Investigating", + PreviousStatus: "Open", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: false, + }, + { + name: "missing incidentGUID", + change: IncidentStatusChange{ + Status: "Open", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: true, + errorMsg: "incidentGUID is required", + }, + { + name: "missing status", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: true, + errorMsg: "status is required", + }, + { + name: "missing customerGUID", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "Open", + ClusterName: "prod-cluster", + }, + expectError: true, + errorMsg: "customerGUID is required", + }, + { + name: "missing clusterName", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "Open", + CustomerGUID: "customer-456", + }, + expectError: true, + errorMsg: "clusterName is required", + }, + { + name: "invalid status", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "InvalidStatus", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: true, + errorMsg: "invalid status", + }, + { + name: "invalid previousStatus", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "Open", + PreviousStatus: "InvalidPrevStatus", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: true, + errorMsg: "invalid previous_status", + }, + { + name: "no-op change (Status == PreviousStatus)", + change: IncidentStatusChange{ + IncidentGUID: "incident-123", + Status: "Open", + PreviousStatus: "Open", + CustomerGUID: "customer-456", + ClusterName: "prod-cluster", + }, + expectError: true, + errorMsg: "status and previousStatus are the same", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.change.Validate() + if tt.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestFindProcessRecursive(t *testing.T) { tree := Process{ PID: 1,