Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nthienan committed Dec 12, 2021
0 parents commit b56a39c
Show file tree
Hide file tree
Showing 22 changed files with 650 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[**/*.md]
trim_trailing_whitespace = false

[**/*.py]
indent_size = 4
indent_style = space
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.swp
package-lock.json
__pycache__
.pytest_cache
.venv
*.egg-info

# CDK asset staging directory
.cdk.staging
cdk.out
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

# Welcome to your CDK Python project!

This is a blank project for Python development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

This project is set up like a standard Python project. The initialization
process also creates a virtualenv within this project, stored under the `.venv`
directory. To create the virtualenv it assumes that there is a `python3`
(or `python` for Windows) executable in your path with access to the `venv`
package. If for any reason the automatic creation of the virtualenv fails,
you can create the virtualenv manually.

To manually create a virtualenv on MacOS and Linux:

```
$ python3 -m venv .venv
```

After the init process completes and the virtualenv is created, you can use the following
step to activate your virtualenv.

```
$ source .venv/bin/activate
```

If you are a Windows platform, you would activate the virtualenv like this:

```
% .venv\Scripts\activate.bat
```

Once the virtualenv is activated, you can install the required dependencies.

```
$ pip install -r requirements.txt
```

At this point you can now synthesize the CloudFormation template for this code.

```
$ cdk synth
```

To add additional dependencies, for example other CDK libraries, just add
them to your `setup.py` file and rerun the `pip install -r requirements.txt`
command.

## Useful commands

