├── README.md ├── lambda.yml ├── pipeline.yml └── src ├── aktion.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # AKTION 2 | 3 | AKTION is a script that gathers the latest AWS Services and Actions, compares it to last weeks services and actions and sends the difference (the new ones) to your chosen email address. The aim of this is to help keep track of the new releases from AWS for policy management. 4 | 5 | ## Deployment: 6 | 7 | Included is a CodePipeline defined in CloudFormation. This bundles up the Lambda and it's dependencies via CodeBuild, and uploads the resulting zip to S3 where it is then referenced in a CloudFormation deployment step for the Lambda itself. The pipeline updates itself based on the pipeline.yml included in the repo. 8 | 9 | ### First time setup: 10 | 11 | 1. Add you sending and receiving email addresses to AWS SES in your chosen region. Verify them by confirming the subscription email it sends. 12 | 2. Make a Secret in Secrets Manager called 'Github' and place two values in it: 13 | - PersonalAccessToken: A Personal Access Token from Github 14 | - WebhookSecret: A random string for adding access control to the CodePipeline webhook 15 | 3. Deploy the `pipeline.yml` via CloudFormation 16 | 4. For automatic pipeline execution on push events, configure the Webhook URL that is exported from the resulting stack in your Github repo's Webhook settings. 17 | -------------------------------------------------------------------------------- /lambda.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | 5 | LambdaBucketName: 6 | Type: String 7 | Description: The name of the S3 bucket that has the code 8 | 9 | S3FunctionCodeVersion: 10 | Type: String 11 | Description: The S3ObjectVersion for the function code 12 | 13 | SesRegion: 14 | Type: String 15 | Default: us-east-1 16 | 17 | SesSenderEmail: 18 | Type: String 19 | Description: >- 20 | Email address to display as the sender of notifications. 21 | Address or Domain must be verified in SES. 22 | 23 | SesRecieverEmail: 24 | Type: String 25 | Description: >- 26 | Email address to send notifications to. 27 | Address or Domain must be verified in SES. 28 | 29 | Resources: 30 | 31 | AktionBucket: 32 | Type: AWS::S3::Bucket 33 | DeletionPolicy: Retain 34 | 35 | ScheduledRule: 36 | Type: AWS::Events::Rule 37 | Properties: 38 | ScheduleExpression: cron(00 09 ? * MON *) 39 | State: ENABLED 40 | Targets: 41 | - Arn: !GetAtt Lambda.Arn 42 | Id: !Ref Lambda 43 | 44 | ScheduledEventPermission: 45 | Type: AWS::Lambda::Permission 46 | Properties: 47 | FunctionName: !Ref Lambda 48 | Action: lambda:InvokeFunction 49 | Principal: events.amazonaws.com 50 | SourceArn: !GetAtt ScheduledRule.Arn 51 | 52 | Lambda: 53 | Type: AWS::Lambda::Function 54 | Properties: 55 | FunctionName: aktion 56 | Handler: aktion.lambda_handler 57 | Timeout: 60 58 | Role: !GetAtt LambdaRole.Arn 59 | Environment: 60 | Variables: 61 | BUCKET_NAME: !Ref AktionBucket 62 | SENDER_EMAIL: !Ref SesSenderEmail 63 | RECEIVER_EMAIL: !Ref SesRecieverEmail 64 | SES_REGION: !Ref SesRegion 65 | Code: 66 | S3Bucket: !Ref LambdaBucketName 67 | S3Key: code.zip 68 | S3ObjectVersion: !Ref S3FunctionCodeVersion 69 | Runtime: python3.7 70 | 71 | LambdaRole: 72 | Type: AWS::IAM::Role 73 | Properties: 74 | RoleName: aktion-lambda 75 | AssumeRolePolicyDocument: 76 | Version: 2012-10-17 77 | Statement: 78 | - Effect: Allow 79 | Principal: 80 | Service: 81 | - lambda.amazonaws.com 82 | Action: 83 | - sts:AssumeRole 84 | Path: / 85 | Policies: 86 | - PolicyName: aktion-lambda 87 | PolicyDocument: 88 | Version: 2012-10-17 89 | Statement: 90 | - Effect: Allow 91 | Action: ses:SendEmail 92 | Resource: 93 | - !Sub arn:aws:ses:${SesRegion}:${AWS::AccountId}:identity/${SesSenderEmail} 94 | - !Sub arn:aws:ses:${SesRegion}:${AWS::AccountId}:identity/${SesRecieverEmail} 95 | - Effect: Allow 96 | Action: 97 | - s3:GetObject* 98 | - s3:PutObject* 99 | Resource: 100 | - !Sub arn:aws:s3:::${AktionBucket}/* 101 | - !Sub arn:aws:s3:::${AktionBucket} 102 | - Effect: Allow 103 | Action: 104 | - logs:CreateLogGroup 105 | - logs:CreateLogStream 106 | - logs:PutLogEvents 107 | Resource: arn:aws:logs:*:*:* 108 | -------------------------------------------------------------------------------- /pipeline.yml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | 3 | GithubOwner: 4 | Type: String 5 | Description: Name of the owner (Organization or person) in Github that owns the repo 6 | Default: alanakirby 7 | 8 | GithubRepo: 9 | Type: String 10 | Description: Name of Github repo 11 | Default: aktion 12 | 13 | GithubBranch: 14 | Type: String 15 | Description: Name of Github branch 16 | Default: master 17 | 18 | SesRegion: 19 | Type: String 20 | Default: us-east-1 21 | 22 | SesSenderEmail: 23 | Type: String 24 | Description: >- 25 | Email address to display as the sender of notifications. 26 | Address or Domain must be verified in SES. 27 | 28 | SesRecieverEmail: 29 | Type: String 30 | Description: >- 31 | Email address to send notifications to. 32 | Address or Domain must be verified in SES. 33 | 34 | Resources: 35 | 36 | PipelineS3Bucket: 37 | Type: AWS::S3::Bucket 38 | DependsOn: AutomationRole 39 | 40 | LambdaS3Bucket: 41 | Type: AWS::S3::Bucket 42 | DependsOn: AutomationRole 43 | Properties: 44 | VersioningConfiguration: 45 | Status: Enabled 46 | 47 | Pipeline: 48 | DependsOn: 49 | - LambdaS3Bucket 50 | Type: AWS::CodePipeline::Pipeline 51 | Properties: 52 | Name: !Ref GithubRepo 53 | RoleArn: !GetAtt AutomationRole.Arn 54 | RestartExecutionOnUpdate: true 55 | ArtifactStore: 56 | Type: S3 57 | Location: !Ref PipelineS3Bucket 58 | Stages: 59 | - Name: Source 60 | Actions: 61 | - Name: Github 62 | ActionTypeId: 63 | Category: Source 64 | Owner: ThirdParty 65 | Version: 1 66 | Provider: GitHub 67 | OutputArtifacts: 68 | - Name: source 69 | Configuration: 70 | Branch: !Ref GithubBranch 71 | OAuthToken: '{{resolve:secretsmanager:Github:SecretString:PersonalAccessToken}}' 72 | Owner: !Ref GithubOwner 73 | Repo: !Ref GithubRepo 74 | RunOrder: 1 75 | - Name: SelfUpdate 76 | Actions: 77 | - Name: Pipeline 78 | ActionTypeId: 79 | Category: Deploy 80 | Owner: AWS 81 | Provider: CloudFormation 82 | Version: '1' 83 | RunOrder: 1 84 | InputArtifacts: 85 | - Name: source 86 | Configuration: 87 | ActionMode: REPLACE_ON_FAILURE 88 | Capabilities: CAPABILITY_NAMED_IAM 89 | RoleArn: !GetAtt AutomationRole.Arn 90 | StackName: !Ref AWS::StackName 91 | TemplatePath: source::pipeline.yml 92 | ParameterOverrides: !Sub | 93 | { 94 | "GithubOwner": "${GithubOwner}", 95 | "GithubRepo": "${GithubRepo}", 96 | "GithubBranch": "${GithubBranch}", 97 | "SesRegion": "${SesRegion}", 98 | "SesSenderEmail": "${SesSenderEmail}", 99 | "SesRecieverEmail": "${SesRecieverEmail}" 100 | } 101 | - Name: Build 102 | Actions: 103 | - Name: Lambda 104 | ActionTypeId: 105 | Category: Build 106 | Owner: AWS 107 | Provider: CodeBuild 108 | Version: '1' 109 | RunOrder: 1 110 | InputArtifacts: 111 | - Name: source 112 | OutputArtifacts: 113 | - Name: output 114 | Configuration: 115 | ProjectName: !Ref Deploy 116 | - Name: Deploy 117 | Actions: 118 | - Name: Lambda 119 | InputArtifacts: 120 | - Name: source 121 | - Name: output 122 | ActionTypeId: 123 | Category: Deploy 124 | Owner: AWS 125 | Provider: CloudFormation 126 | Version: '1' 127 | RunOrder: 1 128 | Configuration: 129 | ActionMode: REPLACE_ON_FAILURE 130 | Capabilities: CAPABILITY_NAMED_IAM 131 | RoleArn: !GetAtt AutomationRole.Arn 132 | StackName: aktion-lambda 133 | TemplatePath: source::lambda.yml 134 | ParameterOverrides: !Sub | 135 | { 136 | "LambdaBucketName": "${LambdaS3Bucket}", 137 | "SesRegion": "${SesRegion}", 138 | "SesSenderEmail": "${SesSenderEmail}", 139 | "SesRecieverEmail": "${SesRecieverEmail}", 140 | "S3FunctionCodeVersion": { "Fn::GetParam": [ "output", "output.json", "VersionId" ] } 141 | } 142 | 143 | Deploy: 144 | Type: AWS::CodeBuild::Project 145 | Properties: 146 | Name: aktion 147 | ServiceRole: !GetAtt AutomationRole.Arn 148 | TimeoutInMinutes: 5 149 | Artifacts: 150 | Type: CODEPIPELINE 151 | Environment: 152 | Type: LINUX_CONTAINER 153 | ComputeType: BUILD_GENERAL1_SMALL 154 | Image: aws/codebuild/standard:2.0 155 | Source: 156 | Type: CODEPIPELINE 157 | BuildSpec: 158 | !Sub | 159 | version: 0.2 160 | artifacts: 161 | discard-paths: yes 162 | files: 163 | - output.json 164 | phases: 165 | install: 166 | runtime-versions: 167 | python: 3.7 168 | build: 169 | commands: 170 | - cd src; 171 | - pip3 install -r requirements.txt -t .; 172 | - zip -r code.zip .; 173 | post_build: 174 | commands: 175 | - du -hs * 176 | - aws s3api put-object --bucket ${LambdaS3Bucket} --key code.zip --body code.zip --output json > ../output.json; 177 | 178 | AutomationRole: 179 | Type: AWS::IAM::Role 180 | Properties: 181 | RoleName: aktion 182 | AssumeRolePolicyDocument: 183 | Version: 2012-10-17 184 | Statement: 185 | - Effect: Allow 186 | Principal: 187 | Service: 188 | - codepipeline.amazonaws.com 189 | - codebuild.amazonaws.com 190 | - cloudformation.amazonaws.com 191 | Action: 192 | - sts:AssumeRole 193 | Policies: 194 | - PolicyName: aktion 195 | PolicyDocument: 196 | Version: 2012-10-17 197 | Statement: 198 | - Effect: Allow 199 | Action: cloudformation:* 200 | Resource: '*' 201 | - Effect: Allow 202 | Action: codepipeline:* 203 | Resource: '*' 204 | - Effect: Allow 205 | Action: lambda:* 206 | Resource: '*' 207 | - Effect: Allow 208 | Action: codebuild:* 209 | Resource: '*' 210 | - Effect: Allow 211 | Action: secretsmanager:GetSecretValue 212 | Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:Github-?????? 213 | - Effect: Allow 214 | Action: 215 | - logs:CreateLogGroup 216 | - logs:CreateLogStream 217 | - logs:PutLogEvents 218 | Resource: arn:aws:logs:*:*:* 219 | - Effect: Allow 220 | Action: events:* 221 | Resource: '*' 222 | - Effect: Allow 223 | Action: s3:* 224 | Resource: '*' 225 | - Effect: Allow 226 | Action: iam:* 227 | Resource: 228 | - !Sub arn:aws:iam::${AWS::AccountId}:policy/aktion* 229 | - !Sub arn:aws:iam::${AWS::AccountId}:role/aktion* 230 | -------------------------------------------------------------------------------- /src/aktion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import boto3 4 | from botocore.exceptions import ClientError 5 | import os 6 | import subprocess 7 | import requests 8 | import json 9 | import difflib 10 | import filecmp 11 | import pyfiglet 12 | import re 13 | 14 | def lambda_handler(event, context): 15 | # Banner 16 | print('___________________________________________________________________\n\n') 17 | ascii_banner = pyfiglet.figlet_format(' AKTION') 18 | print(ascii_banner) 19 | print(' Created by Alana Kirby.') 20 | print('___________________________________________________________________') 21 | print('This script finds out what the latest AWS Services and Actions are\nand sends an email with them inside it.\n') 22 | print('Purpose: Keep up to date with policy management.') 23 | print('___________________________________________________________________') 24 | 25 | # Set variables 26 | print('Connecting to bucket.......................................( ͡° ͜ʖ ͡°)') 27 | s3_resource = boto3.resource('s3') 28 | s3_client = boto3.client('s3') 29 | bucket = os.environ.get('BUCKET_NAME') 30 | 31 | past_output_filename = '/tmp/previous.txt' 32 | past_output_s3_key = 'previous.txt' 33 | current_output_filename = '/tmp/latest.txt' 34 | current_output_s3_key = 'latest.txt' 35 | diff_filename = '/tmp/difference.txt' 36 | diff_s3_key = 'difference.txt' 37 | 38 | # Download last file 39 | print('Loading previous services................................(~˘▾˘)~') 40 | s3_client.download_file(bucket, current_output_s3_key, past_output_filename) 41 | 42 | print('Gathering today\'s services..................................~(˘▾˘~)') 43 | response = requests.get('https://awspolicygen.s3.amazonaws.com/js/policies.js') 44 | content_json = response.content.decode('UTF-8').lstrip('app.PolicyEditorConfig=') 45 | content_dict = json.loads(content_json)['serviceMap'] 46 | 47 | service_actions = [] 48 | for service, values in content_dict.items(): 49 | service_name = values['StringPrefix'] 50 | for action in values['Actions']: 51 | service_actions.append(f'{service_name}:{action}') 52 | 53 | service_actions.sort() 54 | 55 | service_string = '\n'.join(service_actions) 56 | 57 | with open(current_output_filename, 'w') as f: 58 | f.write(service_string) 59 | 60 | # Compare files 61 | print('Comparing services...................(ノ◕ヮ◕)ノ*:・゚✧ ✧゚・: *ヽ(◕ヮ◕ヽ)') 62 | past_file = open(past_output_filename).readlines() 63 | latest_file = open(current_output_filename).readlines() 64 | 65 | with open(diff_filename, 'w') as d: 66 | for line in difflib.unified_diff(past_file, latest_file): 67 | d.write(line) 68 | 69 | # array 70 | array = [] 71 | 72 | # Delete dumb extra bits 73 | with open(diff_filename, 'r') as file: 74 | for line in file.readlines(): 75 | if re.match(r"^\+\w", line): 76 | remove = re.sub(r"^\++", '', line) 77 | if remove.strip() is not '': 78 | array.append(remove) 79 | 80 | with open(diff_filename, 'w') as c: 81 | result = ''.join(array) 82 | c.write(result) 83 | 84 | diff_new = open(diff_filename, 'r') 85 | diff_read = diff_new.read() 86 | 87 | 88 | SENDER = os.environ.get('SENDER_EMAIL') 89 | RECIPIENT = os.environ.get('RECEIVER_EMAIL') 90 | SES_REGION = os.environ.get('SES_REGION') 91 | 92 | # if its empty don't send it 93 | if os.path.getsize('/tmp/difference.txt') > 0: 94 | # n = print(new_services) 95 | print('___________________________________________________________________') 96 | print('Any new services and actions will appear in the difference.txt file\nand will be sent to an email weekly.') 97 | print('___________________________________________________________________') 98 | 99 | # AWS SES 100 | SUBJECT = 'New AWS Services and Actions have been released this week!' 101 | BODY_TEXT = ('New AWS Services and Actions have been released this week!\n' 102 | 'Here are the newbies to action on:\n' + str(diff_read)) 103 | BODY_HTML = """ 104 | 105 | 106 |

