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