From d9a11ec5c242b09e19338c6b8a5a39ddf6ad368d Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Tue, 21 Jul 2020 17:55:14 -0600 Subject: [PATCH] feat(api): add AWS CloudWatch Alert Channels Int With this change users can now do CRUD operations of AWS CloudWatch Alert Channels (Integrations), here is a basic usage: Initialize a new `AwsCloudWatchIntegration` struct, then use the new instance to do CRUD operations. ```go client, err := api.NewClient("account") if err != nil { return err } awsCloudWatch := api.NewAwsCloudWatchIntegration("foo", api.AwsCloudWatchData{ EventBusArn: "arn:aws:events:us-west-2:1234567890:event-bus/default", MinAlertSeverity: api.MediumAlertLevel, }, ) client.Integrations.CreateAwsCloudWatch(awsCloudWatch) ``` Signed-off-by: Salim Afiune Maya --- ...tegration_alert_channels_aws_cloudwatch.go | 101 +++++++ ...tion_alert_channels_aws_cloudwatch_test.go | 252 ++++++++++++++++++ api/integrations.go | 6 +- 3 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 api/integration_alert_channels_aws_cloudwatch.go create mode 100644 api/integration_alert_channels_aws_cloudwatch_test.go diff --git a/api/integration_alert_channels_aws_cloudwatch.go b/api/integration_alert_channels_aws_cloudwatch.go new file mode 100644 index 000000000..f5cf8a2fa --- /dev/null +++ b/api/integration_alert_channels_aws_cloudwatch.go @@ -0,0 +1,101 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package api + +// NewAwsCloudWatchIntegration returns an instance of AwsCloudWatchInt +// with the provided name and data. +// +// Basic usage: Initialize a new AwsCloudWatchInt struct, then +// use the new instance to do CRUD operations +// +// client, err := api.NewClient("account") +// if err != nil { +// return err +// } +// +// awsCloudWatch := api.NewAwsCloudWatchIntegration("foo", +// api.AwsCloudWatchData{ +// EventBusArn: "arn:aws:events:us-west-2:1234567890:event-bus/default", +// MinAlertSeverity: api.MediumAlertLevel, +// }, +// ) +// +// client.Integrations.CreateAwsCloudWatch(awsCloudWatch) +// +func NewAwsCloudWatchIntegration(name string, data AwsCloudWatchData) AwsCloudWatchInt { + return AwsCloudWatchInt{ + commonIntegrationData: commonIntegrationData{ + Name: name, + Type: AwsCloudWatchIntegration.String(), + Enabled: 1, + }, + Data: data, + } +} + +// CreateAwsCloudWatch creates a AWS CloudWatch alert integration on the Lacework Server +func (svc *IntegrationsService) CreateAwsCloudWatch(integration AwsCloudWatchInt) ( + response AwsCloudWatchIntResponse, + err error, +) { + err = svc.create(integration, &response) + return +} + +// GetAwsCloudWatch gets a AWS CloudWatch alert integration that matches with +// the provided integration guid on the Lacework Server +func (svc *IntegrationsService) GetAwsCloudWatch(guid string) ( + response AwsCloudWatchIntResponse, + err error, +) { + err = svc.get(guid, &response) + return +} + +// UpdateAwsCloudWatch updates a single AWS CloudWatch alert integration +func (svc *IntegrationsService) UpdateAwsCloudWatch(data AwsCloudWatchInt) ( + response AwsCloudWatchIntResponse, + err error, +) { + err = svc.update(data.IntgGuid, data, &response) + return +} + +// ListAwsCloudWatch lists the CLOUDWATCH_EB external integrations available on the Lacework Server +func (svc *IntegrationsService) ListAwsCloudWatch() (response AwsCloudWatchIntResponse, err error) { + err = svc.listByType(AwsCloudWatchIntegration, &response) + return +} + +type AwsCloudWatchIntResponse struct { + Data []AwsCloudWatchInt `json:"data"` + Ok bool `json:"ok"` + Message string `json:"message"` +} + +type AwsCloudWatchInt struct { + commonIntegrationData + Data AwsCloudWatchData `json:"DATA"` +} + +type AwsCloudWatchData struct { + IssueGrouping string `json:"ISSUE_GROUPING,omitempty" mapstructure:"ISSUE_GROUPING"` + EventBusArn string `json:"EVENT_BUS_ARN" mapstructure:"EVENT_BUS_ARN"` + MinAlertSeverity AlertLevel `json:"MIN_ALERT_SEVERITY,omitempty" mapstructure:"MIN_ALERT_SEVERITY"` +} diff --git a/api/integration_alert_channels_aws_cloudwatch_test.go b/api/integration_alert_channels_aws_cloudwatch_test.go new file mode 100644 index 000000000..dff8356f2 --- /dev/null +++ b/api/integration_alert_channels_aws_cloudwatch_test.go @@ -0,0 +1,252 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package api_test + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/lacework/go-sdk/api" + "github.com/lacework/go-sdk/internal/intgguid" + "github.com/lacework/go-sdk/internal/lacework" +) + +func TestIntegrationsNewAwsCloudWatchIntegration(t *testing.T) { + subject := api.NewAwsCloudWatchIntegration("integration_name", + api.AwsCloudWatchData{ + EventBusArn: "arn:aws:events:us-west-2:1234567890:event-bus/default", + MinAlertSeverity: 1, + }, + ) + assert.Equal(t, api.AwsCloudWatchIntegration.String(), subject.Type) + assert.Equal(t, api.CriticalAlertLevel, subject.Data.MinAlertSeverity) +} + +func TestIntegrationsCreateAwsCloudWatch(t *testing.T) { + var ( + intgGUID = intgguid.New() + fakeServer = lacework.MockServer() + ) + fakeServer.MockAPI("external/integrations", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method, "CreateAwsCloudWatch should be a POST method") + + if assert.NotNil(t, r.Body) { + body := httpBodySniffer(r) + assert.Contains(t, body, "integration_name", "integration name is missing") + assert.Contains(t, body, "CLOUDWATCH_EB", "wrong integration type") + assert.Contains(t, body, "arn:aws:events:us-west-2:1234567890:event-bus/default", "wrong event bus arn") + assert.Contains(t, body, "MIN_ALERT_SEVERITY\":3", "wrong alert severity") + assert.Contains(t, body, "ENABLED\":1", "integration is not enabled") + } + + fmt.Fprintf(w, awsCloudWatchIntegrationJsonResponse(intgGUID)) + }) + defer fakeServer.Close() + + c, err := api.NewClient("test", + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + data := api.NewAwsCloudWatchIntegration("integration_name", + api.AwsCloudWatchData{ + EventBusArn: "arn:aws:events:us-west-2:1234567890:event-bus/default", + MinAlertSeverity: 3, + }, + ) + assert.Equal(t, "integration_name", data.Name, "AwsCloudWatch integration name mismatch") + assert.Equal(t, "CLOUDWATCH_EB", data.Type, "a new AwsCloudWatch integration should match its type") + assert.Equal(t, 1, data.Enabled, "a new AwsCloudWatch integration should be enabled") + + response, err := c.Integrations.CreateAwsCloudWatch(data) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.True(t, response.Ok) + if assert.Equal(t, 1, len(response.Data)) { + resData := response.Data[0] + assert.Equal(t, intgGUID, resData.IntgGuid) + assert.Equal(t, "integration_name", resData.Name) + assert.True(t, resData.State.Ok) + assert.Equal(t, "arn:aws:events:us-west-2:1234567890:event-bus/default", resData.Data.EventBusArn) + assert.Equal(t, api.AlertLevel(3), resData.Data.MinAlertSeverity) + } +} + +func TestIntegrationsGetAwsCloudWatch(t *testing.T) { + var ( + intgGUID = intgguid.New() + apiPath = fmt.Sprintf("external/integrations/%s", intgGUID) + fakeServer = lacework.MockServer() + ) + fakeServer.MockAPI(apiPath, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "GetAwsCloudWatch should be a GET method") + fmt.Fprintf(w, awsCloudWatchIntegrationJsonResponse(intgGUID)) + }) + defer fakeServer.Close() + + c, err := api.NewClient("test", + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + response, err := c.Integrations.GetAwsCloudWatch(intgGUID) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.True(t, response.Ok) + if assert.Equal(t, 1, len(response.Data)) { + resData := response.Data[0] + assert.Equal(t, intgGUID, resData.IntgGuid) + assert.Equal(t, "integration_name", resData.Name) + assert.True(t, resData.State.Ok) + assert.Equal(t, "arn:aws:events:us-west-2:1234567890:event-bus/default", resData.Data.EventBusArn) + assert.Equal(t, api.AlertLevel(3), resData.Data.MinAlertSeverity) + } +} + +func TestIntegrationsUpdateAwsCloudWatch(t *testing.T) { + var ( + intgGUID = intgguid.New() + apiPath = fmt.Sprintf("external/integrations/%s", intgGUID) + fakeServer = lacework.MockServer() + ) + fakeServer.MockAPI(apiPath, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method, "UpdateAwsCloudWatch should be a PATCH method") + + if assert.NotNil(t, r.Body) { + body := httpBodySniffer(r) + assert.Contains(t, body, intgGUID, "INTG_GUID missing") + assert.Contains(t, body, "integration_name", "integration name is missing") + assert.Contains(t, body, "CLOUDWATCH_EB", "wrong integration type") + assert.Contains(t, body, "arn:aws:events:us-west-2:1234567890:event-bus/default", "wrong event bus arn") + assert.Contains(t, body, "MIN_ALERT_SEVERITY\":3", "wrong alert severity") + assert.Contains(t, body, "ENABLED\":1", "integration is not enabled") + } + + fmt.Fprintf(w, awsCloudWatchIntegrationJsonResponse(intgGUID)) + }) + defer fakeServer.Close() + + c, err := api.NewClient("test", + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + data := api.NewAwsCloudWatchIntegration("integration_name", + api.AwsCloudWatchData{ + EventBusArn: "arn:aws:events:us-west-2:1234567890:event-bus/default", + MinAlertSeverity: 3, + }, + ) + assert.Equal(t, "integration_name", data.Name, "AwsCloudWatch integration name mismatch") + assert.Equal(t, "CLOUDWATCH_EB", data.Type, "a new AwsCloudWatch integration should match its type") + assert.Equal(t, 1, data.Enabled, "a new AwsCloudWatch integration should be enabled") + data.IntgGuid = intgGUID + + response, err := c.Integrations.UpdateAwsCloudWatch(data) + assert.Nil(t, err) + assert.NotNil(t, response) + assert.Equal(t, "SUCCESS", response.Message) + assert.Equal(t, 1, len(response.Data)) + assert.Equal(t, intgGUID, response.Data[0].IntgGuid) +} + +func TestIntegrationsListAwsCloudWatch(t *testing.T) { + var ( + intgGUIDs = []string{intgguid.New(), intgguid.New(), intgguid.New()} + fakeServer = lacework.MockServer() + ) + fakeServer.MockAPI("external/integrations/type/CLOUDWATCH_EB", + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "ListAwsCloudWatch should be a GET method") + fmt.Fprintf(w, awsCloudWatchMultiIntegrationJsonResponse(intgGUIDs)) + }, + ) + defer fakeServer.Close() + + c, err := api.NewClient("test", + api.WithToken("TOKEN"), + api.WithURL(fakeServer.URL()), + ) + assert.Nil(t, err) + + response, err := c.Integrations.ListAwsCloudWatch() + assert.Nil(t, err) + assert.NotNil(t, response) + assert.True(t, response.Ok) + assert.Equal(t, len(intgGUIDs), len(response.Data)) + for _, d := range response.Data { + assert.Contains(t, intgGUIDs, d.IntgGuid) + } +} + +func awsCloudWatchIntegrationJsonResponse(intgGUID string) string { + return ` +{ + "data": [` + singleAwsCloudWatchIntegration(intgGUID) + `], + "ok": true, + "message": "SUCCESS" +} +` +} + +func awsCloudWatchMultiIntegrationJsonResponse(guids []string) string { + integrations := []string{} + for _, guid := range guids { + integrations = append(integrations, singleAwsCloudWatchIntegration(guid)) + } + return ` +{ +"data": [` + strings.Join(integrations, ", ") + `], +"ok": true, +"message": "SUCCESS" +} +` +} + +func singleAwsCloudWatchIntegration(id string) string { + return ` +{ + "INTG_GUID": "` + id + `", + "CREATED_OR_UPDATED_BY": "user@email.com", + "CREATED_OR_UPDATED_TIME": "2020-Jul-16 19:59:22 UTC", + "DATA": { + "ISSUE_GROUPING": "Events", + "MIN_ALERT_SEVERITY": 3, + "EVENT_BUS_ARN": "arn:aws:events:us-west-2:1234567890:event-bus/default" + }, + "ENABLED": 1, + "IS_ORG": 0, + "NAME": "integration_name", + "STATE": { + "lastSuccessfulTime": "2020-Jul-16 18:26:54 UTC", + "lastUpdatedTime": "2020-Jul-16 18:26:54 UTC", + "ok": true + }, + "TYPE": "CLOUDWATCH_EB", + "TYPE_NAME": "CLOUDWATCH_EB" +} +` +} diff --git a/api/integrations.go b/api/integrations.go index 7db17a793..4ed8a4c09 100644 --- a/api/integrations.go +++ b/api/integrations.go @@ -55,8 +55,11 @@ const ( // Container registry integration type ContainerRegistryIntegration - //Slack channel integration type + // Slack channel integration type SlackChannelIntegration + + // AWS CloudWatch integration type + AwsCloudWatchIntegration ) // IntegrationTypes is the list of available integration types @@ -70,6 +73,7 @@ var IntegrationTypes = map[integrationType]string{ AzureActivityLogIntegration: "AZURE_AL_SEQ", ContainerRegistryIntegration: "CONT_VULN_CFG", SlackChannelIntegration: "SLACK_CHANNEL", + AwsCloudWatchIntegration: "CLOUDWATCH_EB", } // String returns the string representation of an integration type