Using AWS Lambda With Slack To Block Malicious IPs

Vinayak Pandey
4 min readJan 18, 2025

--

In this post, we’ll see how we can use AWS Lambda with Slack to block malicious IPs accessing our CloudFront. CloudFront is protected by WAF and this WAF has a SQL injection protection rule along with a ip_blacklist rule. Now we want to add IPs making SQL injection requests to ip_blacklist WAF Ipset.

Step 1: Create a Slack Webhook URL. For this, sign in to your Slack account and select the workspace where you want to create the webhook.

Go to the Slack API website (https://api.slack.com/apps) and click on the “Create a Slack App” button.

Give your app a name and select the workspace where you want to install it. Click “Create App”.

On the next page, scroll down to the “Add features and functionality” section and click on “Incoming Webhooks”.

Activate the Incoming Webhooks feature by setting the toggle to “On”. Then click on the “Add New Webhook to Workspace” button.
Select the channel where you want to send the webhook messages and click “Authorize”.

You will be taken back to the Incoming Webhooks page. Scroll down to the “Webhook URLs for Your Workspace” section and copy the webhook URL.

Step 2: Create a Lambda function with following Python code. This will query WAF CloudWatch Logs to find IP addresses making malicious requests.

You can trigger this based on Eventbridge rule or CloudWatch alarm based on WAF metrics.

import boto3
import datetime
import requests
import json

# Initialize the CloudWatch Logs client
client = boto3.client('logs',region_name='us-east-1')

# Define the log group and query string
log_group_name = 'aws-waf-logs-prd'
SLACK_WEBHOOK_URL = '<SLACK_WEBHOOK_URL>'

# Function to query CloudWatch Logs
def query_cloudwatch_logs():
query_string = """
fields httpRequest.clientIp,httpRequest.country
| filter terminatingRuleId='default-sqli-rule'
| stats count(*) as requestCount by httpRequest.clientIp,httpRequest.country
| filter requestCount > 5
| sort requestCount desc
"""
end_time = int(datetime.datetime.utcnow().timestamp() * 1000) # Current time in ms
start_time = end_time - (60 * 60 * 1000) # 1 hour ago in ms

response = client.start_query(
logGroupName=log_group_name,
startTime=start_time,
endTime=end_time,
queryString=query_string
)

query_id = response['queryId']

# Wait for the query to complete
status = 'Running'
while status in ['Running', 'Scheduled']:
result = client.get_query_results(queryId=query_id)
status = result['status']

if status == 'Complete':
try:
# Check structure and handle missing or malformed data
results = []
for row in result['results']:
row_data = {field['field']: field['value'] for field in row if 'field' in field and 'value' in field}
results.append(row_data)
return results
except KeyError as e:
raise Exception(f"Unexpected structure in query results: {e}")
else:
raise Exception(f"Query failed with status: {status}")
# Function to send message to Slack
def send_to_slack(results):
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Potential Malicious IPs Detected Making SQL Injection Requests:*"
}
}
]

for result in results:
ip = result.get("httpRequest.clientIp", "N/A")
country = result.get("httpRequest.country", "N/A")
count = result.get("requestCount", "N/A")
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"IP: `{ip}`, Country: `{country}`, Requests: `{count}`"
},
"accessory": {
"type": "button",
"text": {"type": "plain_text", "text": "Block"},
"style": "primary",
"action_id": f"approve_{ip}"
}
})

blocks.append({"type": "divider"})

headers = {"Content-Type": "application/json"}
data = {
"text": "Potential Malicious IPs Detected",
"blocks": blocks
}

response = requests.post(SLACK_WEBHOOK_URL, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print("Message sent to Slack successfully.")
else:
print(f"Failed to send message to Slack: {response.text}")


def lambda_handler(event,context):
results = query_cloudwatch_logs()
send_to_slack(results)

Once the Lambda is triggered, you may see a message like this.

Step 3: Create another Lambda function with following Python code. Update values for IPSET_ID and IPSET_NAME and [‘user’][‘username’].

import json
import base64
import urllib.parse
import boto3

waf_client = boto3.client('wafv2',region_name='us-east-1')
IPSET_ID=''
IPSET_NAME=''

def lambda_handler(event, context):
decoded_body = base64.b64decode(event["body"]).decode("utf-8")
print(urllib.parse.unquote(decoded_body))
input_string=urllib.parse.unquote(decoded_body)
json_string = input_string.replace("payload=", "").strip()


payload = json.loads(json_string)

if payload['user']['username'] == 'vinayak': #Slack username
blocks=payload['message']['blocks']
print(blocks)
for action in payload['actions']:
blacklisted_ip=action['action_id'].split('_')[1]
response = waf_client.get_ip_set(Name=IPSET_NAME,Scope='CLOUDFRONT',Id=IPSET_ID)
ipsetv4_lock_token=response['LockToken']
current_ipv4_address=response['IPSet']['Addresses']
current_ipv4_address.append(blacklisted_ip+'/32')
try:
response = waf_client.update_ip_set(Name=IPSET_NAME,Scope='CLOUDFRONT',Id=IPSET_ID,Addresses=current_ipv4_address,LockToken=ipsetv4_lock_token)
print("IPV4 Address Blacklisted")
except botocore.exceptions.ClientError as error:
return {'message': "Error updating WAF IPSet. Please reach out to DevOps team"}

return {
'statusCode': 200,
'body': json.dumps('IP blacklisted!')
}

Add following permission to this Lambda

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"wafv2:GetIPSet",
"wafv2:ListIPSets",
"wafv2:UpdateIPSet"
],
"Resource": "*"
}
]
}

For this demo, we have enabled Lambda Function URL.Now go back to your Slack App and select Interactivity & Shortcuts and Enable Interactivity and provide your Lambda function URL as Request URL.

Step 4: Now go back to Slack channel and click Block and these IP addresses will be added to the IPSet which you can use in your WAF to block requests if it’s coming from IPs in this IPSet.

Note: For queries with escape characters, can use this format

query_string = r"""
fields httpRequest.clientIp, httpRequest.country
| filter httpRequest.uri like /.*(\.php|\.xml|wp-).*/
| filter action='ALLOW'
| stats count(*) as requestCount by httpRequest.clientIp,httpRequest.country
| filter requestCount > 10
| sort requestCount asc

--

--

Vinayak Pandey
Vinayak Pandey

Written by Vinayak Pandey

Experienced Cloud Engineer with a knack of automation. Linkedin profile: https://www.linkedin.com/in/vinayakpandeyit/

No responses yet