New AWS Services and Actions have been released this week!

107 |

Here are the newbies to action on:\n

108 | """ + str(diff_read) + """ 109 | 110 | """ 111 | 112 | CHARSET = 'UTF-8' 113 | 114 | ses_client = boto3.client('ses',region_name=SES_REGION) 115 | 116 | try: 117 | ses_response = ses_client.send_email( 118 | Destination={ 119 | 'ToAddresses': [ 120 | RECIPIENT, 121 | ], 122 | }, 123 | Message={ 124 | 'Body': { 125 | 'Text': { 126 | 'Charset': CHARSET, 127 | 'Data': BODY_TEXT, 128 | }, 129 | 'Html': { 130 | 'Charset': CHARSET, 131 | 'Data': BODY_HTML, 132 | }, 133 | }, 134 | 'Subject': { 135 | 'Charset': CHARSET, 136 | 'Data': SUBJECT, 137 | }, 138 | }, 139 | Source=SENDER, 140 | ) 141 | 142 | except ClientError as e: 143 | print(e.response['Error']['Message']) 144 | else: 145 | print("Email sent! Message ID:"), 146 | print(ses_response['MessageId']) 147 | else: 148 | print('___________________________________________________________________') 149 | print('Any new services and actions will appear in the difference.txt file\nand will be sent to your email weekly.') 150 | print('___________________________________________________________________') 151 | 152 | # AWS SES 153 | SUBJECT = 'AWS has taken a holiday this week!' 154 | BODY_TEXT = ('AWS has taken a holiday this week!\n' 155 | 'No new services or actions this week.') 156 | BODY_HTML = """ 157 | 158 | 159 |

