From 7a87e60ab8142fab73a74d54f72cd7a7a351b0fc Mon Sep 17 00:00:00 2001 From: Russell Leighton Date: Mon, 13 Apr 2020 14:14:19 -0400 Subject: [PATCH] Add support for step functions in alarm generation (#694) --- tools/cfngen/cloudwatchcf/alarms.go | 38 +++++++++---- tools/cfngen/cloudwatchcf/alarms_sfn.go | 57 +++++++++++++++++++ tools/cfngen/cloudwatchcf/testdata/cf.yml | 20 +++++++ .../testdata/generated_test_alarms.json | 29 ++++++++++ 4 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 tools/cfngen/cloudwatchcf/alarms_sfn.go diff --git a/tools/cfngen/cloudwatchcf/alarms.go b/tools/cfngen/cloudwatchcf/alarms.go index 92f665e743..8baeabc7ff 100644 --- a/tools/cfngen/cloudwatchcf/alarms.go +++ b/tools/cfngen/cloudwatchcf/alarms.go @@ -60,26 +60,42 @@ type AlarmProperties struct { type MetricDimension struct { Name string - // Use only one of Value or ValueRef + // Use only one of Value, ValueSub or ValueRef Value string valueRef *RefString + valueSub *SubString } func (m *MetricDimension) MarshalJSON() ([]byte, error) { - if m.valueRef == nil { + if m.valueRef == nil && m.valueSub == nil { // Most common case - the struct can be marshaled like normal (json ignores nil valueRef) return jsoniter.Marshal(*m) // dereference to avoid infinite recursion } - // Otherwise, marshal a new struct where "Value" is actually a struct with the nested ref - return jsoniter.Marshal(&struct { - Name string - Value RefString - }{ - Name: m.Name, - Value: *m.valueRef, - }) + // marshal a new struct where "Value" is actually a struct with the nested ref + if m.valueRef != nil && m.valueSub == nil { + return jsoniter.Marshal(&struct { + Name string + Value RefString + }{ + Name: m.Name, + Value: *m.valueRef, + }) + } + + // marshal a new struct where "Value" is actually a struct with the nested sub + if m.valueRef == nil && m.valueSub != nil { + return jsoniter.Marshal(&struct { + Name string + Value SubString + }{ + Name: m.Name, + Value: *m.valueSub, + }) + } + + panic("valueRef and valueSub cannot both be set") } type RefString struct { @@ -242,6 +258,8 @@ func alarmDispatchOnType(resourceType string, resource map[interface{}]interface return generateDynamoDBAlarms(resource) case "AWS::Serverless::Function": return generateLambdaAlarms(resource, settings) + case "AWS::StepFunctions::StateMachine": + return generateSFNAlarms(resource) } return alarms } diff --git a/tools/cfngen/cloudwatchcf/alarms_sfn.go b/tools/cfngen/cloudwatchcf/alarms_sfn.go new file mode 100644 index 0000000000..2bced3916e --- /dev/null +++ b/tools/cfngen/cloudwatchcf/alarms_sfn.go @@ -0,0 +1,57 @@ +package cloudwatchcf + +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import "fmt" + +type SFNAlarm struct { + Alarm +} + +func NewSFNAlarm(alarmType, metricName, message string, resource map[interface{}]interface{}) *SFNAlarm { + const ( + metricDimension = "StateMachineArn" + metricNamespace = "AWS/States" + ) + stateMachineName := getResourceProperty("StateMachineName", resource) + stateMachineArn := fmt.Sprintf("arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:%s", + stateMachineName) + alarmName := AlarmName(alarmType, stateMachineName) + alarm := &SFNAlarm{ + Alarm: *NewAlarm(stateMachineName, alarmName, + fmt.Sprintf("State machine %s %s. See: %s#%s", stateMachineName, message, documentationURL, stateMachineName)), + } + alarm.Alarm.Metric(metricNamespace, metricName, []MetricDimension{{ + Name: metricDimension, + valueSub: &SubString{Sub: stateMachineArn}, + }, + }) + return alarm +} + +func generateSFNAlarms(resource map[interface{}]interface{}) []*Alarm { + return []*Alarm{ + NewSFNAlarm( + "SFNError", + "ExecutionsFailed", + "is failing", + resource, + ).SumCountThreshold(0, 60*5), + } +} diff --git a/tools/cfngen/cloudwatchcf/testdata/cf.yml b/tools/cfngen/cloudwatchcf/testdata/cf.yml index c46b073a23..bac1ffa7b8 100644 --- a/tools/cfngen/cloudwatchcf/testdata/cf.yml +++ b/tools/cfngen/cloudwatchcf/testdata/cf.yml @@ -167,3 +167,23 @@ Resources: - kms:Encrypt - kms:GenerateDataKey Resource: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${SQSKeyId} + + MyStateMachine: + Type: 'AWS::StepFunctions::StateMachine' + Properties: + StateMachineName: my-state-machine + DefinitionString: !Sub + - |- + { + "Comment": "A Hello World example using an AWS Lambda function", + "StartAt": "HelloWorld", + "States": { + "HelloWorld": { + "Type": "Task", + "Resource": "${lambdaArn}", + "End": true + } + } + } + - { lambdaArn: !GetAtt [MyLambdaFunction, Arn] } + RoleArn: !GetAtt [StatesExecutionRole, Arn] diff --git a/tools/cfngen/cloudwatchcf/testdata/generated_test_alarms.json b/tools/cfngen/cloudwatchcf/testdata/generated_test_alarms.json index 5a8ed6d363..5f441942c2 100644 --- a/tools/cfngen/cloudwatchcf/testdata/generated_test_alarms.json +++ b/tools/cfngen/cloudwatchcf/testdata/generated_test_alarms.json @@ -665,6 +665,35 @@ "Statistic": "Sum" } }, + "PantherAlarmSFNErrormystatemachine": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "AlarmName": "PantherAlarm-SFNError-my-state-machine", + "AlarmDescription": "State machine my-state-machine is failing. See: https://docs.runpanther.io/operations/runbooks#my-state-machine", + "AlarmActions": [ + { + "Ref": "AlarmTopicArn" + } + ], + "TreatMissingData": "notBreaching", + "Namespace": "AWS/States", + "MetricName": "ExecutionsFailed", + "Dimensions": [ + { + "Name": "StateMachineArn", + "Value": { + "Fn::Sub": "arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:my-state-machine" + } + } + ], + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "Period": 300, + "Threshold": 0, + "Unit": "Count", + "Statistic": "Sum" + } + }, "PantherAlarmSNSErrortestnotifications": { "Type": "AWS::CloudWatch::Alarm", "Properties": {