Add working end to end bot
* Add functions to utils * store_oauth_token : Store an OAuth 2 access token in SSM parameter store * get_access_token : Fetch the OAuth 2 access token for a given client_id from cache or SSM parameter store * emit_to_mozdef : Send a message with the user's response to SQS for pickup by MozDef * call_slack : POST to a slack URL and return the result * provision_token : Given an OAuth 2 code, obtain a Slack access token and store it * redirect_to_slack_authorize : Build a Slack OAuth 2 authorization URL and redirect the user to it * Fill out lambda_handler in app to cover all URL paths and direct invocations * Add functions to app * API calls * process_api_call : Process an API Gateway call depending on the URL path called * `/authorize` : utils.redirect_to_slack_authorize * `/redirect_uri` : utils.provision_token * `/slack/interactive-endpoint` : handle_message_interaction : Process a user's interaction with a Slack message * send_slack_message_response : Respond to a user's selection by updating the Slack message with a reply * Direct invocations * send_message_to_slack : Send a message to a user via IM or Slack App conversation * get_user_from_email : Fetch a slack user dictionary for an email address * compose_message : Create a Slack message object * create_slack_channel : Create an IM channel with a user * post_message : Post a message to a slack channel * Update README with configuration details * Add details on discovering the SQS URL to the README * Add additional test invocations to the Makefile * Add SQS URL discovery to the Makefile * Add requests to the requirements.txt * Update CloudFormation template to * accept Slack client ID and secret * Grant Lambda function rights to * read and write to SSM parameter store * decrypt parameter store secrets * send messages to the SQS queue * Create the SQS queue * Grant the MozDef user rights to read from the SQS queue * Add new settings to config.py
This commit is contained in:
Родитель
b0ece0d2c3
Коммит
03aa28745b
65
README.md
65
README.md
|
@ -5,39 +5,66 @@ A Slack bot that facilitates triaging MozDef alerts by automating outreach to Mo
|
|||
|
||||
### Deployment
|
||||
|
||||
To deploy the slack-triage-bot-api into AWS run
|
||||
To deploy the slack-triage-bot-api into AWS
|
||||
* determine the Slack Client Secret
|
||||
* Run the make command for the environment you want
|
||||
|
||||
```shell script
|
||||
make deploy-mozdef-slack-triage-bot-api
|
||||
DEV_SLACK_CLIENT_SECRET=0123456789abcdef0123456789abcdef make deploy-mozdef-slack-triage-bot-api
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell script
|
||||
make deploy-mozdef-slack-triage-bot-api-dev
|
||||
DEV_SLACK_CLIENT_SECRET=0123456789abcdef0123456789abcdef make deploy-mozdef-slack-triage-bot-api-dev
|
||||
```
|
||||
|
||||
depending on the account
|
||||
|
||||
### Configuring in Slack
|
||||
|
||||
* Interactive Components
|
||||
* Interactivity
|
||||
* Request URL
|
||||
* `https://example.com/slack/interactive-endpoint`
|
||||
* Select Menus
|
||||
* Options Load URL
|
||||
* `https://example.com/slack/options-load-endpoint`
|
||||
* OAuth & Permissions
|
||||
* OAuth Tokens & Redirect URLs
|
||||
* Redirect URLs
|
||||
* `https://example.com/redirect_uri`
|
||||
* Scopes
|
||||
* Bot Token Scopes
|
||||
* `users:read.email` : https://api.slack.com/methods/users.lookupByEmail
|
||||
* `users:read` : This is required because of users:read.email
|
||||
* `im:write` : https://api.slack.com/methods/conversations.open
|
||||
* `chat:write` : https://api.slack.com/methods/chat.postMessage
|
||||
* Install app
|
||||
|
||||
### Testing
|
||||
|
||||
You can test invoking the function with
|
||||
You can test invoking the function by passing the email address of the user
|
||||
you want to send a message to and calling
|
||||
|
||||
```shell script
|
||||
make test-mozdef-slack-triage-bot-api-invoke
|
||||
EMAIL_ADDRESS=user@example.com make test-mozdef-slack-triage-bot-api-invoke
|
||||
```
|
||||
|
||||
which will pass
|
||||
|
||||
```json
|
||||
{"foo": "baz"}
|
||||
{
|
||||
"identifier":"9Zo02m4B7gIfixq3c4Xh",
|
||||
"alert":"duo_bypass_codes_generated",
|
||||
"identityConfidence":"lowest",
|
||||
"summary":"DUO bypass codes have been generated for your account. ",
|
||||
"user":"user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
to the API which will return
|
||||
|
||||
```json
|
||||
{"result": "bar"}
|
||||
```
|
||||
to the API which will return the JSON response from Slack of the message that
|
||||
was sent to the user.
|
||||
|
||||
You can also test the API Gateway interface by running
|
||||
|
||||
|
@ -47,4 +74,18 @@ make test-mozdef-slack-triage-bot-api-http
|
|||
|
||||
which will hit the `/test` API endpoint and get back a 200 `API request received`
|
||||
|
||||
You can also visit the `/error` endpoint to get a 400 or any other endpoint to get a 404
|
||||
You can also visit the `/error` endpoint to get a 400 or any other endpoint to get a 404
|
||||
|
||||
### Discovering the SQS URL containing user responses
|
||||
|
||||
To discover the URL of the SQS queue into which user responses are sent, call
|
||||
|
||||
```shell script
|
||||
make discover-sqs-queue-url
|
||||
```
|
||||
|
||||
which will return a value like
|
||||
|
||||
```json
|
||||
{"result": "https://sqs.us-west-2.amazonaws.com/012345678901/MozDefSlackTraigeBotAPI-SlackTriageBotMozDefQueue-ABCDEFGHIJKL"}
|
||||
```
|
|
@ -10,6 +10,10 @@ PROD_DOMAIN_ZONE := security.mozilla.org.
|
|||
DEV_DOMAIN_ZONE := security.allizom.org.
|
||||
PROD_CERT_ARN := arn:aws:acm:us-west-2:371522382791:certificate/71e3c455-de1f-4a9d-b50e-440624854c48
|
||||
DEV_CERT_ARN := arn:aws:acm:us-west-2:656532927350:certificate/2e59930d-ae8c-43fa-adff-ed3b1a902caa
|
||||
PROD_SLACK_CLIENT_ID := ???
|
||||
DEV_SLACK_CLIENT_ID := 371351187216.856548004901
|
||||
# PROD_SLACK_CLIENT_SECRET := ???
|
||||
# DEV_SLACK_CLIENT_SECRET := ???
|
||||
|
||||
.PHONE: deploy-mozdef-slack-triage-bot-api-dev
|
||||
deploy-mozdef-slack-triage-bot-api-dev:
|
||||
|
@ -21,7 +25,9 @@ deploy-mozdef-slack-triage-bot-api-dev:
|
|||
$(CODE_STORAGE_S3_PREFIX) \
|
||||
"CustomDomainName=$(DEV_DOMAIN_NAME) \
|
||||
DomainNameZone=$(DEV_DOMAIN_ZONE) \
|
||||
CertificateArn=$(DEV_CERT_ARN)" \
|
||||
CertificateArn=$(DEV_CERT_ARN) \
|
||||
SlackClientId=$(DEV_SLACK_CLIENT_ID) \
|
||||
SlackClientSecret=$(DEV_SLACK_CLIENT_SECRET)" \
|
||||
SlackTriageBotApiUrl
|
||||
|
||||
.PHONE: deploy-mozdef-slack-triage-bot-api
|
||||
|
@ -34,7 +40,9 @@ deploy-mozdef-slack-triage-bot-api:
|
|||
$(CODE_STORAGE_S3_PREFIX) \
|
||||
"CustomDomainName=$(PROD_DOMAIN_NAME) \
|
||||
DomainNameZone=$(PROD_DOMAIN_ZONE) \
|
||||
CertificateArn=$(PROD_CERT_ARN)" \
|
||||
CertificateArn=$(PROD_CERT_ARN) \
|
||||
SlackClientId=$(PROD_SLACK_CLIENT_ID) \
|
||||
SlackClientSecret=$(PROD_SLACK_CLIENT_SECRET)" \
|
||||
SlackTriageBotApiUrl
|
||||
|
||||
.PHONE: test-mozdef-slack-triage-bot-api-http
|
||||
|
@ -47,7 +55,58 @@ test-mozdef-slack-triage-bot-api-invoke:
|
|||
FUNCTION_NAME=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotFunctionName'].OutputValue" --output text` && \
|
||||
ACCESS_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerAccessKeyId'].OutputValue" --output text` && \
|
||||
SECRET_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerSecretAccessKey'].OutputValue" --output text` && \
|
||||
AWS_ACCESS_KEY_ID=$$ACCESS_KEY AWS_SECRET_ACCESS_KEY=$$SECRET_KEY AWS_SESSION_TOKEN= aws lambda invoke --function-name $$FUNCTION_NAME --payload '{"foo": "baz"}' response.json && \
|
||||
AWS_ACCESS_KEY_ID=$$ACCESS_KEY AWS_SECRET_ACCESS_KEY=$$SECRET_KEY AWS_SESSION_TOKEN= aws lambda invoke \
|
||||
--function-name $$FUNCTION_NAME \
|
||||
--payload '{"identifier": "9Zo02m4B7gIfixq3c4Xh", "alert": "duo_bypass_codes_generated", "identityConfidence": "highest", "summary": "DUO bypass codes have been generated for your account. ", "user": "$(EMAIL_ADDRESS)"}' \
|
||||
response.json && \
|
||||
cat response.json && \
|
||||
rm response.json
|
||||
|
||||
.PHONE: test-mozdef-slack-triage-bot-api-invoke-ssh1
|
||||
test-mozdef-slack-triage-bot-api-invoke-ssh1:
|
||||
FUNCTION_NAME=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotFunctionName'].OutputValue" --output text` && \
|
||||
ACCESS_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerAccessKeyId'].OutputValue" --output text` && \
|
||||
SECRET_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerSecretAccessKey'].OutputValue" --output text` && \
|
||||
AWS_ACCESS_KEY_ID=$$ACCESS_KEY AWS_SECRET_ACCESS_KEY=$$SECRET_KEY AWS_SESSION_TOKEN= aws lambda invoke \
|
||||
--function-name $$FUNCTION_NAME \
|
||||
--payload '{"identifier": "9Zo02m4B7gIfixq3c4Xh", "alert": "sensitive_host_session", "identityConfidence": "low", "summary": "An SSH session to a potentially sensitive host sensitive.example.com was made by your user account.", "user": "$(EMAIL_ADDRESS)"}' \
|
||||
response.json && \
|
||||
cat response.json && \
|
||||
rm response.json
|
||||
|
||||
.PHONE: test-mozdef-slack-triage-bot-api-invoke-ssh2
|
||||
test-mozdef-slack-triage-bot-api-invoke-ssh2:
|
||||
FUNCTION_NAME=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotFunctionName'].OutputValue" --output text` && \
|
||||
ACCESS_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerAccessKeyId'].OutputValue" --output text` && \
|
||||
SECRET_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerSecretAccessKey'].OutputValue" --output text` && \
|
||||
AWS_ACCESS_KEY_ID=$$ACCESS_KEY AWS_SECRET_ACCESS_KEY=$$SECRET_KEY AWS_SESSION_TOKEN= aws lambda invoke \
|
||||
--function-name $$FUNCTION_NAME \
|
||||
--payload '{"identifier": "9Zo02m4B7gIfixq3c4Xh", "alert": "ssh_access_sign_releng", "identityConfidence": "low", "summary": "An SSH session was established to host host.example.com by your user account.", "user": "$(EMAIL_ADDRESS)"}' \
|
||||
response.json && \
|
||||
cat response.json && \
|
||||
rm response.json
|
||||
|
||||
.PHONE: test-mozdef-slack-triage-bot-api-invoke-duo-code-used
|
||||
test-mozdef-slack-triage-bot-api-invoke-duo-code-used:
|
||||
FUNCTION_NAME=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotFunctionName'].OutputValue" --output text` && \
|
||||
ACCESS_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerAccessKeyId'].OutputValue" --output text` && \
|
||||
SECRET_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerSecretAccessKey'].OutputValue" --output text` && \
|
||||
AWS_ACCESS_KEY_ID=$$ACCESS_KEY AWS_SECRET_ACCESS_KEY=$$SECRET_KEY AWS_SESSION_TOKEN= aws lambda invoke \
|
||||
--function-name $$FUNCTION_NAME \
|
||||
--payload '{"identifier": "9Zo02m4B7gIfixq3c4Xh", "alert": "duo_bypass_codes_used", "identityConfidence": "highest", "summary": "DUO bypass codes belonging to your account have been used to ", "user": "$(EMAIL_ADDRESS)"}' \
|
||||
response.json && \
|
||||
cat response.json && \
|
||||
rm response.json
|
||||
|
||||
.PHONE: discover-sqs-queue-url
|
||||
discover-sqs-queue-url:
|
||||
FUNCTION_NAME=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotFunctionName'].OutputValue" --output text` && \
|
||||
ACCESS_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerAccessKeyId'].OutputValue" --output text` && \
|
||||
SECRET_KEY=`aws cloudformation describe-stacks --stack-name $(STACK_NAME) --query "Stacks[0].Outputs[?OutputKey=='SlackTriageBotInvokerSecretAccessKey'].OutputValue" --output text` && \
|
||||
AWS_ACCESS_KEY_ID=$$ACCESS_KEY AWS_SECRET_ACCESS_KEY=$$SECRET_KEY AWS_SESSION_TOKEN= aws lambda invoke \
|
||||
--function-name $$FUNCTION_NAME \
|
||||
--payload '{"action": "discover-sqs-queue-url"}' \
|
||||
response.json && \
|
||||
cat response.json && \
|
||||
rm response.json
|
||||
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import urllib.parse
|
||||
import requests
|
||||
|
||||
from .config import CONFIG
|
||||
|
||||
from .utils import (
|
||||
foo
|
||||
call_slack,
|
||||
emit_to_mozdef,
|
||||
provision_token,
|
||||
redirect_to_slack_authorize,
|
||||
SlackException
|
||||
)
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
@ -13,9 +21,394 @@ logging.getLogger('botocore').propagate = False
|
|||
logging.getLogger('urllib3').propagate = False
|
||||
|
||||
|
||||
def send_message_to_slack() -> str:
|
||||
result = foo()
|
||||
return result
|
||||
def get_user_from_email(email: str) -> dict:
|
||||
"""Fetch a slack user dictionary for an email address
|
||||
|
||||
Required slack scopes
|
||||
* bot - users:read.email : https://api.slack.com/methods/users.lookupByEmail
|
||||
* bot - users:read : This scope must be requested if users:read.email is
|
||||
requested
|
||||
:param email: email address of the slack user
|
||||
:return: dictionary of user information
|
||||
"""
|
||||
data = {'email': email}
|
||||
url = 'https://slack.com/api/users.lookupByEmail'
|
||||
return call_slack(url, data, 'user')
|
||||
|
||||
|
||||
def create_slack_channel(user: str) -> dict:
|
||||
"""Create an IM channel with a user
|
||||
|
||||
Required slack scopes
|
||||
* bot - im:write : https://api.slack.com/methods/conversations.open
|
||||
|
||||
:param user: The Slack user ID of the user to create an IM channel with
|
||||
:return: dictionary of channel information
|
||||
"""
|
||||
data = {'users': user}
|
||||
url = 'https://slack.com/api/conversations.open'
|
||||
return call_slack(url, data, 'channel', True)
|
||||
|
||||
|
||||
def compose_message(
|
||||
identifier: str,
|
||||
alert: str,
|
||||
summary: str,
|
||||
email: str,
|
||||
identity_confidence: str) -> dict:
|
||||
"""Create a Slack message object
|
||||
|
||||
:param identifier: The unique identifier for this message
|
||||
:param alert: The name of the MozDef alert
|
||||
:param summary: The summary text of the alert
|
||||
:param email: The email address of the user
|
||||
:param identity_confidence: The identity confidence sent from MozDef
|
||||
:return: A Slack message dictionary
|
||||
"""
|
||||
|
||||
default_response = {
|
||||
'identifier': identifier,
|
||||
'email': email,
|
||||
'alert': alert,
|
||||
'identity_confidence': identity_confidence
|
||||
}
|
||||
|
||||
# TODO : Add something that if the identity_confidence is high don't offer
|
||||
# the wronguser option
|
||||
blocks = [
|
||||
{
|
||||
"block_id": "mozdef-triage-bot-api-question",
|
||||
"text": {
|
||||
"text": "{}\nWas this action taken by you ({})?".format(
|
||||
summary, email),
|
||||
"type": "mrkdwn",
|
||||
},
|
||||
"type": "section"
|
||||
},
|
||||
{
|
||||
"block_id": "mozdef-triage-bot-api-answer",
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"action_id": "mozdef-triage-bot-api-yes",
|
||||
"style": "primary",
|
||||
"text": {
|
||||
"emoji": False,
|
||||
"text": "Yes, I did that",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"type": "button",
|
||||
"value": json.dumps(
|
||||
{**default_response, "response": "yes"})
|
||||
},
|
||||
{
|
||||
"action_id": "mozdef-triage-bot-api-no",
|
||||
"style": "danger",
|
||||
"text": {
|
||||
"emoji": False,
|
||||
"text": "No, I didn't do that!",
|
||||
"type": "plain_text",
|
||||
},
|
||||
"type": "button",
|
||||
"confirm": {
|
||||
"confirm": {
|
||||
"text": "Ya, I didn't take that action",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"deny": {
|
||||
"text": "Oh, nevermind, I did do that",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"text": {
|
||||
"text": "Are you sure that you didn't take that "
|
||||
"action? If you're sure then someone in "
|
||||
"the security team will contact you to "
|
||||
"follow up.".format(email),
|
||||
"type": "mrkdwn"
|
||||
},
|
||||
"title": {
|
||||
"text": "Are you sure?", "type": "plain_text"}
|
||||
},
|
||||
|
||||
"value": json.dumps({**default_response, "response": "no"})
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
if identity_confidence in ['moderate', 'low', 'lowest']:
|
||||
blocks[1]['elements'].append(
|
||||
{
|
||||
"action_id": "mozdef-triage-bot-api-wronguser",
|
||||
"text": {
|
||||
"text": "You've got the wrong person",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"confirm": {
|
||||
"confirm": {
|
||||
"text": "Ya, that's not me",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"deny": {
|
||||
"text": "Oh, actually that is me",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"text": {
|
||||
"text": (
|
||||
"Are you sure that {} isn't you and we've sent "
|
||||
"this alert to the wrong user?".format(email)),
|
||||
"type": "mrkdwn"
|
||||
},
|
||||
"title": {"text": "Are you sure?", "type": "plain_text"}
|
||||
},
|
||||
"type": "button",
|
||||
"value": json.dumps(
|
||||
{**default_response, "response": "wronguser"})
|
||||
}
|
||||
)
|
||||
blocks[1]['elements'].append(
|
||||
{
|
||||
"action_id": "mozdef-triage-bot-api-notsure",
|
||||
"text": {
|
||||
"text": "Hmm... I'm not sure",
|
||||
"type": "plain_text"
|
||||
},
|
||||
"type": "button",
|
||||
"value": json.dumps({**default_response, "response": "notsure"})
|
||||
}
|
||||
)
|
||||
blocks_json = json.dumps(blocks)
|
||||
|
||||
# We can't pass "as_user": False here as doing so causes the Slack API to
|
||||
# return an error that the chat:write:bot scope is missing
|
||||
# Slack support reports this is a known bug
|
||||
# https://mozilla-sandbox-scim.slack.com/help/requests/2564183
|
||||
message = {
|
||||
'blocks': blocks_json,
|
||||
'text': summary
|
||||
}
|
||||
return message
|
||||
|
||||
|
||||
def post_message(channel: str, message: dict) -> dict:
|
||||
"""Post a message to a slack channel
|
||||
|
||||
Required slack scopes
|
||||
* bot - chat:write : https://api.slack.com/methods/chat.postMessage
|
||||
|
||||
:param channel: The Slack channel ID to post the message to
|
||||
:param message: The message to post
|
||||
:return: A slack message dictionary
|
||||
"""
|
||||
data = message
|
||||
data['channel'] = channel
|
||||
url = 'https://slack.com/api/chat.postMessage'
|
||||
return call_slack(url, data, 'message', True)
|
||||
|
||||
|
||||
def send_message_to_slack(
|
||||
identifier: str,
|
||||
alert: str,
|
||||
summary: str,
|
||||
email_address: str,
|
||||
identity_confidence: str) -> dict:
|
||||
"""Send a message to a user via IM or Slack App conversation
|
||||
|
||||
:param identifier: The unique identifier sent by MozDef originally
|
||||
:param alert: The name of the MozDef alert
|
||||
:param summary: The summary text of the alert
|
||||
:param email_address: The user's email address
|
||||
:param identity_confidence: The identityConfidence sent by MozDef
|
||||
originally
|
||||
:return: A slack message dictionary
|
||||
"""
|
||||
send_to_im = False
|
||||
user = get_user_from_email(email_address)
|
||||
message = compose_message(
|
||||
identifier, alert, summary, email_address, identity_confidence)
|
||||
if send_to_im:
|
||||
channel = create_slack_channel(user['id'])
|
||||
post_result = post_message(channel['id'], message)
|
||||
else:
|
||||
post_result = post_message(user['id'], message)
|
||||
return post_result
|
||||
|
||||
|
||||
def send_slack_message_response(
|
||||
response_url: str,
|
||||
message: dict,
|
||||
user_response: str) -> bool:
|
||||
"""Respond to a user's selection by updating the Slack message with a reply
|
||||
|
||||
:param response_url: The Slack URL to send message responses to
|
||||
:param message: The original message that the user made a selection from
|
||||
:param user_response: The user's selection
|
||||
:return: Whether or not the response to the user succeeded
|
||||
"""
|
||||
if user_response == 'yes':
|
||||
bot_response = (
|
||||
':heavy_check_mark: Understood, thanks for letting us know.')
|
||||
elif user_response == 'no':
|
||||
bot_response = (
|
||||
':open_mouth: Got it, thank you. Someone from the security team '
|
||||
'will contact you to follow up on this.')
|
||||
elif user_response == 'wronguser':
|
||||
bot_response = (
|
||||
":flushed: Oh, sorry about that. Someone from the security team "
|
||||
"will look into this and contact the right user. Sorry to bother "
|
||||
"you.")
|
||||
elif user_response == 'notsure':
|
||||
bot_response = (
|
||||
":ok_hand: No problem. Someone from the security team will "
|
||||
"contact you to follow up on this.")
|
||||
else:
|
||||
bot_response = (
|
||||
":heavy_multiplication_x: Hmm, I had some kind of internal error. "
|
||||
"Would you contact the security team to let them know that I'm "
|
||||
"unwell?")
|
||||
|
||||
if 'mozdef-triage-bot-api-response' in [x.get('block_id') for x
|
||||
in message.get('blocks', [])]:
|
||||
bot_response = "You've changed your mind, no problem. " + bot_response
|
||||
response_block = {
|
||||
'block_id': 'mozdef-triage-bot-api-response',
|
||||
'text': {
|
||||
'text': bot_response,
|
||||
'type': "mrkdwn"
|
||||
},
|
||||
"type": "section"
|
||||
}
|
||||
if 'blocks' in message:
|
||||
for i in range(0, len(message['blocks'])):
|
||||
if message['blocks'][i].get('block_id') == 'mozdef-triage-bot-api-response':
|
||||
message['blocks'][i] = response_block
|
||||
break
|
||||
else:
|
||||
message['blocks'].append(response_block)
|
||||
|
||||
message['replace_original'] = True
|
||||
try:
|
||||
response = requests.post(
|
||||
url=response_url,
|
||||
json=message
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(
|
||||
'POST of response to {} failed {} : {} : {} : {}'.format(
|
||||
response_url,
|
||||
message,
|
||||
e,
|
||||
getattr(e.response, 'status_code', None),
|
||||
getattr(e.response, 'text', None)
|
||||
))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def handle_message_interaction(payload: dict) -> bool:
|
||||
"""Process a user's interaction with a Slack message
|
||||
|
||||
payload['type'] :
|
||||
'block_actions' : Parse the value that the user chose, send it to
|
||||
MozDef and send a response to the user
|
||||
:param payload: A dictionary of data sent from Slack about a user's
|
||||
interaction
|
||||
:return: Whether or not the response to the user succeeded
|
||||
"""
|
||||
# The right way to do this is
|
||||
# 1. Drop a message in a message queue (e.g. SQS) with the message to
|
||||
# send back to the user
|
||||
# 2. Return 200
|
||||
# 3. Pull that message off the queue
|
||||
# 4. POST to the response_url with the response message
|
||||
# Until that's added we'll just
|
||||
# 1. POST to the response_url with the response message
|
||||
# 2. Hope that the POST completes in under 3 seconds and return 200
|
||||
if payload.get('type') == 'block_actions':
|
||||
# User clicked a Block Kit interactive component
|
||||
for action in payload.get('actions', []):
|
||||
if 'value' not in action:
|
||||
raise SlackException(
|
||||
'Action encountered with no value : {}'.format(action))
|
||||
try:
|
||||
value = json.loads(action['value'])
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
logger.error('Failed to parse button value "{}" : {}'.format(
|
||||
action['value'],
|
||||
e
|
||||
))
|
||||
raise
|
||||
message_id = emit_to_mozdef(
|
||||
value.get('identifier'),
|
||||
value.get('email'),
|
||||
payload.get('user', {}).get('id'),
|
||||
value['identity_confidence'],
|
||||
value.get('response')
|
||||
)
|
||||
allowed_keys = [
|
||||
'text', 'blocks', 'attachments', 'thread_ts', 'mrkdwn']
|
||||
original_message = {
|
||||
k: v for k, v in
|
||||
payload.get('message', {}).items()
|
||||
if k in allowed_keys}
|
||||
return send_slack_message_response(
|
||||
payload.get('response_url'),
|
||||
original_message,
|
||||
value.get('response'))
|
||||
else:
|
||||
# https://api.slack.com/interactivity/handling#payloads
|
||||
logger.error(
|
||||
"Encountered a message interaction payload type that hasn't yet "
|
||||
"been developed : {}".format(payload))
|
||||
return False
|
||||
|
||||
|
||||
def process_api_call(
|
||||
event: dict,
|
||||
query_string_parameters: dict,
|
||||
body: dict) -> dict:
|
||||
"""Process an API Gateway call depending on the URL path called
|
||||
|
||||
:param event: The API Gateway request event
|
||||
:param query_string_parameters: A dictionary of query string parameters
|
||||
:param body: The parsed body that was POSTed to the API Gateway
|
||||
:return: A dictionary of an API Gateway HTTP response
|
||||
"""
|
||||
if event.get('path') == '/error':
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 400,
|
||||
'body': "Since you requested the /error API endpoint I'll go "
|
||||
"ahead and serve back a 400"}
|
||||
elif event.get('path') == '/test':
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 200,
|
||||
'body': 'API request received'}
|
||||
elif event.get('path') == '/redirect_uri':
|
||||
return provision_token(query_string_parameters)
|
||||
elif event.get('path') == '/authorize':
|
||||
return redirect_to_slack_authorize()
|
||||
elif event.get('path') == '/slack/interactive-endpoint':
|
||||
for payload_raw in body.get('payload', []):
|
||||
payload = json.loads(payload_raw)
|
||||
logger.debug('payload is {}'.format(payload))
|
||||
result = handle_message_interaction(payload)
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 200,
|
||||
'body': 'Acknowledged'}
|
||||
elif event.get('path') == '/slack/options-load-endpoint':
|
||||
# https://api.slack.com/reference/block-kit/block-elements#external_select
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 200,
|
||||
'body': 'Acknowledged'}
|
||||
else:
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 404,
|
||||
'body': "That path wasn't found"}
|
||||
|
||||
|
||||
def lambda_handler(event: dict, context: dict) -> dict:
|
||||
|
@ -28,25 +421,36 @@ def lambda_handler(event: dict, context: dict) -> dict:
|
|||
logger.debug('event is {}'.format(event))
|
||||
try:
|
||||
if event.get('resource') == '/{proxy+}':
|
||||
# this invocation comes from API Gateway
|
||||
if event.get('path') == '/error':
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 400,
|
||||
'body': "Since you requested the /error API endpoint I'll go ahead and serve back a 400"}
|
||||
elif event.get('path') == '/test':
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 200,
|
||||
'body': 'API request received'}
|
||||
headers = event['headers'] if event['headers'] is not None else {}
|
||||
cookie_header = headers.get('Cookie', '')
|
||||
referer = headers.get('Referer', '')
|
||||
query_string_parameters = (
|
||||
event['queryStringParameters']
|
||||
if event['queryStringParameters'] is not None else {})
|
||||
|
||||
parser = None
|
||||
if headers.get('Content-Type') == 'application/x-www-form-urlencoded':
|
||||
parser = urllib.parse.parse_qs
|
||||
elif headers.get('Content-Type') == 'application/json':
|
||||
parser = json.loads
|
||||
if parser is not None and event.get('body') is not None:
|
||||
body = parser(event['body'])
|
||||
else:
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 404,
|
||||
'body': "That path wasn't found"}
|
||||
body = {}
|
||||
return process_api_call(event, query_string_parameters, body)
|
||||
else:
|
||||
# Not an API Gateway invocation, we'll assume a direct Lambda invocation
|
||||
return {'result': send_message_to_slack()}
|
||||
if event.get('action') == 'discover-sqs-queue-url':
|
||||
result = CONFIG.queue_url
|
||||
else:
|
||||
result = send_message_to_slack(
|
||||
event.get('identifier'),
|
||||
event.get('alert'),
|
||||
event.get('summary'),
|
||||
event.get('user'),
|
||||
event.get('identityConfidence')
|
||||
)
|
||||
return {'result': result}
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
logger.error(traceback.format_exc())
|
||||
|
|
|
@ -3,8 +3,14 @@ import os
|
|||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.parameter_store_prefix = '/SlackTriageBot'
|
||||
self.slack_token_parameter_store_name = '{}/SlackOAuthToken'.format(
|
||||
self.parameter_store_prefix)
|
||||
self.domain_name = os.getenv('DOMAIN_NAME')
|
||||
self.log_level = os.getenv('LOG_LEVEL', 'INFO')
|
||||
self.slack_client_id = os.getenv('SLACK_CLIENT_ID')
|
||||
self.slack_client_secret = os.getenv('SLACK_CLIENT_SECRET')
|
||||
self.queue_url = os.getenv('QUEUE_URL')
|
||||
|
||||
|
||||
CONFIG = Config()
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Optional
|
||||
import boto3
|
||||
import requests
|
||||
|
||||
from .config import CONFIG
|
||||
|
||||
|
@ -6,5 +10,222 @@ logger = logging.getLogger(__name__)
|
|||
logger.setLevel(CONFIG.log_level)
|
||||
|
||||
|
||||
def foo() -> str:
|
||||
return 'bar'
|
||||
class SlackException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def store_oauth_token(client_id: str, access_token: str) -> dict:
|
||||
"""Store an OAuth 2 access token in SSM parameter store
|
||||
|
||||
:param client_id: OAuth 2 client_id issued by Slack
|
||||
:param access_token: OAuth 2 access token
|
||||
:return: dictionary containing the "Version" and "Tier" of the stored
|
||||
parameter
|
||||
"""
|
||||
client = boto3.client('ssm')
|
||||
return client.put_parameter(
|
||||
Name='{}-{}'.format(
|
||||
CONFIG.slack_token_parameter_store_name, client_id),
|
||||
Description='The Slack OAuth access token for the MozDef Slack Triage '
|
||||
'Bot API',
|
||||
Value=access_token,
|
||||
Type='SecureString',
|
||||
Overwrite=True,
|
||||
Tags=[
|
||||
{
|
||||
'Key': 'Application',
|
||||
'Value': 'MozDef Slack Triage Bot API'
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_access_token(client_id: str) -> dict:
|
||||
"""Fetch the OAuth 2 access token for a given client_id from cache or SSM
|
||||
parameter store
|
||||
|
||||
:param client_id: The OAuth 2 client_id to fetch the associated access
|
||||
token from
|
||||
:return: string of the access token
|
||||
"""
|
||||
global access_token
|
||||
if 'access_token' not in globals():
|
||||
access_token = {}
|
||||
if client_id not in access_token:
|
||||
client = boto3.client('ssm')
|
||||
response = client.get_parameter(
|
||||
Name='{}-{}'.format(
|
||||
CONFIG.slack_token_parameter_store_name, client_id),
|
||||
WithDecryption=True
|
||||
)
|
||||
access_token[client_id] = response['Parameter']['Value']
|
||||
return access_token
|
||||
|
||||
|
||||
def emit_to_mozdef(
|
||||
identifier: str,
|
||||
email: str,
|
||||
slack_user_id: str,
|
||||
identity_confidence: str,
|
||||
response: str) -> str:
|
||||
"""Send a message with the user's response to SQS for pickup by MozDef
|
||||
|
||||
:param identifier: The unique identifier sent by MozDef originally
|
||||
:param email: The user's email address
|
||||
:param slack_user_id: The user's slack ID
|
||||
:param identity_confidence: The identityConfidence sent by MozDef
|
||||
originally
|
||||
:param response: The user's response
|
||||
:return: The message ID returned from SQS after sending the message
|
||||
"""
|
||||
data = {
|
||||
"identifier": identifier,
|
||||
"user": {
|
||||
"email": email,
|
||||
"slack": slack_user_id
|
||||
},
|
||||
"identityConfidence": identity_confidence,
|
||||
"response": response
|
||||
}
|
||||
client = boto3.client('sqs')
|
||||
response = client.send_message(
|
||||
QueueUrl=CONFIG.queue_url,
|
||||
MessageBody=json.dumps(data)
|
||||
)
|
||||
return response['MessageId']
|
||||
|
||||
|
||||
def call_slack(
|
||||
url: str,
|
||||
data: dict,
|
||||
key_to_return: str,
|
||||
post_as_json: Optional[bool] = False) -> dict:
|
||||
"""POST to a slack URL and return the result
|
||||
|
||||
:param url: The Slack URL to POST to
|
||||
:param data: The payload to pass in the POST body
|
||||
:param key_to_return: The key in the dictionary that is returned by Slack
|
||||
to return to the caller of the call_slack method
|
||||
:param post_as_json: A boolean of whether or not to POST a JSON payload
|
||||
or a URL encoded payload
|
||||
:return: The response from Slack based on the key_to_return
|
||||
"""
|
||||
access_token = get_access_token(CONFIG.slack_client_id)
|
||||
headers = {
|
||||
'Authorization': 'Bearer {}'.format(
|
||||
access_token[CONFIG.slack_client_id])}
|
||||
try:
|
||||
if post_as_json:
|
||||
response = requests.post(url, json=data, headers=headers)
|
||||
else:
|
||||
response = requests.post(url, data=data, headers=headers)
|
||||
response.raise_for_status()
|
||||
if not response.json().get('ok'):
|
||||
raise SlackException(response.json().get('error'))
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(
|
||||
'POST of response to {} failed {} : {} : {} : {}'.format(
|
||||
url,
|
||||
data,
|
||||
e,
|
||||
getattr(e.response, 'status_code', None),
|
||||
getattr(e.response, 'text', None)
|
||||
))
|
||||
raise
|
||||
logger.debug('Called slack with {} and received response of {}'.format(
|
||||
data,
|
||||
response.json()
|
||||
))
|
||||
return response.json().get(key_to_return)
|
||||
|
||||
|
||||
def provision_token(query_string_parameters: dict) -> dict:
|
||||
"""Given an OAuth 2 code, obtain a Slack access token and store it
|
||||
|
||||
:param query_string_parameters: A dictionary of the URL query parameters
|
||||
:return: A dictionary of an API Gateway HTTP response
|
||||
"""
|
||||
if query_string_parameters.get('error'):
|
||||
logger.error('redirect_uri error : {}'.format(
|
||||
query_string_parameters.get('error')
|
||||
))
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 400,
|
||||
'body': "Unable to provision and store an OAuth access token"}
|
||||
data = {
|
||||
'code': query_string_parameters.get('code'),
|
||||
'client_id': CONFIG.slack_client_id,
|
||||
'client_secret': CONFIG.slack_client_secret
|
||||
}
|
||||
url = 'https://slack.com/api/oauth.v2.access'
|
||||
try:
|
||||
response = requests.post(
|
||||
url=url,
|
||||
data=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
if not response.json().get('ok'):
|
||||
raise SlackException(response.json().get('error'))
|
||||
except (requests.exceptions.RequestException, SlackException) as e:
|
||||
logger.error(
|
||||
'Failed to provision and store OAuth access token with url {} '
|
||||
'and data {} : {} : {} : {}'.format(
|
||||
url,
|
||||
data,
|
||||
e,
|
||||
getattr(e.response, 'status_code', None),
|
||||
getattr(e.response, 'text', None)
|
||||
))
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 400,
|
||||
'body': "Unable to provision and store an OAuth access token"}
|
||||
|
||||
access_token = response.json().get('access_token')
|
||||
if access_token is not None:
|
||||
store_oauth_token(CONFIG.slack_client_id, access_token)
|
||||
return {
|
||||
'headers': {'Content-Type': 'text/html'},
|
||||
'statusCode': 200,
|
||||
'body': 'Success : OAuth access token has been '
|
||||
'provisioned and stored'}
|
||||
|
||||
|
||||
def redirect_to_slack_authorize() -> dict:
|
||||
"""Build a Slack OAuth 2 authorization URL and redirect the user to it
|
||||
|
||||
:return: A dictionary of an API Gateway HTTP response
|
||||
"""
|
||||
redirect_uri = 'https://{}/redirect_uri'.format(
|
||||
CONFIG.domain_name)
|
||||
# TODO : Do we want to pass a "state" argument here? If so we'll need
|
||||
# to store it in the client's cookies or somewhere
|
||||
|
||||
# users:read scope must be requested at the same time as
|
||||
# users:read.email or the error "Invalid permissions requested" is
|
||||
# returned
|
||||
scopes = [
|
||||
'chat:write',
|
||||
'users:read',
|
||||
'users:read.email',
|
||||
'im:write'
|
||||
]
|
||||
url = (
|
||||
'https://slack.com/oauth/v2/authorize?'
|
||||
'redirect_uri={redirect_uri}&'
|
||||
'client_id={client_id}&'
|
||||
'scope={scopes}'.format(
|
||||
redirect_uri=redirect_uri,
|
||||
client_id=CONFIG.slack_client_id,
|
||||
scopes=' '.join(scopes)
|
||||
))
|
||||
return {
|
||||
'statusCode': 302,
|
||||
'headers': {
|
||||
'Location': url,
|
||||
'Cache-Control': 'max-age=0',
|
||||
'Content-Type': 'text/html'
|
||||
},
|
||||
'body': 'Redirecting to identity provider'
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
requests
|
|
@ -10,6 +10,11 @@ Metadata:
|
|||
- CustomDomainName
|
||||
- DomainNameZone
|
||||
- CertificateArn
|
||||
- Label:
|
||||
default: Slack
|
||||
Parameters:
|
||||
- SlackClientId
|
||||
- SlackClientSecret
|
||||
ParameterLabels:
|
||||
CustomDomainName:
|
||||
default: Custom DNS Domain Name
|
||||
|
@ -17,6 +22,10 @@ Metadata:
|
|||
default: DNS Zone containing the Custom DNS Domain Name
|
||||
CertificateArn:
|
||||
default: AWS ACM Certificate ARN for the Custom DNS Domain Name
|
||||
SlackClientId:
|
||||
default: Slack App OAuth client ID
|
||||
SlackClientSecret:
|
||||
default: Slack App OAuth client secret
|
||||
Parameters:
|
||||
CustomDomainName:
|
||||
Type: String
|
||||
|
@ -30,6 +39,13 @@ Parameters:
|
|||
Type: String
|
||||
Description: The ARN of the AWS ACM Certificate for your custom domain name
|
||||
Default: ''
|
||||
SlackClientId:
|
||||
Type: String
|
||||
Description: Slack App OAuth client ID
|
||||
SlackClientSecret:
|
||||
Type: String
|
||||
NoEcho: true
|
||||
Description: Slack App OAuth client secret
|
||||
Conditions:
|
||||
UseCustomDomainName: !Not [ !Equals [ !Ref 'CustomDomainName', '' ] ]
|
||||
Rules:
|
||||
|
@ -62,6 +78,37 @@ Resources:
|
|||
- logs:CreateLogStream
|
||||
- logs:PutLogEvents
|
||||
Resource: '*'
|
||||
- PolicyName: AllowReadWriteParameterStore
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- ssm:GetParameter
|
||||
- ssm:PutParameter
|
||||
- ssm:AddTagsToResource
|
||||
Resource: !Join [ '', [ 'arn:aws:ssm:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':parameter/SlackTriageBot/*' ] ]
|
||||
- PolicyName: AllowEncryptDecryptParameterStore
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- kms:Decrypt
|
||||
- kms:Encrypt
|
||||
Resource: '*' # This is a wildcard because determining the CMK ARN for the aws/ssm KMS key is difficult
|
||||
Condition:
|
||||
StringLike:
|
||||
'kms:EncryptionContext:PARAMETER_ARN': !Join [ '', [ 'arn:aws:ssm:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':parameter/SlackTriageBot/*' ] ]
|
||||
- PolicyName: AllowSendSlackTriageBotSQSQueue
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- sqs:SendMessage
|
||||
Resource:
|
||||
- !GetAtt SlackTriageBotMozDefQueue.Arn
|
||||
SlackTriageBotApiFunction:
|
||||
Type: AWS::Lambda::Function
|
||||
Properties:
|
||||
|
@ -70,7 +117,10 @@ Resources:
|
|||
Environment:
|
||||
Variables:
|
||||
DOMAIN_NAME: !Ref CustomDomainName # What if a domain name isn't provided?
|
||||
LOG_LEVEL: DEBUG
|
||||
SLACK_CLIENT_ID: !Ref SlackClientId
|
||||
SLACK_CLIENT_SECRET: !Ref SlackClientSecret
|
||||
QUEUE_URL: !Ref SlackTriageBotMozDefQueue
|
||||
LOG_LEVEL: INFO
|
||||
Handler: slack_triage_bot_api.app.lambda_handler
|
||||
Runtime: python3.7
|
||||
Role: !GetAtt SlackTriageBotApiFunctionRole.Arn
|
||||
|
@ -205,6 +255,17 @@ Resources:
|
|||
Uri: !Join [ '', [ 'arn:aws:apigateway:', !Ref 'AWS::Region', ':lambda:path/2015-03-31/functions/', !GetAtt 'SlackTriageBotApiFunction.Arn', '/invocations' ] ]
|
||||
ResourceId: !Ref SlackTriageBotApiResource
|
||||
RestApiId: !Ref SlackTriageBotApiApi
|
||||
SlackTriageBotMozDefQueue:
|
||||
Type: AWS::SQS::Queue
|
||||
Properties:
|
||||
MessageRetentionPeriod: 345600 # 4 days, the AWS default
|
||||
Tags:
|
||||
- Key: application
|
||||
Value: slack-triage-bot-api
|
||||
- Key: stack
|
||||
Value: !Ref AWS::StackName
|
||||
- Key: source
|
||||
Value: https://github.com/mozilla/MozDef-Triage-Bot
|
||||
SlackTriageBotInvokerUser:
|
||||
Type: AWS::IAM::User
|
||||
Properties:
|
||||
|
@ -218,6 +279,21 @@ Resources:
|
|||
- lambda:InvokeFunction
|
||||
Resource:
|
||||
- !GetAtt SlackTriageBotApiFunction.Arn
|
||||
- PolicyName: AllowReceiveSlackTriageBotSQSQueue
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- sqs:ChangeMessageVisibility
|
||||
- sqs:DeleteMessage
|
||||
- sqs:GetQueueAttributes
|
||||
- sqs:GetQueueUrl
|
||||
- sqs:ListQueueTags
|
||||
- sqs:PurgeQueue
|
||||
- sqs:ReceiveMessage
|
||||
Resource:
|
||||
- !GetAtt SlackTriageBotMozDefQueue.Arn
|
||||
SlackTriageBotInvokerAccessKey:
|
||||
Type: AWS::IAM::AccessKey
|
||||
Properties:
|
||||
|
@ -244,3 +320,12 @@ Outputs:
|
|||
SlackTriageBotInvokerSecretAccessKey:
|
||||
Description: The AWS API Access Key Secret Key of the SlackTriageBotInvoker
|
||||
Value: !GetAtt SlackTriageBotInvokerAccessKey.SecretAccessKey
|
||||
SlackTriageBotMozDefSQSQueueArn:
|
||||
Description: The ARN of the MozDef SQS Queue
|
||||
Value: !GetAtt SlackTriageBotMozDefQueue.Arn
|
||||
SlackTriageBotMozDefSQSQueueName:
|
||||
Description: The Name of the MozDef SQS Queue
|
||||
Value: !GetAtt SlackTriageBotMozDefQueue.QueueName
|
||||
SlackTriageBotMozDefSQSQueueUrl:
|
||||
Description: The Url of the MozDef SQS Queue
|
||||
Value: !Ref SlackTriageBotMozDefQueue
|
||||
|
|
Загрузка…
Ссылка в новой задаче