Skip to content

Commit

Permalink
Added support for alerting on Archiving GuardDuty alerts.
Browse files Browse the repository at this point in the history
  • Loading branch information
stevemac007 committed Dec 3, 2022
1 parent d3230d1 commit 8f02391
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 117 deletions.
111 changes: 2 additions & 109 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,110 +1,3 @@
AWS_CLI ?= /usr/local/bin/aws
TEMP_PATH = .temp
RELEASE_ZIP = release.zip
BUCKET_PREFIX ?= aws-to-slack

# Load from .env file
ifdef TARGET
include $(TARGET)
export
endif

# Dependency definitions
ifdef AWS_REGION
regionArg= --region $(AWS_REGION)
endif
ifdef AWS_PROFILE
awsProfile= --profile $(AWS_PROFILE)
endif
ifndef LAMBDA_NAME
ifndef STACK_ID
usesLambdaName := create-stack load-lambda-name
else
usesLambdaName := load-lambda-name
endif
endif
ifeq (,$(wildcard $(RELEASE_ZIP)))
usesReleaseZip := package
endif

info:
@echo "Deploying to $(BUCKET_PREFIX)"

# Create release.zip file
.PHONY: package
package:
# Prepare
-@rm -r "$(TEMP_PATH)" 2>/dev/null || true
-@rm "$(RELEASE_ZIP)" 2>/dev/null || true
@mkdir -p "$(TEMP_PATH)"

# Copy sources to temporary folder
@cp -R src package-lock.json package.json "$(TEMP_PATH)/"

# Install dependencies
@cd "$(TEMP_PATH)" && npm install --production

# Package artifact
@cd "$(TEMP_PATH)" && zip -rq "../$(RELEASE_ZIP)" .

# Cleanup
@rm -r "$(TEMP_PATH)"


# Perform create-stack operation
.PHONY: create-stack-raw
create-stack-raw:
# Create CloudFormation Stack
aws $(awsProfile) cloudformation create-stack --stack-name "$(STACK_NAME)" --template-body file://cloudformation.yaml \
$(regionArg) --capabilities CAPABILITY_IAM --parameters $(STACK_PARAMS)
aws $(awsProfile) cloudformation wait stack-create-complete --stack-name "$(STACK_NAME)" $(regionArg)


# Create the stack, print output, and save to TARGET file
# (must be separate from create-stack-raw because uses $(shell ...)
.PHONY: create-stack
create-stack: create-stack-raw
$(eval STACK_ID := $(shell aws $(awsProfile) cloudformation describe-stacks --stack-name "$(STACK_NAME)" \
$(regionArg) --output text --query 'Stacks[0].StackId' ))
@echo "Add to your .env file: STACK_ID=$(STACK_ID)"
@ [ -z "$(TARGET)" ] || { echo "# Makefile on `date`" >> "$(TARGET)"; echo "STACK_ID=$(STACK_ID)" >> "$(TARGET)"; }


# Update CloudFormation stack
.PHONY: update-stack
update-stack:
aws $(awsProfile) cloudformation update-stack --stack-name "$(STACK_NAME)" --template-body file://cloudformation.yaml \
$(regionArg) --capabilities CAPABILITY_IAM --parameters $(STACK_PARAMS)


# Perform describe-stack to retrieve name of Lambda function
.PHONY: load-lambda-name
load-lambda-name:
# Load Lambda name from CloudFormation
@if [ -z "$(STACK_NAME)" ]; then echo "Var STACK_NAME must be defined"; exit 1; fi;
$(eval LAMBDA_NAME := $(shell aws $(awsProfile) cloudformation describe-stacks --stack-name "$(STACK_NAME)" \
$(regionArg) --output text --query 'Stacks[0].Outputs[?OutputKey==`LambdaFunction`].OutputValue'))
@echo "Add to your .env file: LAMBDA_NAME=$(LAMBDA_NAME)"
@ [ -z "$(TARGET)" ] || { echo "# Makefile on `date`" >> "$(TARGET)"; echo "LAMBDA_NAME=$(LAMBDA_NAME)" >> "$(TARGET)"; }


# Update existing Lambda function
.PHONY: deploy
deploy: $(usesReleaseZip) $(usesLambdaName)
# Update Lambda function code
aws $(awsProfile) lambda update-function-code --function-name "$(LAMBDA_NAME)" \
$(regionArg) --zip-file "fileb://$(RELEASE_ZIP)" --publish


