Skip to content

Commit 5b55497

Browse files
author
B Pearson
committed
adds pagerduty as an endpoint #781
Signed-off-by: B Pearson <[email protected]>
1 parent 95cad9f commit 5b55497

File tree

4 files changed

+396
-0
lines changed

4 files changed

+396
-0
lines changed

pkg/target/factory.go

+6
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ type GCSOptions struct {
140140
Bucket string `mapstructure:"bucket"`
141141
}
142142

143+
type PagerDutyOptions struct {
144+
APIToken string `mapstructure:"apiToken"`
145+
ServiceID string `mapstructure:"serviceId"`
146+
}
147+
143148
type Targets struct {
144149
Loki *Config[LokiOptions] `mapstructure:"loki"`
145150
Elasticsearch *Config[ElasticsearchOptions] `mapstructure:"elasticsearch"`
@@ -153,6 +158,7 @@ type Targets struct {
153158
Kinesis *Config[KinesisOptions] `mapstructure:"kinesis"`
154159
SecurityHub *Config[SecurityHubOptions] `mapstructure:"securityHub"`
155160
GCS *Config[GCSOptions] `mapstructure:"gcs"`
161+
PagerDuty *Config[PagerDutyOptions] `mapstructure:"pagerduty"`
156162
}
157163

158164
type Factory interface {

pkg/target/factory/factory.go

+45
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/kyverno/policy-reporter/pkg/target/http"
2424
"github.com/kyverno/policy-reporter/pkg/target/kinesis"
2525
"github.com/kyverno/policy-reporter/pkg/target/loki"
26+
"github.com/kyverno/policy-reporter/pkg/target/pagerduty"
2627
"github.com/kyverno/policy-reporter/pkg/target/provider/aws"
2728
gs "github.com/kyverno/policy-reporter/pkg/target/provider/gcs"
2829
"github.com/kyverno/policy-reporter/pkg/target/s3"
@@ -93,6 +94,7 @@ func (f *TargetFactory) CreateClients(config *target.Targets) *target.Collection
9394
targets = append(targets, createClients("Kinesis", config.Kinesis, f.CreateKinesisTarget)...)
9495
targets = append(targets, createClients("SecurityHub", config.SecurityHub, f.CreateSecurityHubTarget)...)
9596
targets = append(targets, createClients("GoogleCloudStorage", config.GCS, f.CreateGCSTarget)...)
97+
targets = append(targets, createClients("PagerDuty", config.PagerDuty, f.CreatePagerDutyTarget)...)
9698

9799
return target.NewCollection(targets...)
98100
}
@@ -741,6 +743,49 @@ func (f *TargetFactory) CreateGCSTarget(config, parent *target.Config[target.GCS
741743
}
742744
}
743745

746+
func (f *TargetFactory) CreatePagerDutyTarget(config, parent *target.Config[target.PagerDutyOptions]) *target.Target {
747+
if config == nil || config.Config == nil {
748+
return nil
749+
}
750+
751+
if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" {
752+
f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret)
753+
}
754+
755+
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
756+
f.mapSecretValues(config, config.SecretRef, config.MountedSecret)
757+
}
758+
759+
if config.Config.APIToken == "" || config.Config.ServiceID == "" {
760+
return nil
761+
}
762+
763+
setFallback(&config.Config.APIToken, parent.Config.APIToken)
764+
setFallback(&config.Config.ServiceID, parent.Config.ServiceID)
765+
766+
config.MapBaseParent(parent)
767+
768+
zap.S().Infof("%s configured", config.Name)
769+
770+
return &target.Target{
771+
ID: uuid.NewString(),
772+
Type: target.PagerDuty,
773+
Config: config,
774+
ParentConfig: parent,
775+
Client: pagerduty.NewClient(pagerduty.Options{
776+
ClientOptions: target.ClientOptions{
777+
Name: config.Name,
778+
SkipExistingOnStartup: config.SkipExisting,
779+
ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources),
780+
ReportFilter: createReportFilter(config.Filter),
781+
},
782+
APIToken: config.Config.APIToken,
783+
ServiceID: config.Config.ServiceID,
784+
CustomFields: config.CustomFields,
785+
}),
786+
}
787+
}
788+
744789
func (f *TargetFactory) createResultFilter(filter target.Filter, minimumSeverity string, sources []string) *report.ResultFilter {
745790
sourceFilter := filter.Sources
746791
if len(sources) > 0 {

pkg/target/pagerduty/pagerduty.go

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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

Comments
 (0)