* `cdk ls` list all stacks in the app
* `cdk synth` emits the synthesized CloudFormation template
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk docs` open CDK documentation

Enjoy!
32 changes: 32 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from database import AwsActivityDatabaseStack
from notification import AwsActivityNotificationStack
from login import AwsSignInActivityStack


app = cdk.App()

us_east_1 = cdk.Environment(region=os.environ["CDK_DEFAULT_REGION"], account=os.getenv('CDK_DEFAULT_ACCOUNT'))

# database
db_stack = AwsActivityDatabaseStack(app, 'aws-activity-db', env=us_east_1,
description='Tracking AWS activities for security compliance'
)

# notification
notification_stack = AwsActivityNotificationStack(app, 'aws-activity-notification', env=us_east_1,
description='Notify AWS activities for security compliance'
)

# sign-in activity
AwsSignInActivityStack(app, 'aws-sign-in-activity', env=us_east_1,
dynamodb_table=db_stack.table,
notification_topic=notification_stack.topic,
description='Tracking AWS sign-in activities for security compliance'
)

app.synth()
60 changes: 60 additions & 0 deletions assets/lambda-functions/count-failed-sign-in-attempt/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging
import time
import boto3
import os
from boto3.dynamodb.conditions import Key, Attr


logging.basicConfig(level=logging.DEBUG)
logger=logging.getLogger(__name__)

def handler(event, context):
logger.setLevel(logging.DEBUG)

TABLE_NAME = os.getenv('DYNAMODB_TABLE_NAME')
user_identity_index = {
'name': 'UserIdentityIndex',
'hash_key': 'userIdentity',
'sort_key': 'timestamp'
}


user_identity = event['userIdentity']

# last 1 hour = now - 1h
# seconds
now = int(time.time())
last_one_hour = now - 1 * 60 * 60

# table = boto3.resource('dynamodb', region_name='us-east-1').Table(TABLE_NAME)
table = boto3.resource('dynamodb').Table(TABLE_NAME)
results = []
last_evaluated_key = None

while True:
if not last_evaluated_key:
response = table.query(
IndexName=user_identity_index['name'],
KeyConditionExpression=Key(user_identity_index['hash_key']).eq(user_identity) & Key(user_identity_index['sort_key']).between(last_one_hour, now),
FilterExpression=Attr('eventName').eq('ConsoleLogin') & Attr('detail.responseElements.ConsoleLogin').eq('Failure'),
ScanIndexForward=False,
)
else:
response = table.query(
IndexName=user_identity_index['name'],
KeyConditionExpression=Key(user_identity_index['hash_key']).eq(user_identity) & Key(user_identity_index['sort_key']).between(last_one_hour, now),
FilterExpression=Attr('eventName').eq('ConsoleLogin') & Attr('detail.responseElements.ConsoleLogin').eq('Failure'),
ScanIndexForward=False,
ExclusiveStartKey=last_evaluated_key
)
results.extend(response['Items'])

last_evaluated_key = response.get('LastEvaluatedKey')
if not last_evaluated_key:
break
logger.info(len(results))

event['failedAttempts'] = len(results)
logger.debug(event)

return event
Empty file.
117 changes: 117 additions & 0 deletions assets/lambda-functions/slack-notification/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import logging
import requests
import json
from datetime import datetime


logging.basicConfig(level=logging.DEBUG)
logger=logging.getLogger(__name__)

SLACK_CHANNELS = {
'alarm-aws': 'https://hooks.slack.com/services/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'
}

def handler(event, context):
logger.setLevel(logging.DEBUG)
logger.debug(event)

message = json.loads(event['Records'][0]['Sns']['Message'])
message_attributes = event['Records'][0]['Sns']['MessageAttributes']

fields = []

text = 'See detail below'
reason = message_attributes['reason']['Value']
if reason == 'ManyFailedSignInAttempt':
text = 'There are many failed sign-in attempts in last one hour'
fields.extend([
{
'title': 'User',
"value": message['detail']['userIdentity']['userName'],
'short': True
},
{
'title': 'Failed attempt',
"value": message['failedAttempts'],
'short': True
}
])
elif reason == 'NoMFAUsed':
text = 'Detected MFA not used'
fields.extend([
{
'title': 'User',
"value": message['detail']['userIdentity']['userName'],
'short': True
},
{
'title': 'Event name',
"value": message['eventName'],
'short': True
}
])
elif reason == 'RootActivity':
text = 'Detected Root activity'
fields.extend([
{
'title': 'User',
"value": 'Root',
'short': True
},
{
'title': 'Event name',
"value": message['eventName'],
'short': True
}
])

fields.extend([
{
'title': 'IP address',
"value": message['detail']['sourceIPAddress'],
'short': True
},
{
'title': 'Severity',
"value": message_attributes['severity']['Value'],
'short': True
},
{
'title': 'Event ID',
"value": message['id'],
'short': True
}
])

send_slack(
channel=message_attributes['channel']['Value'],
title=text,
fields=fields,
severity=message_attributes['severity']['Value']
)

def send_slack(channel: str, title: str, fields: list, severity='Medium'):
color = '#36a64f'
if severity == 'Medium':
color = '#edaf2b'
elif severity == 'High':
color = '#cc5f00'
elif severity == 'Critical':
color = '#cc0000'

payload = {
'username': 'Cloud Guard',
'attachments': [{
'title': title,
'color': color,
'fields': fields,
'footer': datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC'),
'fallback': 'Required plain-text summary of the attachment.'
}]
}
if severity.lower() == 'critical':
payload['attachments'][0].update({'pretext': '<!here>'})

logger.info(payload)
requests.post(SLACK_CHANNELS.get(channel), data=json.dumps(payload), headers={'Content-Type': 'application/json'})
logger.info('Send Slack notify successfully')
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests==2.26.0
40 changes: 40 additions & 0 deletions assets/lambda-functions/store-sign-in-activity/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging
import time
from dateutil import parser
import boto3
import os
import json


logging.basicConfig(level=logging.DEBUG)
logger=logging.getLogger(__name__)

def handler(event, context):
# logger.setLevel(logging.DEBUG)

# 365 days
ttl = int(time.time()) + 365 * 24 * 60 * 60
timestamp = int(parser.parse(event['time']).timestamp())

event_detail = event['detail']
event_name = event_detail['eventName']
user_identity_type = event_detail['userIdentity']['type']

user_identity = f'{user_identity_type}#Unknown'
if user_identity_type == 'IAMUser':
user_identity = f'{user_identity_type}-{event_detail["userIdentity"]["userName"]}'
elif user_identity_type == 'Root':
user_identity = f'{user_identity_type}#Root'
elif user_identity == 'AssumedRole':
user_identity = f'{user_identity_type}#{event_detail["userIdentity"]["sessionContext"]["sessionIssuer"]["userName"]}'

event = {**event, 'eventName': event_name, 'userIdentity': user_identity, 'timestamp': timestamp, 'ttl': ttl}
logger.debug(json.dumps(event))

# dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.getenv('DYNAMODB_TABLE_NAME'))
table.put_item(Item=event)
logger.info(f'Activity has been stored into database successfully')

return event
Empty file.
10 changes: 10 additions & 0 deletions cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"app": "python3 app.py",
"context": {
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
"@aws-cdk/core:stackRelativeExports": true,
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
}
}
1 change: 1 addition & 0 deletions database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .infra import AwsActivityDatabaseStack
28 changes: 28 additions & 0 deletions database/infra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from aws_cdk import (
Stack,
aws_dynamodb as dynamodb
)
from constructs import Construct


class AwsActivityDatabaseStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)

# dynamodb
self.table = dynamodb.Table(self, 'DynamoDB',
table_name='aws-activity',
partition_key=dynamodb.Attribute(name='id', type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(name='timestamp', type=dynamodb.AttributeType.NUMBER),
time_to_live_attribute='ttl',
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST
)

# DynamoDB GSI
self.table.add_global_secondary_index(
index_name='UserIdentityIndex',
partition_key=dynamodb.Attribute(name='userIdentity', type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(name='timestamp', type=dynamodb.AttributeType.NUMBER),
non_key_attributes=['id', 'eventName', 'detail'],
projection_type=dynamodb.ProjectionType.INCLUDE
)
1 change: 1 addition & 0 deletions login/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .infra import AwsSignInActivityStack
Loading

0 comments on commit b56a39c

Please sign in to comment.