Skip to content

Commit

Permalink
adds pagerduty as an endpoint #781
Browse files Browse the repository at this point in the history
Signed-off-by: B Pearson <[email protected]>
  • Loading branch information
B Pearson committed Feb 26, 2025
1 parent 95cad9f commit 5b55497
Show file tree
Hide file tree
Showing 4 changed files with 396 additions and 0 deletions.
6 changes: 6 additions & 0 deletions pkg/target/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ type GCSOptions struct {
Bucket string `mapstructure:"bucket"`
}

type PagerDutyOptions struct {
APIToken string `mapstructure:"apiToken"`
ServiceID string `mapstructure:"serviceId"`
}

type Targets struct {
Loki *Config[LokiOptions] `mapstructure:"loki"`
Elasticsearch *Config[ElasticsearchOptions] `mapstructure:"elasticsearch"`
Expand All @@ -153,6 +158,7 @@ type Targets struct {
Kinesis *Config[KinesisOptions] `mapstructure:"kinesis"`
SecurityHub *Config[SecurityHubOptions] `mapstructure:"securityHub"`
GCS *Config[GCSOptions] `mapstructure:"gcs"`
PagerDuty *Config[PagerDutyOptions] `mapstructure:"pagerduty"`
}

type Factory interface {
Expand Down
45 changes: 45 additions & 0 deletions pkg/target/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/target/http"
"github.com/kyverno/policy-reporter/pkg/target/kinesis"
"github.com/kyverno/policy-reporter/pkg/target/loki"
"github.com/kyverno/policy-reporter/pkg/target/pagerduty"
"github.com/kyverno/policy-reporter/pkg/target/provider/aws"
gs "github.com/kyverno/policy-reporter/pkg/target/provider/gcs"
"github.com/kyverno/policy-reporter/pkg/target/s3"
Expand Down Expand Up @@ -93,6 +94,7 @@ func (f *TargetFactory) CreateClients(config *target.Targets) *target.Collection
targets = append(targets, createClients("Kinesis", config.Kinesis, f.CreateKinesisTarget)...)
targets = append(targets, createClients("SecurityHub", config.SecurityHub, f.CreateSecurityHubTarget)...)
targets = append(targets, createClients("GoogleCloudStorage", config.GCS, f.CreateGCSTarget)...)
targets = append(targets, createClients("PagerDuty", config.PagerDuty, f.CreatePagerDutyTarget)...)

return target.NewCollection(targets...)
}
Expand Down Expand Up @@ -741,6 +743,49 @@ func (f *TargetFactory) CreateGCSTarget(config, parent *target.Config[target.GCS
}
}

func (f *TargetFactory) CreatePagerDutyTarget(config, parent *target.Config[target.PagerDutyOptions]) *target.Target {
if config == nil || config.Config == nil {
return nil
}

if (parent.SecretRef != "" && f.secretClient != nil) || parent.MountedSecret != "" {
f.mapSecretValues(parent, parent.SecretRef, parent.MountedSecret)
}

if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(config, config.SecretRef, config.MountedSecret)
}

if config.Config.APIToken == "" || config.Config.ServiceID == "" {
return nil
}

setFallback(&config.Config.APIToken, parent.Config.APIToken)
setFallback(&config.Config.ServiceID, parent.Config.ServiceID)

config.MapBaseParent(parent)

zap.S().Infof("%s configured", config.Name)

return &target.Target{
ID: uuid.NewString(),
Type: target.PagerDuty,
Config: config,
ParentConfig: parent,
Client: pagerduty.NewClient(pagerduty.Options{
ClientOptions: target.ClientOptions{
Name: config.Name,
SkipExistingOnStartup: config.SkipExisting,
ResultFilter: f.createResultFilter(config.Filter, config.MinimumSeverity, config.Sources),
ReportFilter: createReportFilter(config.Filter),
},
APIToken: config.Config.APIToken,
ServiceID: config.Config.ServiceID,
CustomFields: config.CustomFields,
}),
}
}

