Skip to content
Open
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
48 changes: 48 additions & 0 deletions armotypes/runtimeincidents.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package armotypes

import (
"encoding/json"
"fmt"
"slices"
"time"

"github.com/armosec/armoapi-go/armotypes/cdr"
Expand Down Expand Up @@ -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)
Expand Down
118 changes: 118 additions & 0 deletions armotypes/runtimeincidents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading