1
+ package pagerduty
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ "sync"
7
+ "time"
8
+
9
+ "github.com/PagerDuty/go-pagerduty"
10
+ "go.uber.org/zap"
11
+
12
+ "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
13
+ "github.com/kyverno/policy-reporter/pkg/target"
14
+ "github.com/kyverno/policy-reporter/pkg/target/formatting"
15
+ )
16
+
17
+ // Options to configure the PagerDuty target
18
+ type Options struct {
19
+ target.ClientOptions
20
+ APIToken string
21
+ ServiceID string
22
+ CustomFields map [string ]string
23
+ }
24
+
25
+ type client struct {
26
+ target.BaseClient
27
+ client * pagerduty.Client
28
+ serviceID string
29
+ customFields map [string ]string
30
+ // Track active incidents by policy+resource
31
+ incidents sync.Map
32
+ }
33
+
34
+ // Create a unique key for tracking incidents
35
+ func incidentKey (result v1alpha2.PolicyReportResult ) string {
36
+ key := result .Policy
37
+ if result .HasResource () {
38
+ res := result .GetResource ()
39
+ key = fmt .Sprintf ("%s/%s/%s/%s" ,
40
+ result .Policy ,
41
+ res .Kind ,
42
+ res .Namespace ,
43
+ res .Name ,
44
+ )
45
+ }
46
+ return key
47
+ }
48
+
49
+ func (p * client ) Send (result v1alpha2.PolicyReportResult ) {
50
+ key := incidentKey (result )
51
+
52
+ if result .Result == v1alpha2 .StatusPass {
53
+ // Check if we have an active incident to resolve
54
+ if incidentID , ok := p .incidents .Load (key ); ok {
55
+ p .resolveIncident (incidentID .(string ))
56
+ p .incidents .Delete (key )
57
+ }
58
+ return
59
+ }
60
+
61
+ if result .Result != v1alpha2 .StatusFail {
62
+ // Only create incidents for failed policies
63
+ return
64
+ }
65
+
66
+ // Check if we already have an incident for this policy/resource
67
+ if _ , exists := p .incidents .Load (key ); exists {
68
+ // Incident already exists, no need to create another
69
+ return
70
+ }
71
+
72
+ details := map [string ]interface {}{
73
+ "policy" : result .Policy ,
74
+ "rule" : result .Rule ,
75
+ "message" : result .Message ,
76
+ "severity" : result .Severity ,
77
+ }
78
+
79
+ if result .HasResource () {
80
+ res := result .GetResource ()
81
+ details ["resource" ] = formatting .ResourceString (res )
82
+ }
83
+
84
+ for k , v := range p .customFields {
85
+ details [k ] = v
86
+ }
87
+
88
+ for k , v := range result .Properties {
89
+ details [k ] = v
90
+ }
91
+
92
+ incident := pagerduty.CreateIncidentOptions {
93
+ Type : "incident" ,
94
+ Title : fmt .Sprintf ("Policy Violation: %s" , result .Policy ),
95
+ Service : & pagerduty.APIReference {ID : p .serviceID , Type : "service_reference" },
96
+ Body : & pagerduty.APIDetails {
97
+ Type : "incident_body" ,
98
+ Details : details ,
99
+ },
100
+ Urgency : mapSeverityToUrgency (result .Severity ),
101
+ }
102
+
103
+ resp , err := p .client .CreateIncident ("policy-reporter" , & incident )
104
+ if err != nil {
105
+ zap .L ().Error ("failed to create PagerDuty incident" ,
106
+ zap .String ("policy" , result .Policy ),
107
+ zap .Error (err ),
108
+ )
109
+ return
110
+ }
111
+
112
+ // Store the incident ID for later resolution
113
+ p .incidents .Store (key , resp .Id )
114
+
115
+ zap .L ().Info ("PagerDuty incident created" ,
116
+ zap .String ("policy" , result .Policy ),
117
+ zap .String ("severity" , string (result .Severity )),
118
+ zap .String ("incidentId" , resp .Id ),
119
+ )
120
+ }
121
+
122
+ func (p * client ) resolveIncident (incidentID string ) {
123
+ incident := pagerduty.ManageIncidentsOptions {
124
+ ID : incidentID ,
125
+ Incidents : []pagerduty.ManageIncident {
126
+ {
127
+ Status : "resolved" ,
128
+ Resolution : "Policy violation has been resolved" ,
129
+ },
130
+ },
131
+ }
132
+
133
+ if err := p .client .ManageIncidents ("policy-reporter" , & incident ); err != nil {
134
+ zap .L ().Error ("failed to resolve PagerDuty incident" ,
135
+ zap .String ("incidentId" , incidentID ),
136
+ zap .Error (err ),
137
+ )
138
+ return
139
+ }
140
+
141
+ zap .L ().Info ("PagerDuty incident resolved" ,
142
+ zap .String ("incidentId" , incidentID ),
143
+ )
144
+ }
145
+
146
+ func (p * client ) Type () target.ClientType {
147
+ return target .SingleSend
148
+ }
149
+
150
+ func mapSeverityToUrgency (severity v1alpha2.PolicySeverity ) string {
151
+ switch severity {
152
+ case v1alpha2 .SeverityCritical , v1alpha2 .SeverityHigh :
153
+ return "high"
154
+ default :
155
+ return "low"
156
+ }
157
+ }
158
+
159
+ // SetClient allows replacing the PagerDuty client for testing
160
+ func (p * client ) SetClient (c interface {}) {
161
+ if pdClient , ok := c .(interface {
162
+ CreateIncident (string , * pagerduty.CreateIncidentOptions ) (* pagerduty.Incident , error )
163
+ ManageIncidents (string , * pagerduty.ManageIncidentsOptions ) error
164
+ }); ok {
165
+ p .client = pdClient
166
+ }
167
+ }
168
+
169
+ // NewClient creates a new PagerDuty client
170
+ func NewClient (options Options ) target.Client {
171
+ return & client {
172
+ target .NewBaseClient (options .ClientOptions ),
173
+ pagerduty .NewClient (options .APIToken ),
174
+ options .ServiceID ,
175
+ options .CustomFields ,
176
+ sync.Map {},
177
+ }
178
+ }
0 commit comments