func (f *TargetFactory) createResultFilter(filter target.Filter, minimumSeverity string, sources []string) *report.ResultFilter {
sourceFilter := filter.Sources
if len(sources) > 0 {
Expand Down
178 changes: 178 additions & 0 deletions pkg/target/pagerduty/pagerduty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package pagerduty

import (
"fmt"
"strings"
"sync"
"time"

"github.com/PagerDuty/go-pagerduty"
"go.uber.org/zap"

"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/formatting"
)

// Options to configure the PagerDuty target
type Options struct {
target.ClientOptions
APIToken string
ServiceID string
CustomFields map[string]string
}

type client struct {
target.BaseClient
client *pagerduty.Client
serviceID string
customFields map[string]string
// Track active incidents by policy+resource
incidents sync.Map
}

// Create a unique key for tracking incidents
func incidentKey(result v1alpha2.PolicyReportResult) string {
key := result.Policy
if result.HasResource() {
res := result.GetResource()
key = fmt.Sprintf("%s/%s/%s/%s",
result.Policy,
res.Kind,
res.Namespace,
res.Name,
)
}
return key
}

func (p *client) Send(result v1alpha2.PolicyReportResult) {
key := incidentKey(result)

if result.Result == v1alpha2.StatusPass {
// Check if we have an active incident to resolve
if incidentID, ok := p.incidents.Load(key); ok {
p.resolveIncident(incidentID.(string))
p.incidents.Delete(key)
}
return
}

if result.Result != v1alpha2.StatusFail {
// Only create incidents for failed policies
return
}

// Check if we already have an incident for this policy/resource
if _, exists := p.incidents.Load(key); exists {
// Incident already exists, no need to create another
return
}

details := map[string]interface{}{
"policy": result.Policy,
"rule": result.Rule,
"message": result.Message,
"severity": result.Severity,
}

if result.HasResource() {
res := result.GetResource()
details["resource"] = formatting.ResourceString(res)
}

for k, v := range p.customFields {
details[k] = v
}

for k, v := range result.Properties {
details[k] = v
}

incident := pagerduty.CreateIncidentOptions{
Type: "incident",
Title: fmt.Sprintf("Policy Violation: %s", result.Policy),
Service: &pagerduty.APIReference{ID: p.serviceID, Type: "service_reference"},
Body: &pagerduty.APIDetails{
Type: "incident_body",
Details: details,
},
Urgency: mapSeverityToUrgency(result.Severity),
}

resp, err := p.client.CreateIncident("policy-reporter", &incident)
if err != nil {
zap.L().Error("failed to create PagerDuty incident",
zap.String("policy", result.Policy),
zap.Error(err),
)
return
}

// Store the incident ID for later resolution
p.incidents.Store(key, resp.Id)

zap.L().Info("PagerDuty incident created",
zap.String("policy", result.Policy),
zap.String("severity", string(result.Severity)),
zap.String("incidentId", resp.Id),
)
}

func (p *client) resolveIncident(incidentID string) {
incident := pagerduty.ManageIncidentsOptions{
ID: incidentID,
Incidents: []pagerduty.ManageIncident{
{
Status: "resolved",
Resolution: "Policy violation has been resolved",
},
},
}

if err := p.client.ManageIncidents("policy-reporter", &incident); err != nil {
zap.L().Error("failed to resolve PagerDuty incident",
zap.String("incidentId", incidentID),
zap.Error(err),
)
return
}

zap.L().Info("PagerDuty incident resolved",
zap.String("incidentId", incidentID),
)
}

func (p *client) Type() target.ClientType {
return target.SingleSend
}

func mapSeverityToUrgency(severity v1alpha2.PolicySeverity) string {
switch severity {
case v1alpha2.SeverityCritical, v1alpha2.SeverityHigh:
return "high"
default:
return "low"
}
}

// SetClient allows replacing the PagerDuty client for testing
func (p *client) SetClient(c interface{}) {
if pdClient, ok := c.(interface {
CreateIncident(string, *pagerduty.CreateIncidentOptions) (*pagerduty.Incident, error)
ManageIncidents(string, *pagerduty.ManageIncidentsOptions) error
}); ok {
p.client = pdClient
}
}

// NewClient creates a new PagerDuty client
func NewClient(options Options) target.Client {
return &client{
target.NewBaseClient(options.ClientOptions),
pagerduty.NewClient(options.APIToken),
options.ServiceID,
options.CustomFields,
sync.Map{},
}
}
Loading

0 comments on commit 5b55497

Please sign in to comment.