-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(actions): Add Pagerduty action. (#468)
Signed-off-by: Simar <simar@linux.com> Signed-off-by: Simar <simar@linux.com>
- Loading branch information
Showing
10 changed files
with
305 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package actions | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"time" | ||
|
||
"github.com/PagerDuty/go-pagerduty" | ||
"github.com/aquasecurity/postee/v2/formatting" | ||
"github.com/aquasecurity/postee/v2/layout" | ||
) | ||
|
||
type Clock interface { | ||
Now() time.Time | ||
} | ||
|
||
type realClock struct{} | ||
|
||
func (rc *realClock) Now() time.Time { | ||
return time.Now() | ||
} | ||
|
||
type PagerdutyClient struct { | ||
client *pagerduty.Client | ||
clock Clock | ||
|
||
Name string | ||
AuthToken string | ||
RoutingKey string | ||
} | ||
|
||
func (p *PagerdutyClient) GetName() string { | ||
return p.Name | ||
} | ||
|
||
func (p *PagerdutyClient) Init() error { | ||
if len(p.AuthToken) <= 0 { | ||
return fmt.Errorf("pagerduty auth token is required to send events") | ||
} | ||
if len(p.RoutingKey) <= 0 { | ||
return fmt.Errorf("pagerduty routing key is required to send events") | ||
} | ||
|
||
p.client = pagerduty.NewClient(p.AuthToken) | ||
p.clock = &realClock{} | ||
return nil | ||
} | ||
|
||
func (p *PagerdutyClient) Send(m map[string]string) error { | ||
ctx := context.Background() | ||
resp, err := p.client.ManageEventWithContext(ctx, &pagerduty.V2Event{ | ||
RoutingKey: p.RoutingKey, | ||
Action: "trigger", | ||
Payload: &pagerduty.V2Payload{ | ||
Summary: m["title"], // required | ||
Source: "postee", | ||
Severity: "critical", | ||
Timestamp: p.clock.Now().Format(time.RFC3339), | ||
Details: m["description"], // required | ||
}, | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("failed to send event to pagerduty: %w", err) | ||
} | ||
|
||
log.Printf("successfully sent event to pagerduty, response msg: %s, status: %s", resp.Message, resp.Status) | ||
return nil | ||
} | ||
|
||
func (p *PagerdutyClient) Terminate() error { | ||
return nil | ||
} | ||
|
||
func (p *PagerdutyClient) GetLayoutProvider() layout.LayoutProvider { | ||
/*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/ | ||
return new(formatting.HtmlProvider) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package actions | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/PagerDuty/go-pagerduty" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
type fakeClock struct{} | ||
|
||
func (fc *fakeClock) Now() time.Time { | ||
t, _ := time.Parse(time.RFC3339, "2022-09-22T22:07:55-07:00") | ||
return t | ||
} | ||
|
||
func TestPagerdutyClient_Init(t *testing.T) { | ||
t.Run("happy path", func(t *testing.T) { | ||
require.NoError(t, (&PagerdutyClient{ | ||
Name: "my-pagerduty", | ||
AuthToken: "123456", | ||
RoutingKey: "foobarbaz", | ||
}).Init()) | ||
}) | ||
|
||
t.Run("sad path, no auth token", func(t *testing.T) { | ||
assert.Equal(t, "pagerduty auth token is required to send events", (&PagerdutyClient{ | ||
Name: "my-pagerduty", | ||
RoutingKey: "foobarbaz", | ||
}).Init().Error()) | ||
}) | ||
|
||
t.Run("sad path, no routing key", func(t *testing.T) { | ||
assert.Equal(t, "pagerduty routing key is required to send events", (&PagerdutyClient{ | ||
Name: "my-pagerduty", | ||
AuthToken: "123456", | ||
}).Init().Error()) | ||
}) | ||
} | ||
|
||
func TestPagerdutyClient_Send(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
handlerFunc http.HandlerFunc | ||
expectedError string | ||
pagerdutyClient PagerdutyClient | ||
inputEvent map[string]string | ||
}{ | ||
{ | ||
name: "happy path", | ||
handlerFunc: func(writer http.ResponseWriter, request *http.Request) { | ||
b, _ := io.ReadAll(request.Body) | ||
assert.JSONEq(t, `{"routing_key":"123456","event_action":"trigger","payload":{"summary":"my fancy title","source":"postee","severity":"critical","timestamp":"2022-09-22T22:07:55-07:00","custom_details":"foo bar baz details"}}`, string(b)) | ||
_, _ = fmt.Fprint(writer, `{"status": "ok", "dedup_key": "yes", "message": "ok"}`) | ||
}, | ||
pagerdutyClient: PagerdutyClient{ | ||
Name: "my-pagerduty", | ||
AuthToken: "foo-bar-baz", | ||
RoutingKey: "123456", | ||
}, | ||
inputEvent: map[string]string{ | ||
"description": "foo bar baz details", | ||
"title": "my fancy title", | ||
}, | ||
}, | ||
{ | ||
name: "sad path, pagerduty api returns an error", | ||
handlerFunc: func(writer http.ResponseWriter, request *http.Request) { | ||
writer.WriteHeader(http.StatusInternalServerError) | ||
}, | ||
pagerdutyClient: PagerdutyClient{ | ||
Name: "my-pagerduty", | ||
AuthToken: "foo-bar-baz", | ||
RoutingKey: "123456", | ||
}, | ||
inputEvent: map[string]string{ | ||
"description": "foo bar baz details", | ||
"title": "my fancy title", | ||
}, | ||
expectedError: "failed to send event to pagerduty: HTTP response with status code 500 does not contain Content-Type: application/json", | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
ts := httptest.NewServer(tc.handlerFunc) | ||
defer ts.Close() | ||
|
||
tc.pagerdutyClient.client = pagerduty.NewClient(tc.pagerdutyClient.AuthToken, pagerduty.WithV2EventsAPIEndpoint(ts.URL)) | ||
tc.pagerdutyClient.clock = &fakeClock{} | ||
|
||
err := tc.pagerdutyClient.Send(tc.inputEvent) | ||
switch { | ||
case tc.expectedError != "": | ||
assert.Equal(t, tc.expectedError, err.Error(), tc.name) | ||
default: | ||
assert.NoError(t, err, tc.name) | ||
} | ||
}) | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Paging DevOps Teams | ||
|
||
## Introduction | ||
In this walkthrough, we will setup vulnerability scanning with [Trivy](https://github.com/aquasecurity/trivy) and send the results to Postee for paging DevOps team members for critical vulnerabilities as they are introduced. | ||
|
||
## Scenario | ||
A DevOps team would like to configure alerts for scheduled vulnerability scans to notify them about any vulnerable images that they might be running in their clusters. For this they decide to install Trivy, run it on a schedule and send the results to Postee. | ||
|
||
They decide to configure Postee so that upon receiving such alerts, Postee sends an event to PagerDuty which fires off an alert to inform DevOps teams to take necessary action. | ||
|
||
![img.png](assets/trivy-pagerduty.png) | ||
|
||
## Sample Configs | ||
In this case a sample configuration for the components can be described as follows: | ||
|
||
### Postee Config | ||
```yaml | ||
routes: | ||
- name: Trivy Alerts to Pagerduty | ||
input: input.report.summary.criticalCount > 0 | ||
actions: [alert-devops] | ||
template: trivy-raw-json | ||
|
||
# Templates are used to format a message | ||
templates: | ||
- name: trivy-raw-json | ||
rego-package: postee.rawmessage.json | ||
|
||
# Actions are target services that should consume the messages | ||
actions: | ||
- name: alert-devops | ||
type: pagerduty | ||
enable: true | ||
pagerduty-auth-token: "<auth token>" | ||
pagerduty-routing-key: "<routing key>" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.