# Copy local files to global S3 deployment buckets
REGIONS ?= \
us-east-1 us-east-2 us-west-1 us-west-2 \
eu-central-1 eu-west-1 eu-west-2 eu-west-3 \
ap-northeast-1 ap-northeast-2 ap-south-1 ap-southeast-1 ap-southeast-2 \
ca-central-1 sa-east-1
# disabled: cn-north-1 cn-northwest-1
.PHONY: publish
publish: $(usesReleaseZip) $(REGIONS)
$(REGIONS):
aws $(awsProfile) s3 cp "./cloudformation.yaml" "s3://$(BUCKET_PREFIX)-$@" --acl public-read
aws $(awsProfile) s3 cp "$(RELEASE_ZIP)" "s3://$(BUCKET_PREFIX)-$@" --acl public-read
deploy:
sam deploy
56 changes: 49 additions & 7 deletions src/parsers/guardduty.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,62 @@ exports.matches = event =>
exports.parse = event => {
const detail = event.get("detail");

//const id = _.get(detail, "id");
const title = _.get(detail, "title");
const description = _.get(detail, "description");
let title = _.get(detail, "title");
let description = _.get(detail, "description");
const createdAt = new Date(_.get(detail, "time"));
let accountId = _.get(detail, "accountId");
let region = _.get(detail, "region");
let color = event.COLORS.neutral; //low severity below 4
const fields = [];

const eventName = _.get(detail, "eventName")

if (eventName === "ArchiveFindings") {
title = "Findings Archived"
let actionedBy = _.get(detail, "userIdentity.principalId")
accountId = _.get(detail, "recipientAccountId");
region = _.get(detail, "awsRegion");
description = `Findings Archived by ${actionedBy}`

fields.push({
title: "Account",
value: accountId,
short: true
});

fields.push({
title: "Region",
value: region,
short: true
});

fields.push({
title: "Archived by",
value: actionedBy,
short: true
});

const findings = _.get(detail, "requestParameters.findingIds");

for (const finding of findings) {
fields.push({
title: "Finding ID",
value: finding,
short: true
});
}

}
else {

//const id = _.get(detail, "id");
const severity = _.get(detail, "severity");
const accountId = _.get(detail, "accountId");
const region = _.get(detail, "region");
//const partition = _.get(event, "partition");
//const arn = _.get(event, "arn");
const type = _.get(detail, "type");

const threatName = _.get(detail, "service.additionalInfo.threatName");
const threatListName = _.get(detail, "service.additionalInfo.threatListName");
const fields = [];

fields.push({
title: "Description",
Expand Down Expand Up @@ -281,13 +323,13 @@ exports.parse = event => {
});
}

let color = event.COLORS.neutral; //low severity below 4
if (severity > 4) { //medium seveirty between 4 and 7
color = event.COLORS.warning;
}
if (severity > 7) { //high sevirity above 7
color = event.COLORS.critical;
}
}

return event.attachmentWithDefaults({
author_name: "Amazon GuardDuty",
Expand Down
146 changes: 146 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'SNS notification sent to Slack channel'

Transform:
- AWS::Serverless-2016-10-31

Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: 'Slack Setup'
Parameters:
- HookUrl
- Channel
- KmsDecryptKeyArn
- Label:
default: 'Default Subscriptions'
Parameters:
- ErrorAlertEmail
Parameters:
ErrorAlertEmail:
Description: 'Optional: email address to receive alert that function is failing. The email will only contain a count of failures -- you will need to then review CloudWatch logs to determine cause of the issue. HIGHLY RECOMMEND handling failures like this within a DeadLetterQueue via ParentAlertStack instead.'
Type: String
Default: ''
HookUrl:
Type: String
Description: 'Slack webhook URL; see https://example.slack.com/apps/'
Channel:
Type: String
Description: 'Optional: Channel name to post within'
Default: ''
KmsDecryptKeyArn:
Type: String
Description: 'Optional: Key used to encrypt Hook or Channel values. If provided will create IAM policy to grant access to decrypt the values.'
Default: ''

Conditions:
HasChannel: !Not [!Equals [!Ref Channel, '']]
HasKmsKey: !Not [!Equals [!Ref KmsDecryptKeyArn, '']]
HasAlertEmail: !Not [!Equals [!Ref ErrorAlertEmail, '']]

Resources:

SlackAlertTopic:
Type: AWS::SNS::Topic
Properties:
TopicName:
Fn::Sub: ${AWS::StackName}
Tags:
- Key: Name
Value:
Fn::Sub: ${AWS::StackName}

SlackFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/index.handler
Runtime: nodejs16.x
Role: !GetAtt FunctionRole.Arn
MemorySize: 256
Timeout: 15 # Cross-region metrics lookup requires at least 10s
Tracing: Active
Environment:
Variables:
SLACK_CHANNEL: !If
- HasChannel
- !Ref Channel
- !Ref 'AWS::NoValue'
SLACK_HOOK_URL: !Ref HookUrl
Events:
SNSEvent:
Type: SNS
Properties:
Topic:
Ref: SlackAlertTopic

FunctionRole:
Type: 'AWS::IAM::Role'
Properties:
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess
- arn:aws:iam::aws:policy/AWSCodeCommitReadOnly
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: [ 'sts:AssumeRole' ]
Effect: Allow
Principal:
Service: [ 'lambda.amazonaws.com' ]

KmsDecryptPolicy:
Condition: HasKmsKey
Type: 'AWS::IAM::Policy'
Properties:
Roles: [ !Ref FunctionRole ]
PolicyName: AllowDecryptKms
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: [ 'kms:Decrypt' ]
Resource: !Ref KmsDecryptKeyArn

#
# CloudWatch Alarm
#
FunctionFailing:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'AWS-to-Slack Lambda function is failing'
Namespace: 'AWS/Lambda'
MetricName: Errors
Statistic: Sum
Period: 300
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
TreatMissingData: notBreaching
Dimensions:
- Name: FunctionName
Value: !Ref SlackFunction
AlarmActions:
- !Ref FunctionFailingTopic

FunctionFailingTopic:
Type: 'AWS::SNS::Topic'
Properties: {}

FunctionFailingEmailSubscription:
Condition: HasAlertEmail
Type: 'AWS::SNS::Subscription'
Properties:
TopicArn: !Ref FunctionFailingTopic
Protocol: email
Endpoint: !Ref ErrorAlertEmail

Outputs:
LambdaFunction:
Description: 'Lambda function name created by this stack.'
Value: !Ref SlackFunction

LambdaFunctionArn:
Description: 'Lambda function ARN created by this stack.'
Value: !GetAtt SlackFunction.Arn
Loading

0 comments on commit 8f02391

Please sign in to comment.