AWS has taken a holiday this week!

160 |

No new services or actions this week.

161 | 162 | """ 163 | 164 | CHARSET = 'UTF-8' 165 | 166 | ses_client = boto3.client('ses',region_name=SES_REGION) 167 | 168 | try: 169 | ses_response = ses_client.send_email( 170 | Destination={ 171 | 'ToAddresses': [ 172 | RECIPIENT, 173 | ], 174 | }, 175 | Message={ 176 | 'Body': { 177 | 'Text': { 178 | 'Charset': CHARSET, 179 | 'Data': BODY_TEXT, 180 | }, 181 | 'Html': { 182 | 'Charset': CHARSET, 183 | 'Data': BODY_HTML, 184 | }, 185 | }, 186 | 'Subject': { 187 | 'Charset': CHARSET, 188 | 'Data': SUBJECT, 189 | }, 190 | }, 191 | Source=SENDER, 192 | ) 193 | 194 | except ClientError as e: 195 | print(e.response['Error']['Message']) 196 | else: 197 | print("Email sent! Message ID:"), 198 | print(ses_response['MessageId']) 199 | 200 | # Store latest results in S3 201 | print('Storing results in S3.......................ヽ(〃^▽^〃)ノ') 202 | s3_client.upload_file(past_output_filename, bucket, past_output_s3_key) 203 | s3_client.upload_file(current_output_filename, bucket, current_output_s3_key) 204 | s3_client.upload_file(diff_filename, bucket, diff_s3_key) 205 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | jq 3 | awscli 4 | requests 5 | pyfiglet 6 | --------------------------------------------------------------------------------