From 648fca735deb0787a75dc6063a7f69661cbd33c6 Mon Sep 17 00:00:00 2001 From: sfanous Date: Thu, 8 Jul 2021 18:46:40 -0400 Subject: [PATCH] Initial commit --- .gitignore | 7 + .travis.yml | 9 + CHANGES.md | 5 + LICENSE.md | 19 + Makefile | 4 + README.md | 170 ++++++ go.mod | 14 + go.sum | 93 ++++ security-groups-manager/cmd/configuration.go | 125 +++++ security-groups-manager/cmd/controller.go | 189 +++++++ security-groups-manager/cmd/delta.go | 533 ++++++++++++++++++ security-groups-manager/cmd/environment.go | 89 +++ security-groups-manager/cmd/main.go | 56 ++ security-groups-manager/cmd/main_test.go | 548 +++++++++++++++++++ security-groups-manager/cmd/tabulate.go | 415 ++++++++++++++ security-groups-manager/testdata/step_1.json | 197 +++++++ security-groups-manager/testdata/step_2.json | 191 +++++++ security-groups-manager/testdata/step_3.json | 15 + security-groups-manager/testdata/step_4.json | 13 + template.yaml | 107 ++++ 20 files changed, 2799 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGES.md create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 security-groups-manager/cmd/configuration.go create mode 100644 security-groups-manager/cmd/controller.go create mode 100644 security-groups-manager/cmd/delta.go create mode 100644 security-groups-manager/cmd/environment.go create mode 100644 security-groups-manager/cmd/main.go create mode 100644 security-groups-manager/cmd/main_test.go create mode 100644 security-groups-manager/cmd/tabulate.go create mode 100644 security-groups-manager/testdata/step_1.json create mode 100644 security-groups-manager/testdata/step_2.json create mode 100644 security-groups-manager/testdata/step_3.json create mode 100644 security-groups-manager/testdata/step_4.json create mode 100644 template.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f235c04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.aws +.aws-sam +.vscode +SecurityGroupsManager Architecture.drawio +SecurityGroupsManager.code-workspace +cloudFormation +samconfig.toml \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c31266c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: + - "1.16.x" +script: + - go test -v ./security-groups-manager/cmd +notifications: + email: + on_failure: always + on_success: never \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..0d7c181 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## v1.0.0 + +- First public version \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9580c9d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2021 Sherif Fanous + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6699b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: build + +build: + sam build diff --git a/README.md b/README.md new file mode 100644 index 0000000..f469a2b --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +[![Build Status](https://travis-ci.com/sfanous/SecurityGroupsManager.svg?branch=master)](https://travis-ci.com/sfanous/SecurityGroupsManager) +[![Release](https://img.shields.io/github/v/release/sfanous/SecurityGroupsManager.svg?style=flat)](https://github.com/sfanous/SecurityGroupsManager/releases/latest) + +# SecurityGroupsManager + +## Overview + +SecurityGroupsManager is an AWS serverless application. The core of the application is a Lambda Function written in Go. + +## Problem Statment + +The premise from SecurityGroupsManager is simple. + +You provide a desired state of one or more security groups and SecurityGroupsManager monitors the security groups in question and ensures they are always kept in sync with the desired state. + +Additionally, SecurityGroupsManager addresses AWS' security groups limitation where the source or destination of a security group rule requires any of the following + +1. An individual IPv4 or IPv6 address, in CIDR block notation +2. A range of IPv4 or IPv6 addresses, in CIDR block notation +3. A prefix list ID +4. Another security group + +The list above excludes fully qualified domain names (FQDN). This is an issue for home users like myself who don't have a static IP address they can configure their security group rules with. + +With no static IP address, the simplest solution commonly resorted to is to create wide open security group rules using 0.0.0.0/0 as the source. This allows the whole world to send traffic to the destination port but goes against the principle of least privilege. + +A dynamic DNS service takes your dynamic IP address and makes it act as though it is static by pointing a static hostname to it. A dynamic DNS update client running on your PC or router is configured to periodically check for changes to your IP address. If your IP address changes, the dynamic DNS update client updates your dynamic DNS hostname with the current IP address. + +## Solution + +You've determined the desired state of your security groups and setup a dynamic DNS hostname. You've made sure the dynamic DNS hostname si always updated to point to your currently assigned dynamic IP address. Now all you need is a solution that monitors your security groups and ensures they are always kept in sync with the desired state. + +That's where SecurityGroupsManager comes in. You provide the Lambda Function with a configuration that describes the desired state of one or more security group. On each invocation the Lamda Function compares the as is state against the desired state of each configured security group and executes any required remediations. + +## Architecture +![Imgur](https://i.imgur.com/E652VQq.png) + +## Deployment + +The easiest way to deploy SecurityGroupsManager is to use the CloudFormation Quick Create Stack Launch URL + +[![](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home#/stacks/quickcreate?templateURL=https://manage-security-groups-cloudformation-artifacts.s3.ca-central-1.amazonaws.com/template.yaml&stackName=SecurityGroupsManagerStack) + +This will open the CloudFormation Quick Create Stack Console + +![Imgur](https://i.imgur.com/J7udell.png) + +You'll need to make sure you're in the AWS region in which you want CloudFormation to create your resources. To switch regions, choose the region list to the right of your account information on the navigation bar. + +## CloudFormation Stack Setup + +- **Stack name** + - This is the name of the CloudFormation Stack that will be created + - There's no need to make any changes to here, but if you feel that you want to name your stack differently then Feel free to change the value from the default **SecurityGroupsManagerStack** +- **Configuration** + - This parameter sets the initial value of the Lambda Function's CONFIGURATION environment variable. After the Lambda Function is created you can always update the value of the CONFIGURATION environment variable from the Lambda Function console + - This is the where you enter your security groups desired state + - The configuration is a superset of the `aws ec2 describe-security-groups` JSON output. Here's a sample configuration + + ```json + { + "SecurityGroups": [ + { + "Description": "Test SG", + "GroupId": "sg-6d9a02303c07f74e2", + "GroupName": "Test SG", + "IpPermissions": [ + { + "FromPort": 22, + "Hosts": [ + { + "Description": "My home IP address", + "FQDN": "myHome.hopto.org" + } + ], + "IpProtocol": "tcp", + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 22, + "UserIdGroupPairs": [] + }, + { + "FromPort": 80, + "Hosts": [ + { + "Description": "My home IP address", + "FQDN": "myHome.hopto.org" + } + ], + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "1.2.3.4/32" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 80, + "UserIdGroupPairs": [] + } + ], + "IpPermissionsEgress": [ + { + "IpProtocol": "-1", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "UserIdGroupPairs": [] + } + ], + "OwnerId": "467087866041", + "VpcId": "vpc-c86ad37e" + } + ] + } + ``` + + The new addition to this JSON structure is the `Hosts` array within the `IpPermissions` or `IpPermissionsEgress` objects. This is where you configure your dynamic DNS hostname using the `FQDN` attribute. You can define as many host objects within the `Hosts` array as you need. + + In this sample we're defining the following desired state + + - Ingress rule for TCP port 22 to allow traffic from the IPv4 address pointed to by `myHome.hopto.org` + - Ingress rule for TCP port 80 to allow traffic from the IPv4 address pointed to by `myHome.hopto.org` and the static IPv4 address `1.2.3.4` + + The Lambda Function resolves the dynamic DNS hostnames defined using the `FQDN` attribute of each host within the `Hosts` array to IPv4 & IPv6 addresses in CIDR notation and merges the results with any pre-configured `CidrIp` within the `IpRanges` and `Ipv6Ranges` respectively to create a consolidated `IpRanges` and `Ipv6Ranges` arrays then proceeds to compare the desired state with the configured state. In case of a discrepancy the current remediations are determined and applied. + + - The simplest way to create this configuration is as follows + - Execute `aws ec2 describe-security-groups` and copy the full JSON output to your favorite editor + - Add a `Hosts` array to the security group rules you want the Lambda Function to monitor and update + - Copy your edited JSON and paste it into the **Configuration** parameter + +- **EnableDebugMode** + - This parameter sets the initial value of the Lambda Function's DEBUG environment variable. After the Lambda Function is created you can always update the value of the DEBUG environment variable from the Lambda Function console + +- **RateExpressionMinutes** + - This parameter configure the rate expression of the EventBridge rule. The Lambda Function is invoked by an EventBridge rule and this parameter controls the frequency of invocations + +# Sample Output + +SecurityGroupsManager sends its output to CloudWatch Logs. The output is displayed in tabular form + +**The tabular form in CloudWatch Logs will look completely messed up. This is due to text wrapping** + +To be able to view the tabular form as intended +1. Check the `View as text` checkbox +2. Select the whole output for a request (From the beginning of the line that starts with `START RequestId` to the end of the line that starts with `END RequestId`) +3. Copy the selected output +4. Paste the copied output into your favorite text editor and disable text wrapping + +Here are some sample outputs + +- Remediations determined and applied + +![svgur](https://svgshare.com/i/YxU.svg) + +- No remediations determined + +![svgur](https://svgshare.com/i/Yw4.svg) + +- No matching security group found + +![svgur](https://svgshare.com/i/YwV.svg) + +## Important Notes + +- If SecurityGroupsManager encounters a configued security group for which it is unable to find a matching security group in AWS then SecurityGroupsManager will report this as seen in the last sample output. SecurityGroupsManager will not create a new security group in this case. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c386232 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module security-groups-manager + +require ( + github.com/aws/aws-lambda-go v1.23.0 + github.com/aws/aws-sdk-go-v2 v1.6.0 + github.com/aws/aws-sdk-go-v2/config v1.3.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.9.0 + github.com/go-test/deep v1.0.7 + github.com/jedib0t/go-pretty/v6 v6.2.2 + github.com/stretchr/testify v1.6.1 + inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda +) + +go 1.16 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a10d28a --- /dev/null +++ b/go.sum @@ -0,0 +1,93 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-lambda-go v1.23.0 h1:Vjwow5COkFJp7GePkk9kjAo/DyX36b7wVPKwseQZbRo= +github.com/aws/aws-lambda-go v1.23.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/aws/aws-sdk-go-v2 v1.6.0 h1:r20hdhm8wZmKkClREfacXrKfX0Y7/s0aOoeraFbf/sY= +github.com/aws/aws-sdk-go-v2 v1.6.0/go.mod h1:tI4KhsR5VkzlUa2DZAdwx7wCAYGwkZZ1H31PYrBFx1w= +github.com/aws/aws-sdk-go-v2/config v1.3.0 h1:0JAnp0WcsgKilFLiZEScUTKIvTKa2LkicadZADza+u0= +github.com/aws/aws-sdk-go-v2/config v1.3.0/go.mod h1:lOxzHWDt/k7MMidA/K8DgXL4+ynnZYsDq65Qhs/l3dg= +github.com/aws/aws-sdk-go-v2/credentials v1.2.1 h1:AqQ8PzWll1wegNUOfIKcbp/JspTbJl54gNonrO6VUsY= +github.com/aws/aws-sdk-go-v2/credentials v1.2.1/go.mod h1:Rfvim1eZTC9W5s8YJyYYtl1KMk6e8fHv+wMRQGO4Ru0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.1.1 h1:w1ocBIhQkLgupEB3d0uOuBddqVYl0xpubz7HSTzWG8A= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.1.1/go.mod h1:GTXAhrxHQOj9N+J5tYVjwt+rpRyy/42qLjlgw9pz1a0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.0.0 h1:k7I9E6tyVWBo7H9ffpnxDWudtjau6Qt9rnOYgV+ciEQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.0.0/go.mod h1:g3XMXuxvqSMUjnsXXp/960152w0wFS4CXVYgQaSVOHE= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.9.0 h1:SF0h/HR4zUDBbGv6Hf/fbbG6ywTVi9r2DmpIhfZMckI= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.9.0/go.mod h1:XzzkrryeCoPUd9jxcdDnI2/UmlfIp13nBSpjl2SDSCM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.1.1 h1:l7pDLsmOGrnR8LT+3gIv8NlHpUhs7220E457KEC2UM0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.1.1/go.mod h1:2+ehJPkdIdl46VCj67Emz/EH2hpebHZtaLdzqg+sWOI= +github.com/aws/aws-sdk-go-v2/service/sso v1.2.1 h1:alpXc5UG7al7QnttHe/9hfvUfitV8r3w0onPpPkGzi0= +github.com/aws/aws-sdk-go-v2/service/sso v1.2.1/go.mod h1:VimPFPltQ/920i1X0Sb0VJBROLIHkDg2MNP10D46OGs= +github.com/aws/aws-sdk-go-v2/service/sts v1.4.1 h1:9Z00tExoaLutWVDmY6LyvIAcKjHetkbdmpRt4JN/FN0= +github.com/aws/aws-sdk-go-v2/service/sts v1.4.1/go.mod h1:G9osDWA52WQ38BDcj65VY1cNmcAQXAXTsE8IWH8j81w= +github.com/aws/smithy-go v1.4.0 h1:3rsQpgRe+OoQgJhEwGNpIkosl0fJLdmQqF4gSFRjg+4= +github.com/aws/smithy-go v1.4.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jedib0t/go-pretty/v6 v6.2.2 h1:o3McN0rQ4X+IU+HduppSp9TwRdGLRW2rhJXy9CJaCRw= +github.com/jedib0t/go-pretty/v6 v6.2.2/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg= +go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda h1:N1UNOTFyoz00Zw10uv9elxer4zdyqqhsMOOqAFPVTfM= +inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls= diff --git a/security-groups-manager/cmd/configuration.go b/security-groups-manager/cmd/configuration.go new file mode 100644 index 0000000..08e16e5 --- /dev/null +++ b/security-groups-manager/cmd/configuration.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/json" + "log" + "net" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "inet.af/netaddr" +) + +type Configuration struct { + SecurityGroups []SecurityGroup +} + +func NewConfiguration(marshaledConfiguration string) (*Configuration, error) { + configuration := new(Configuration) + + debugf("Unmarshalling configuration") + + if err := json.Unmarshal([]byte(marshaledConfiguration), configuration); err != nil { + log.Printf("Unable to unmarshal configuration: %v", err) + + return nil, err + } + + debugf("Unmarshalled configuration") + + return configuration, nil +} + +type SecurityGroup struct { + Description *string + GroupId *string + GroupName *string + IpPermissions []IpPermission + IpPermissionsEgress []IpPermission + OwnerId *string + Tags []types.Tag + VpcId *string +} + +func (s *SecurityGroup) consolidateHostsAndIpRanges(ipPermissions []IpPermission) { + for i := range ipPermissions { + configuredIpPermission := &ipPermissions[i] + + for _, host := range configuredIpPermission.Hosts { + addresses, err := net.LookupHost(*host.FQDN) + if err != nil { + log.Printf("Unable to lookup host: %v", err) + } + + for _, address := range addresses { + ip, err := netaddr.ParseIP(address) + if err != nil { + log.Printf("Host %s resolved to %s: %v", *host.FQDN, address, err) + + continue + } + + if ip.Is6() { + cidrIpv6 := address + "/128" + cidrIpv6Found := false + + for _, Ipv6Range := range configuredIpPermission.Ipv6Ranges { + if cidrIpv6 == *Ipv6Range.CidrIpv6 { + cidrIpv6Found = true + + break + } + } + + if !cidrIpv6Found { + if configuredIpPermission.Ipv6Ranges == nil { + configuredIpPermission.Ipv6Ranges = make([]types.Ipv6Range, 0, 1) + } + + configuredIpPermission.Ipv6Ranges = append(configuredIpPermission.Ipv6Ranges, types.Ipv6Range{ + CidrIpv6: &cidrIpv6, + Description: host.Description, + }) + } + } else { + cidrIp := address + "/32" + cidrIpFound := false + + for _, IpRange := range configuredIpPermission.IpRanges { + if cidrIp == *IpRange.CidrIp { + cidrIpFound = true + + break + } + } + + if !cidrIpFound { + if configuredIpPermission.IpRanges == nil { + configuredIpPermission.IpRanges = make([]types.IpRange, 0, 1) + } + + configuredIpPermission.IpRanges = append(configuredIpPermission.IpRanges, types.IpRange{ + CidrIp: &cidrIp, + Description: host.Description, + }) + } + } + } + } + } +} + +type IpPermission struct { + FromPort *int32 + Hosts []Host + IpProtocol *string + IpRanges []types.IpRange + Ipv6Ranges []types.Ipv6Range + PrefixListIds []types.PrefixListId + ToPort *int32 + UserIdGroupPairs []types.UserIdGroupPair +} + +type Host struct { + FQDN *string + Description *string +} diff --git a/security-groups-manager/cmd/controller.go b/security-groups-manager/cmd/controller.go new file mode 100644 index 0000000..a0e2819 --- /dev/null +++ b/security-groups-manager/cmd/controller.go @@ -0,0 +1,189 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "sync" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" +) + +type Controller struct { + Client *ec2.Client + SecurityGroupIdRegionNameMutex sync.Mutex + SecurityGroupIdRegionName map[string]string + AsIsSecurityGroups []types.SecurityGroup + ToBeSecurityGroups []types.SecurityGroup + SecurityGroupDeltas []SecurityGroupDelta +} + +func NewController(client *ec2.Client) *Controller { + controller := new(Controller) + + controller.Client = client + controller.SecurityGroupIdRegionName = make(map[string]string) + controller.AsIsSecurityGroups = make([]types.SecurityGroup, 0) + controller.ToBeSecurityGroups = make([]types.SecurityGroup, 0) + controller.SecurityGroupDeltas = make([]SecurityGroupDelta, 0) + + return controller +} + +func (c *Controller) CalculateSecurityGroupDeltas() { + log.Printf("Calculating security group deltas") + + securityGroupDeltaChannel := make(chan SecurityGroupDelta) + + for _, toBeSecurityGroup := range c.ToBeSecurityGroups { + go func(toBeSecurityGroup types.SecurityGroup) { + securityGroupDelta := NewSecurityGroupDelta(&toBeSecurityGroup) + + for _, asIsSecurityGroup := range c.AsIsSecurityGroups { + if *toBeSecurityGroup.VpcId == *asIsSecurityGroup.VpcId && *toBeSecurityGroup.GroupId == *asIsSecurityGroup.GroupId { + securityGroupDelta.AsIsSecurityGroup = &asIsSecurityGroup + + c.SecurityGroupIdRegionNameMutex.Lock() + securityGroupDelta.RegionName = c.SecurityGroupIdRegionName[*securityGroupDelta.AsIsSecurityGroup.GroupId] + c.SecurityGroupIdRegionNameMutex.Unlock() + + securityGroupDelta.calculate() + + break + } + } + + securityGroupDeltaChannel <- *securityGroupDelta + }(toBeSecurityGroup) + } + + for range c.ToBeSecurityGroups { + securityGroupDelta := <-securityGroupDeltaChannel + + c.SecurityGroupDeltas = append(c.SecurityGroupDeltas, securityGroupDelta) + } + + log.Printf("Calculated security group deltas") +} + +func (c *Controller) InitAsIsSecurityGroups() error { + describeRegionsOutput, err := c.Client.DescribeRegions(context.TODO(), &ec2.DescribeRegionsInput{ + AllRegions: aws.Bool(true), + }) + if err != nil { + log.Printf("Unable to describe regions: %v", err) + + return err + } + + asIsSecurityGroupsChannel := make(chan []types.SecurityGroup) + + for _, region := range describeRegionsOutput.Regions { + if *region.OptInStatus != "not-opted-in" { + go func(regionName string) { + describeSecurityGroupsOutput, err := c.Client.DescribeSecurityGroups(context.TODO(), nil, func(options *ec2.Options) { + options.Region = regionName + }) + if err != nil { + log.Printf("Unable to describe security groups in region %s: %v", regionName, err) + + asIsSecurityGroupsChannel <- nil + + return + } + + for _, securityGroup := range describeSecurityGroupsOutput.SecurityGroups { + c.SecurityGroupIdRegionNameMutex.Lock() + c.SecurityGroupIdRegionName[*securityGroup.GroupId] = regionName + c.SecurityGroupIdRegionNameMutex.Unlock() + } + + asIsSecurityGroupsChannel <- describeSecurityGroupsOutput.SecurityGroups + }(*region.RegionName) + } + } + + for _, region := range describeRegionsOutput.Regions { + if *region.OptInStatus != "not-opted-in" { + asIsSecurityGroups := <-asIsSecurityGroupsChannel + + if len(asIsSecurityGroups) > 0 { + c.AsIsSecurityGroups = append(c.AsIsSecurityGroups, asIsSecurityGroups...) + } + } + } + + return nil +} + +func (c *Controller) InitToBeSecurityGroups(configuration *Configuration) { + toBeSecurityGroupChannel := make(chan *types.SecurityGroup) + + for _, configuredSecurityGroup := range configuration.SecurityGroups { + go func(configuredSecurityGroup SecurityGroup) { + configuredSecurityGroup.consolidateHostsAndIpRanges(configuredSecurityGroup.IpPermissions) + configuredSecurityGroup.consolidateHostsAndIpRanges(configuredSecurityGroup.IpPermissionsEgress) + + var toBeSecurityGroup types.SecurityGroup + + b, err := json.Marshal(configuredSecurityGroup) + if err != nil { + log.Printf("Unable to marshal configured security group %s: %v", *configuredSecurityGroup.GroupName, err) + + toBeSecurityGroupChannel <- nil + + return + } + if err := json.Unmarshal(b, &toBeSecurityGroup); err != nil { + log.Printf("Unable to unmarshal security group %s: %v", *configuredSecurityGroup.GroupName, err) + + toBeSecurityGroupChannel <- nil + + return + } + + toBeSecurityGroupChannel <- &toBeSecurityGroup + }(configuredSecurityGroup) + } + + for range configuration.SecurityGroups { + toBeSecurityGroup := <-toBeSecurityGroupChannel + if toBeSecurityGroup != nil { + c.ToBeSecurityGroups = append(c.ToBeSecurityGroups, *toBeSecurityGroup) + } + } +} + +func (c *Controller) ProcessSecurityGroupDeltas() { + log.Printf("Processing security group deltas") + + securityGroupDeltaApplyChannel := make(chan SecurityGroupDelta) + + for _, securityGroupDelta := range c.SecurityGroupDeltas { + go func(securityGroupDelta SecurityGroupDelta) { + if securityGroupDelta.AsIsSecurityGroup != nil && (len (securityGroupDelta.IpPermissionsToAuthorize) > 0 || len(securityGroupDelta.IpPermissionsToRevoke) > 0 || len(securityGroupDelta.IpPermissionsToUpdate) > 0 || + len(securityGroupDelta.IpPermissionsEgressToAuthorize) > 0 || len(securityGroupDelta.IpPermissionsEgressToRevoke) > 0 || len(securityGroupDelta.IpPermissionsEgressToUpdate) > 0 || + len(securityGroupDelta.TagsToCreate) > 0 || len(securityGroupDelta.TagsToDelete) > 0) { + securityGroupDelta.apply(c.Client) + } + + securityGroupDeltaApplyChannel <- securityGroupDelta + }(securityGroupDelta) + } + + for range c.SecurityGroupDeltas { + securityGroupDelta := <-securityGroupDeltaApplyChannel + + if securityGroupDelta.AsIsSecurityGroup == nil || len (securityGroupDelta.IpPermissionsToAuthorize) > 0 || len(securityGroupDelta.IpPermissionsToRevoke) > 0 || len(securityGroupDelta.IpPermissionsToUpdate) > 0 || + len(securityGroupDelta.IpPermissionsEgressToAuthorize) > 0 || len(securityGroupDelta.IpPermissionsEgressToRevoke) > 0 || len(securityGroupDelta.IpPermissionsEgressToUpdate) > 0 || + len(securityGroupDelta.TagsToCreate) > 0 || len(securityGroupDelta.TagsToDelete) > 0 { + log.Println("\n" + securityGroupDelta.tabulate()) + } else { + log.Printf("%s / %s is up to date", *securityGroupDelta.AsIsSecurityGroup.GroupId, *securityGroupDelta.AsIsSecurityGroup.GroupName) + } + } + + log.Printf("Processed security group deltas") +} diff --git a/security-groups-manager/cmd/delta.go b/security-groups-manager/cmd/delta.go new file mode 100644 index 0000000..7d90142 --- /dev/null +++ b/security-groups-manager/cmd/delta.go @@ -0,0 +1,533 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +type SecurityGroupDelta struct { + AsIsSecurityGroup *types.SecurityGroup + IpPermissionsToAuthorize []types.IpPermission + IpPermissionsToAuthorizeResult string + IpPermissionsToRevoke []types.IpPermission + IpPermissionsToRevokeResult string + IpPermissionsToUpdate []types.IpPermission + IpPermissionsToUpdateResult string + IpPermissionsEgressToAuthorize []types.IpPermission + IpPermissionsEgressToAuthorizeResult string + IpPermissionsEgressToRevoke []types.IpPermission + IpPermissionsEgressToRevokeResult string + IpPermissionsEgressToUpdate []types.IpPermission + IpPermissionsEgressToUpdateResult string + RegionName string + TagsToCreate []types.Tag + TagsToCreateResult string + TagsToDelete []types.Tag + TagsToDeleteResult string + ToBeSecurityGroup *types.SecurityGroup +} + +func NewSecurityGroupDelta(toBeSecurityGroup *types.SecurityGroup) *SecurityGroupDelta { + securityGroupDelta := new(SecurityGroupDelta) + + securityGroupDelta.AsIsSecurityGroup = nil + securityGroupDelta.IpPermissionsToAuthorize = make([]types.IpPermission, 0) + securityGroupDelta.IpPermissionsToAuthorizeResult = "" + securityGroupDelta.IpPermissionsToRevoke = make([]types.IpPermission, 0) + securityGroupDelta.IpPermissionsToRevokeResult = "" + securityGroupDelta.IpPermissionsToUpdate = make([]types.IpPermission, 0) + securityGroupDelta.IpPermissionsToUpdateResult = "" + securityGroupDelta.IpPermissionsEgressToAuthorize = make([]types.IpPermission, 0) + securityGroupDelta.IpPermissionsEgressToAuthorizeResult = "" + securityGroupDelta.IpPermissionsEgressToRevoke = make([]types.IpPermission, 0) + securityGroupDelta.IpPermissionsEgressToRevokeResult = "" + securityGroupDelta.IpPermissionsEgressToUpdate = make([]types.IpPermission, 0) + securityGroupDelta.IpPermissionsEgressToUpdateResult = "" + securityGroupDelta.TagsToCreate = make([]types.Tag, 0) + securityGroupDelta.TagsToCreateResult = "" + securityGroupDelta.TagsToDelete = make([]types.Tag, 0) + securityGroupDelta.TagsToDeleteResult = "" + securityGroupDelta.ToBeSecurityGroup = toBeSecurityGroup + + return securityGroupDelta +} + +func (s *SecurityGroupDelta) apply(client *ec2.Client) { + log.Printf("Applying remediations") + + if len(s.IpPermissionsToRevoke) > 0 { + if _, err := client.RevokeSecurityGroupIngress(context.TODO(), &ec2.RevokeSecurityGroupIngressInput{ + GroupId: s.AsIsSecurityGroup.GroupId, + IpPermissions: s.IpPermissionsToRevoke, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.IpPermissionsToRevokeResult = fmt.Sprintf("Failed to revoke inbound rules: %v", err) + } else { + s.IpPermissionsToRevokeResult = "Succeeded to revoke inbound rules" + } + } + + if len(s.IpPermissionsToAuthorize) > 0 { + if _, err := client.AuthorizeSecurityGroupIngress(context.TODO(), &ec2.AuthorizeSecurityGroupIngressInput{ + GroupId: s.ToBeSecurityGroup.GroupId, + IpPermissions: s.IpPermissionsToAuthorize, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.IpPermissionsToAuthorizeResult = fmt.Sprintf("Failed to authorize inbound rules: %v", err) + } else { + s.IpPermissionsToAuthorizeResult = "Succeeded to authorize inbound rules" + } + } + + if len(s.IpPermissionsToUpdate) > 0 { + if _, err := client.UpdateSecurityGroupRuleDescriptionsIngress(context.TODO(), &ec2.UpdateSecurityGroupRuleDescriptionsIngressInput{ + GroupId: s.ToBeSecurityGroup.GroupId, + IpPermissions: s.IpPermissionsToUpdate, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.IpPermissionsToUpdateResult = fmt.Sprintf("Failed to update inbound rules: %v", err) + } else { + s.IpPermissionsToUpdateResult = "Succeeded to update inbound rules" + } + } + + if len(s.IpPermissionsEgressToRevoke) > 0 { + if _, err := client.RevokeSecurityGroupEgress(context.TODO(), &ec2.RevokeSecurityGroupEgressInput{ + GroupId: s.AsIsSecurityGroup.GroupId, + IpPermissions: s.IpPermissionsEgressToRevoke, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.IpPermissionsEgressToRevokeResult = fmt.Sprintf("Failed to revoke outbound rules: %v", err) + } else { + s.IpPermissionsEgressToRevokeResult = "Succeeded to revoke outbound rules" + } + } + + if len(s.IpPermissionsEgressToAuthorize) > 0 { + if _, err := client.AuthorizeSecurityGroupEgress(context.TODO(), &ec2.AuthorizeSecurityGroupEgressInput{ + GroupId: s.ToBeSecurityGroup.GroupId, + IpPermissions: s.IpPermissionsEgressToAuthorize, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.IpPermissionsEgressToAuthorizeResult = fmt.Sprintf("Failed to authorize outbound rules: %v", err) + } else { + s.IpPermissionsEgressToAuthorizeResult = "Succeeded to authorize outbound rules" + } + } + + if len(s.IpPermissionsEgressToUpdate) > 0 { + if _, err := client.UpdateSecurityGroupRuleDescriptionsEgress(context.TODO(), &ec2.UpdateSecurityGroupRuleDescriptionsEgressInput{ + GroupId: s.ToBeSecurityGroup.GroupId, + IpPermissions: s.IpPermissionsEgressToUpdate, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.IpPermissionsEgressToUpdateResult = fmt.Sprintf("Failed to update outbound rules: %v", err) + } else { + s.IpPermissionsEgressToUpdateResult = "Succeeded to update outbound rules" + } + } + + if len(s.TagsToDelete) > 0 { + if _, err := client.DeleteTags(context.TODO(), &ec2.DeleteTagsInput{ + Resources: []string{ + *s.AsIsSecurityGroup.GroupId, + }, + Tags: s.TagsToDelete, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.TagsToDeleteResult = fmt.Sprintf("Failed to delete tags: %v", err) + } else { + s.TagsToDeleteResult = "Succeeded to delete tags" + } + } + + if len(s.TagsToCreate) > 0 { + if _, err := client.CreateTags(context.TODO(), &ec2.CreateTagsInput{ + Resources: []string{ + *s.AsIsSecurityGroup.GroupId, + }, + Tags: s.TagsToCreate, + }, func(options *ec2.Options) { + options.Region = s.RegionName + }); err != nil { + s.TagsToCreateResult = fmt.Sprintf("Failed to create tags: %v", err) + } else { + s.TagsToCreateResult = "Succeeded to create tags" + } + } + + log.Printf("Applied remediations") +} + +func (s *SecurityGroupDelta) calculate() { + asIsSecurityGroupIpPermissions := s.AsIsSecurityGroup.IpPermissions + toBeSecurityGroupIpPermissions := s.ToBeSecurityGroup.IpPermissions + + s.diffIpPermissions(asIsSecurityGroupIpPermissions, toBeSecurityGroupIpPermissions, &s.IpPermissionsToRevoke, nil) + s.diffIpPermissions(toBeSecurityGroupIpPermissions, asIsSecurityGroupIpPermissions, &s.IpPermissionsToAuthorize, &s.IpPermissionsToUpdate) + + asIsSecurityGroupIpPermissionsEgress := s.AsIsSecurityGroup.IpPermissionsEgress + toBeSecurityGroupIpPermissionsEgress := s.ToBeSecurityGroup.IpPermissionsEgress + + s.diffIpPermissions(asIsSecurityGroupIpPermissionsEgress, toBeSecurityGroupIpPermissionsEgress, &s.IpPermissionsEgressToRevoke, nil) + s.diffIpPermissions(toBeSecurityGroupIpPermissionsEgress, asIsSecurityGroupIpPermissionsEgress, &s.IpPermissionsEgressToAuthorize, &s.IpPermissionsEgressToUpdate) + + asIsSecurityGroupTags := s.AsIsSecurityGroup.Tags + toBeSecurityGroupTags := s.ToBeSecurityGroup.Tags + + s.diffTags(asIsSecurityGroupTags, toBeSecurityGroupTags, &s.TagsToDelete) + s.diffTags(toBeSecurityGroupTags, asIsSecurityGroupTags, &s.TagsToCreate) +} + +func (s *SecurityGroupDelta) diffIpPermissions(thisIpPermissions []types.IpPermission, otherIpPermissions []types.IpPermission, ipPermissions *[]types.IpPermission, ipPermissionsToUpdate *[]types.IpPermission) { + for _, thisIpPermission := range thisIpPermissions { + ipPermissionFound := false + + for _, otherIpPermission := range otherIpPermissions { + if determinePortRange(thisIpPermission) == determinePortRange(otherIpPermission) && determineProtocol(thisIpPermission) == determineProtocol(otherIpPermission) { + ipPermissionFound = true + + for _, thisIpRange := range thisIpPermission.IpRanges { + ipRangeCidrIpFound := false + + for _, otherIpRange := range otherIpPermission.IpRanges { + if *thisIpRange.CidrIp == *otherIpRange.CidrIp { + ipRangeCidrIpFound = true + + if ipPermissionsToUpdate != nil { + if (thisIpRange.Description != nil && otherIpRange.Description != nil && *thisIpRange.Description != *otherIpRange.Description) || + (thisIpRange.Description != otherIpRange.Description && (thisIpRange.Description == nil || otherIpRange.Description == nil)) { + *ipPermissionsToUpdate = append(*ipPermissionsToUpdate, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + IpRanges: []types.IpRange{ + thisIpRange, + }, + ToPort: thisIpPermission.ToPort, + }) + } + } + + break + } + } + + if !ipRangeCidrIpFound { + *ipPermissions = append(*ipPermissions, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + IpRanges: []types.IpRange{ + thisIpRange, + }, + ToPort: thisIpPermission.ToPort, + }) + } + } + + for _, thisIpv6Range := range thisIpPermission.Ipv6Ranges { + ipv6RangeCidrIpFound := false + + for _, otherIpv6Range := range otherIpPermission.Ipv6Ranges { + if *thisIpv6Range.CidrIpv6 == *otherIpv6Range.CidrIpv6 { + ipv6RangeCidrIpFound = true + + if *ipPermissionsToUpdate != nil { + if (thisIpv6Range.Description != nil && otherIpv6Range.Description != nil && *thisIpv6Range.Description != *otherIpv6Range.Description) || + (thisIpv6Range.Description != otherIpv6Range.Description && (thisIpv6Range.Description == nil || otherIpv6Range.Description == nil)) { + *ipPermissionsToUpdate = append(*ipPermissionsToUpdate, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + Ipv6Ranges: []types.Ipv6Range{ + thisIpv6Range, + }, + ToPort: thisIpPermission.ToPort, + }) + } + } + + break + } + } + + if !ipv6RangeCidrIpFound { + *ipPermissions = append(*ipPermissions, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + Ipv6Ranges: []types.Ipv6Range{ + thisIpv6Range, + }, + ToPort: thisIpPermission.ToPort, + }) + } + } + + for _, thisPrefixListId := range thisIpPermission.PrefixListIds { + prefixListIdFound := false + + for _, otherPrefixListId := range otherIpPermission.PrefixListIds { + if *thisPrefixListId.PrefixListId == *otherPrefixListId.PrefixListId { + prefixListIdFound = true + + if ipPermissionsToUpdate != nil && *thisPrefixListId.Description != *otherPrefixListId.Description { + *ipPermissionsToUpdate = append(*ipPermissionsToUpdate, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + PrefixListIds: []types.PrefixListId{ + thisPrefixListId, + }, + ToPort: thisIpPermission.ToPort, + }) + } + + break + } + } + + if !prefixListIdFound { + *ipPermissions = append(*ipPermissions, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + PrefixListIds: []types.PrefixListId{ + thisPrefixListId, + }, + ToPort: thisIpPermission.ToPort, + }) + } + } + + for _, thisUserIdGroupPair := range thisIpPermission.UserIdGroupPairs { + userIdGroupPairFound := false + + for _, otherUserIdGroupPair := range otherIpPermission.UserIdGroupPairs { + if *thisUserIdGroupPair.UserId == *otherUserIdGroupPair.UserId && *thisUserIdGroupPair.GroupId == *otherUserIdGroupPair.GroupId { + userIdGroupPairFound = true + + if *ipPermissionsToUpdate != nil && *thisUserIdGroupPair.Description != *otherUserIdGroupPair.Description { + *ipPermissionsToUpdate = append(*ipPermissionsToUpdate, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + ToPort: thisIpPermission.ToPort, + UserIdGroupPairs: []types.UserIdGroupPair{ + thisUserIdGroupPair, + }, + }) + } + + break + } + } + + if !userIdGroupPairFound { + *ipPermissions = append(*ipPermissions, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + ToPort: thisIpPermission.ToPort, + UserIdGroupPairs: []types.UserIdGroupPair{ + thisUserIdGroupPair, + }, + }) + } + } + } + } + + if !ipPermissionFound { + *ipPermissions = append(*ipPermissions, types.IpPermission{ + FromPort: thisIpPermission.FromPort, + IpProtocol: thisIpPermission.IpProtocol, + IpRanges: thisIpPermission.IpRanges, + Ipv6Ranges: thisIpPermission.Ipv6Ranges, + PrefixListIds: thisIpPermission.PrefixListIds, + ToPort: thisIpPermission.ToPort, + UserIdGroupPairs: thisIpPermission.UserIdGroupPairs, + }) + } + } +} + +func (s *SecurityGroupDelta) diffTags(thisTags []types.Tag, otherTags []types.Tag, tags *[]types.Tag) { + for _, thisTag := range thisTags { + tagFound := false + + for _, otherTag := range otherTags { + if *thisTag.Key == *otherTag.Key && *thisTag.Value == *otherTag.Value { + tagFound = true + + break + } + } + + if !tagFound { + *tags = append(*tags, types.Tag{ + Key: thisTag.Key, + Value: thisTag.Value, + }) + } + } +} + +func (s *SecurityGroupDelta) tabulate() string { + securityGroupDeltaTable := table.NewWriter() + + if s.AsIsSecurityGroup != nil { + securityGroupDeltaTable.AppendHeader(table.Row{"As is", "To be", "Remediation", "Result"}) + securityGroupDeltaTable.SetColumnConfigs([]table.ColumnConfig{ + { + Number: 1, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + VAlign: text.VAlignMiddle, + }, + { + Number: 2, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + VAlign: text.VAlignMiddle, + }, + { + Number: 3, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + VAlign: text.VAlignMiddle, + }, + { + Number: 4, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + VAlign: text.VAlignMiddle, + }, + }) + securityGroupDeltaTable.Style().Box = table.StyleBoxRounded + securityGroupDeltaTable.Style().Format = table.FormatOptions{ + Header: text.FormatDefault, + } + securityGroupDeltaTable.Style().Options.SeparateRows = true + + securityGroupDeltaTable.AppendRow(table.Row{ + tabulateSecurityGroup(*s.AsIsSecurityGroup), + tabulateSecurityGroup(*s.ToBeSecurityGroup), + "", + "", + }) + + ipPermissionsRemediation := make([]string, 0, 3) + ipPermissionsRemediationResult := make([]string, 0, 3) + + if len(s.IpPermissionsToRevoke) > 0 { + ipPermissionsRemediation = append(ipPermissionsRemediation, tabulateIpPermissions(s.IpPermissionsToRevoke, *s.AsIsSecurityGroup, "Inbound rules to revoke")) + ipPermissionsRemediationResult = append(ipPermissionsRemediationResult, s.IpPermissionsToRevokeResult) + } + if len(s.IpPermissionsToAuthorize) > 0 { + ipPermissionsRemediation = append(ipPermissionsRemediation, tabulateIpPermissions(s.IpPermissionsToAuthorize, *s.AsIsSecurityGroup, "Inbound rules to authorize")) + ipPermissionsRemediationResult = append(ipPermissionsRemediationResult, s.IpPermissionsToAuthorizeResult) + } + if len(s.IpPermissionsToUpdate) > 0 { + ipPermissionsRemediation = append(ipPermissionsRemediation, tabulateIpPermissions(s.IpPermissionsToUpdate, *s.AsIsSecurityGroup, "Inbound rules to update")) + ipPermissionsRemediationResult = append(ipPermissionsRemediationResult, s.IpPermissionsToUpdateResult) + } + + securityGroupDeltaTable.AppendRow(table.Row{ + tabulateIpPermissions(s.AsIsSecurityGroup.IpPermissions, *s.AsIsSecurityGroup, "Inbound rules"), + tabulateIpPermissions(s.ToBeSecurityGroup.IpPermissions, *s.ToBeSecurityGroup, "Inbound rules"), + strings.Join(ipPermissionsRemediation, "\n"), + strings.Join(ipPermissionsRemediationResult, "\n"), + }) + + ipPermissionsEgressRemediation := make([]string, 0, 3) + ipPermissionsEgressRemediationResult := make([]string, 0, 3) + + if len(s.IpPermissionsEgressToRevoke) > 0 { + ipPermissionsEgressRemediation = append(ipPermissionsEgressRemediation, tabulateIpPermissions(s.IpPermissionsEgressToRevoke, *s.AsIsSecurityGroup, "Outbound rules to revoke")) + ipPermissionsEgressRemediationResult = append(ipPermissionsEgressRemediationResult, s.IpPermissionsEgressToRevokeResult) + } + if len(s.IpPermissionsEgressToAuthorize) > 0 { + ipPermissionsEgressRemediation = append(ipPermissionsEgressRemediation, tabulateIpPermissions(s.IpPermissionsEgressToAuthorize, *s.AsIsSecurityGroup, "Outbound rules to authorize")) + ipPermissionsEgressRemediationResult = append(ipPermissionsEgressRemediationResult, s.IpPermissionsEgressToAuthorizeResult) + } + if len(s.IpPermissionsEgressToUpdate) > 0 { + ipPermissionsEgressRemediation = append(ipPermissionsEgressRemediation, tabulateIpPermissions(s.IpPermissionsEgressToUpdate, *s.AsIsSecurityGroup, "Outbound rules to update")) + ipPermissionsEgressRemediationResult = append(ipPermissionsEgressRemediationResult, s.IpPermissionsEgressToUpdateResult) + } + + securityGroupDeltaTable.AppendRow(table.Row{ + tabulateIpPermissions(s.AsIsSecurityGroup.IpPermissionsEgress, *s.AsIsSecurityGroup, "Outbound rules"), + tabulateIpPermissions(s.ToBeSecurityGroup.IpPermissionsEgress, *s.ToBeSecurityGroup, "Outbound rules"), + strings.Join(ipPermissionsEgressRemediation, "\n"), + strings.Join(ipPermissionsEgressRemediationResult, "\n"), + }) + + tagsRemediation := make([]string, 0, 3) + tagsRemediationResult := make([]string, 0, 3) + + if len(s.TagsToDelete) > 0 { + tagsRemediation = append(tagsRemediation, tabulateTags(s.TagsToDelete, "Tags to delete")) + tagsRemediationResult = append(tagsRemediationResult, s.TagsToDeleteResult) + } + if len(s.TagsToCreate) > 0 { + tagsRemediation = append(tagsRemediation, tabulateTags(s.TagsToCreate, "Tags to create")) + tagsRemediationResult = append(tagsRemediationResult, s.TagsToCreateResult) + } + + securityGroupDeltaTable.AppendRow(table.Row{ + tabulateTags(s.AsIsSecurityGroup.Tags, "Tags"), + tabulateTags(s.ToBeSecurityGroup.Tags, "Tags"), + strings.Join(tagsRemediation, "\n"), + strings.Join(tagsRemediationResult, "\n"), + }) + } else { + securityGroupDeltaTable.AppendHeader(table.Row{"As is", "To be"}) + securityGroupDeltaTable.SetColumnConfigs([]table.ColumnConfig{ + { + Number: 1, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + { + Number: 2, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + }) + securityGroupDeltaTable.Style().Box = table.StyleBoxRounded + securityGroupDeltaTable.Style().Format = table.FormatOptions{ + Header: text.FormatDefault, + } + securityGroupDeltaTable.Style().Options.SeparateRows = true + + securityGroupDeltaTable.AppendRow(table.Row{ + fmt.Sprintf("No matching security group found with ID: %s in VPC: %s", *s.ToBeSecurityGroup.GroupId, *s.ToBeSecurityGroup.VpcId), + tabulateSecurityGroup(*s.ToBeSecurityGroup), + }) + + securityGroupDeltaTable.AppendRow(table.Row{ + "", + tabulateIpPermissions(s.ToBeSecurityGroup.IpPermissions, *s.ToBeSecurityGroup, "Inbound rules"), + }) + + securityGroupDeltaTable.AppendRow(table.Row{ + "", + tabulateIpPermissions(s.ToBeSecurityGroup.IpPermissionsEgress, *s.ToBeSecurityGroup, "Outbound rules"), + }) + + securityGroupDeltaTable.AppendRow(table.Row{ + "", + tabulateTags(s.ToBeSecurityGroup.Tags, "Tags"), + }) + } + + return securityGroupDeltaTable.Render() +} diff --git a/security-groups-manager/cmd/environment.go b/security-groups-manager/cmd/environment.go new file mode 100644 index 0000000..89d1ac7 --- /dev/null +++ b/security-groups-manager/cmd/environment.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "log" + "os" + "strconv" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" +) + +const awsLambdaFunctionNameEnvironmentVariableName = "AWS_LAMBDA_FUNCTION_NAME" +const configurationEnvironmentVariableName = "CONFIGURATION" +const debugEnvironmentVariableName = "DEBUG" + +type ExecutionEnvironment struct { + Client *ec2.Client + Configuration *Configuration + DoDebug bool + IsLambda bool +} + +func NewExecutionEnvironment(isLambda bool) (*ExecutionEnvironment, error) { + var err error + + executionEnvironment := new(ExecutionEnvironment) + + executionEnvironment.Client, err = initClient() + if err != nil { + return nil, err + } + executionEnvironment.Configuration, err = initConfiguration() + if err != nil { + return nil, err + } + executionEnvironment.DoDebug = initDoDebug() + executionEnvironment.IsLambda = isLambda + + return executionEnvironment, nil +} + +func init() { + if _, ok := os.LookupEnv(awsLambdaFunctionNameEnvironmentVariableName); ok { + var err error + + executionEnvironment, err = NewExecutionEnvironment(true) + if err != nil { + os.Exit(1) + } + } +} + +func initClient() (*ec2.Client, error) { + awsConfiguration, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + log.Printf("Unable to load SDK config: %v", err) + + return nil, err + } + + return ec2.NewFromConfig(awsConfiguration), nil +} + +func initConfiguration() (*Configuration, error) { + configurationEnvironmentVariableValue := lookupEnvironmentVariable(configurationEnvironmentVariableName) + + return NewConfiguration(configurationEnvironmentVariableValue) +} + +func initDoDebug() bool { + debugEnvironmentVariableValue := lookupEnvironmentVariable(debugEnvironmentVariableName) + + doDebug, err := strconv.ParseBool(debugEnvironmentVariableValue) + if err != nil { + log.Printf("Unable to parse %s environment variable: %v", debugEnvironmentVariableName, err) + } + + return doDebug +} + +func lookupEnvironmentVariable(environmentVariableName string) string { + environmentVariableValue, ok := os.LookupEnv(environmentVariableName) + if !ok { + log.Printf("Unable to lookup %s environment variable", environmentVariableName) + } + + return environmentVariableValue +} diff --git a/security-groups-manager/cmd/main.go b/security-groups-manager/cmd/main.go new file mode 100644 index 0000000..1b183df --- /dev/null +++ b/security-groups-manager/cmd/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "log" + + "github.com/aws/aws-lambda-go/lambda" +) + +var executionEnvironment = new(ExecutionEnvironment) + +func debugf(format string, v ...interface{}) { + if executionEnvironment.DoDebug { + log.Printf(format, v...) + } +} + +func execute() (*Controller, error) { + if !executionEnvironment.IsLambda { + var err error + + executionEnvironment, err = NewExecutionEnvironment(false) + if err != nil { + return nil, err + } + } + + controller := NewController(executionEnvironment.Client) + err := controller.InitAsIsSecurityGroups() + if err != nil { + return nil, err + } + controller.InitToBeSecurityGroups(executionEnvironment.Configuration) + controller.CalculateSecurityGroupDeltas() + controller.ProcessSecurityGroupDeltas() + + // Only needed by the Test functions + return controller, nil +} + +func handler() error { + _, err := execute() + if err != nil { + return fmt.Errorf("execution failed") + } + + return nil +} + +func main() { + if executionEnvironment.IsLambda { + lambda.Start(handler) + } else { + handler() + } +} diff --git a/security-groups-manager/cmd/main_test.go b/security-groups-manager/cmd/main_test.go new file mode 100644 index 0000000..084e2e2 --- /dev/null +++ b/security-groups-manager/cmd/main_test.go @@ -0,0 +1,548 @@ +package main + +import ( + "context" + "log" + "math/rand" + "os" + "sort" + "strings" + "sync" + "testing" + "text/template" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +var client *ec2.Client + +var regionNameSecurityGroupMutex sync.Mutex +var regionNameSecurityGroup = map[string]types.SecurityGroup{} + +var securityGroupIdRegionNameMutex sync.Mutex +var securityGroupIdRegionName = map[string]string{} + +func init() { + awsConfiguration, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + log.Fatalf("Unable to load SDK config: %v", err) + } + + client = ec2.NewFromConfig(awsConfiguration) +} + +func extractRegion(groupName string) string { + return strings.Split(groupName, "_")[1] +} + +func generateConfigurationFromTemplate(t *testing.T, templateFile string) string { + securityGroups := make([]types.SecurityGroup, 0, len(regionNameSecurityGroup)) + + for _, securityGroup := range regionNameSecurityGroup { + securityGroups = append(securityGroups, securityGroup) + } + + b, err := os.ReadFile(templateFile) + if err != nil { + t.Errorf("Unable to read template setup.json: %v", err) + } + + templateString := &strings.Builder{} + + template := template.Must(template.New("configuration").Funcs(template.FuncMap{"extractRegion": extractRegion}).Parse(string(b))) + template.Execute(templateString, securityGroups) + + return templateString.String() +} + +func randomizeRegions() []types.Region { + describeRegionsOutput, err := client.DescribeRegions(context.TODO(), &ec2.DescribeRegionsInput{ + AllRegions: aws.Bool(true), + }) + if err != nil { + log.Fatalf("Unable to describe regions: %v", err) + } + + regions := make([]types.Region, 0, len(describeRegionsOutput.Regions)) + + for _, region := range describeRegionsOutput.Regions { + if *region.OptInStatus != "not-opted-in" { + regions = append(regions, region) + } + } + + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(regions), func(i, j int) { + regions[i], regions[j] = regions[j], regions[i] + }) + + // TODO: Convert hardcoded limit to an environment variable + return regions[:2] +} + +func runTemplateBasedTest(t *testing.T, templateFile string) bool { + runNextTest := true + + os.Setenv("CONFIGURATION", generateConfigurationFromTemplate(t, templateFile)) + os.Setenv("DEBUG", "false") + + controller, err := execute() + if err != nil { + runNextTest = false + + t.Error("Unexpected error encountered") + + return runNextTest + } + + for _, toBeSecurityGroup := range controller.ToBeSecurityGroups { + regionName := securityGroupIdRegionName[*toBeSecurityGroup.GroupId] + + describeSecurityGroupsOutput, err := client.DescribeSecurityGroups(context.TODO(), &ec2.DescribeSecurityGroupsInput{ + GroupIds: []string{*toBeSecurityGroup.GroupId}, + }, func(options *ec2.Options) { + options.Region = regionName + }) + if err != nil { + runNextTest = false + + t.Errorf("Unable to describe security groups in region %s: %v", regionName, err) + + return runNextTest + } + + if len(describeSecurityGroupsOutput.SecurityGroups) != 1 { + runNextTest = false + + t.Error("Unexpected error encountered") + + return runNextTest + } + + sortSecurityGroup(toBeSecurityGroup) + sortSecurityGroup(describeSecurityGroupsOutput.SecurityGroups[0]) + + if diff := deep.Equal(toBeSecurityGroup, describeSecurityGroupsOutput.SecurityGroups[0]); diff != nil { + runNextTest = false + + t.Errorf("Want != Got: %v", diff) + + return runNextTest + } + } + + return runNextTest +} + +func setup() bool { + ok := true + + regions := randomizeRegions() + setupOkChannel := make(chan bool) + + for _, region := range regions { + go func(regionName string) { + log.Printf("Creating security group in %s\n", regionName) + + createSecurityGroupOutput, err := client.CreateSecurityGroup(context.TODO(), &ec2.CreateSecurityGroupInput{ + Description: aws.String("Security Group created by SecurityGroupsManager test suite in " + regionName), + GroupName: aws.String("SecurityGroupsManager_" + regionName + "_SG"), + }, func(options *ec2.Options) { + options.Region = regionName + }) + if err != nil { + log.Printf("Unable to create security group in %s: %v\n", regionName, err) + + setupOkChannel <- false + + return + } + + log.Printf("Created security group in %s\n", regionName) + + regionNameSecurityGroupMutex.Lock() + regionNameSecurityGroup[regionName] = types.SecurityGroup{ + GroupId: createSecurityGroupOutput.GroupId, + } + regionNameSecurityGroupMutex.Unlock() + + describeSecurityGroupsOutput, err := client.DescribeSecurityGroups(context.TODO(), &ec2.DescribeSecurityGroupsInput{ + GroupIds: []string{*createSecurityGroupOutput.GroupId}, + }, func(options *ec2.Options) { + options.Region = regionName + }) + if err != nil { + log.Printf("Unable to describe security groups in region %s: %v", regionName, err) + + setupOkChannel <- false + + return + } else { + regionNameSecurityGroupMutex.Lock() + regionNameSecurityGroup[regionName] = describeSecurityGroupsOutput.SecurityGroups[0] + regionNameSecurityGroupMutex.Unlock() + + } + + securityGroupIdRegionNameMutex.Lock() + securityGroupIdRegionName[*createSecurityGroupOutput.GroupId] = regionName + securityGroupIdRegionNameMutex.Unlock() + + setupOkChannel <- true + }(*region.RegionName) + } + + for range regions { + if !<-setupOkChannel { + ok = false + } + } + + return ok +} + +func sortSecurityGroup(securityGroup types.SecurityGroup) { + for _, ipPermission := range securityGroup.IpPermissions { + sort.SliceStable(ipPermission.IpRanges, func(i int, j int) bool { + if *ipPermission.IpRanges[i].CidrIp < *ipPermission.IpRanges[j].CidrIp { + return true + } + if *ipPermission.IpRanges[i].CidrIp > *ipPermission.IpRanges[j].CidrIp { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermission.IpRanges[i].Description != nil { + iDescription = *ipPermission.IpRanges[i].Description + } + if ipPermission.IpRanges[j].Description != nil { + jDescription = *ipPermission.IpRanges[j].Description + } + return iDescription < jDescription + }) + + sort.SliceStable(ipPermission.Ipv6Ranges, func(i int, j int) bool { + if *ipPermission.Ipv6Ranges[i].CidrIpv6 < *ipPermission.Ipv6Ranges[j].CidrIpv6 { + return true + } + if *ipPermission.Ipv6Ranges[i].CidrIpv6 > *ipPermission.Ipv6Ranges[j].CidrIpv6 { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermission.Ipv6Ranges[i].Description != nil { + iDescription = *ipPermission.Ipv6Ranges[i].Description + } + if ipPermission.Ipv6Ranges[j].Description != nil { + jDescription = *ipPermission.Ipv6Ranges[j].Description + } + return iDescription < jDescription + }) + + sort.SliceStable(ipPermission.PrefixListIds, func(i int, j int) bool { + if *ipPermission.PrefixListIds[i].PrefixListId < *ipPermission.PrefixListIds[j].PrefixListId { + return true + } + if *ipPermission.PrefixListIds[i].PrefixListId > *ipPermission.PrefixListIds[j].PrefixListId { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermission.PrefixListIds[i].Description != nil { + iDescription = *ipPermission.PrefixListIds[i].Description + } + if ipPermission.PrefixListIds[j].Description != nil { + jDescription = *ipPermission.PrefixListIds[j].Description + } + return iDescription < jDescription + }) + + sort.SliceStable(ipPermission.UserIdGroupPairs, func(i int, j int) bool { + if *ipPermission.UserIdGroupPairs[i].GroupId < *ipPermission.UserIdGroupPairs[j].GroupId { + return true + } + if *ipPermission.UserIdGroupPairs[i].GroupId > *ipPermission.UserIdGroupPairs[j].GroupId { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermission.UserIdGroupPairs[i].Description != nil { + iDescription = *ipPermission.UserIdGroupPairs[i].Description + } + if ipPermission.UserIdGroupPairs[j].Description != nil { + jDescription = *ipPermission.UserIdGroupPairs[j].Description + } + return iDescription < jDescription + }) + } + + for _, ipPermissionEgress := range securityGroup.IpPermissionsEgress { + sort.SliceStable(ipPermissionEgress.IpRanges, func(i int, j int) bool { + if *ipPermissionEgress.IpRanges[i].CidrIp < *ipPermissionEgress.IpRanges[j].CidrIp { + return true + } + if *ipPermissionEgress.IpRanges[i].CidrIp > *ipPermissionEgress.IpRanges[j].CidrIp { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermissionEgress.IpRanges[i].Description != nil { + iDescription = *ipPermissionEgress.IpRanges[i].Description + } + if ipPermissionEgress.IpRanges[j].Description != nil { + jDescription = *ipPermissionEgress.IpRanges[j].Description + } + return iDescription < jDescription + }) + + sort.SliceStable(ipPermissionEgress.Ipv6Ranges, func(i int, j int) bool { + if *ipPermissionEgress.Ipv6Ranges[i].CidrIpv6 < *ipPermissionEgress.Ipv6Ranges[j].CidrIpv6 { + return true + } + if *ipPermissionEgress.Ipv6Ranges[i].CidrIpv6 > *ipPermissionEgress.Ipv6Ranges[j].CidrIpv6 { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermissionEgress.Ipv6Ranges[i].Description != nil { + iDescription = *ipPermissionEgress.Ipv6Ranges[i].Description + } + if ipPermissionEgress.Ipv6Ranges[j].Description != nil { + jDescription = *ipPermissionEgress.Ipv6Ranges[j].Description + } + return iDescription < jDescription + }) + + sort.SliceStable(ipPermissionEgress.PrefixListIds, func(i int, j int) bool { + if *ipPermissionEgress.PrefixListIds[i].PrefixListId < *ipPermissionEgress.PrefixListIds[j].PrefixListId { + return true + } + if *ipPermissionEgress.PrefixListIds[i].PrefixListId > *ipPermissionEgress.PrefixListIds[j].PrefixListId { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermissionEgress.PrefixListIds[i].Description != nil { + iDescription = *ipPermissionEgress.PrefixListIds[i].Description + } + if ipPermissionEgress.PrefixListIds[j].Description != nil { + jDescription = *ipPermissionEgress.PrefixListIds[j].Description + } + return iDescription < jDescription + }) + + sort.SliceStable(ipPermissionEgress.UserIdGroupPairs, func(i int, j int) bool { + if *ipPermissionEgress.UserIdGroupPairs[i].GroupId < *ipPermissionEgress.UserIdGroupPairs[j].GroupId { + return true + } + if *ipPermissionEgress.UserIdGroupPairs[i].GroupId > *ipPermissionEgress.UserIdGroupPairs[j].GroupId { + return false + } + + iDescription := "" + jDescription := "" + + if ipPermissionEgress.UserIdGroupPairs[i].Description != nil { + iDescription = *ipPermissionEgress.UserIdGroupPairs[i].Description + } + if ipPermissionEgress.UserIdGroupPairs[j].Description != nil { + jDescription = *ipPermissionEgress.UserIdGroupPairs[j].Description + } + return iDescription < jDescription + }) + } + + sort.SliceStable(securityGroup.IpPermissions, func(i int, j int) bool { + if *securityGroup.IpPermissions[i].IpProtocol < *securityGroup.IpPermissions[j].IpProtocol { + return true + } + if *securityGroup.IpPermissions[i].IpProtocol > *securityGroup.IpPermissions[j].IpProtocol { + return false + } + + iFromPort := int32(-65536) + jFromPort := int32(-65536) + + if securityGroup.IpPermissions[i].FromPort != nil { + iFromPort = *securityGroup.IpPermissions[i].FromPort + } + if securityGroup.IpPermissions[j].FromPort != nil { + jFromPort = *securityGroup.IpPermissions[j].FromPort + } + + if iFromPort < jFromPort { + return true + } + if iFromPort > jFromPort { + return false + } + + iToPort := int32(-65536) + jToPort := int32(-65536) + + if securityGroup.IpPermissions[i].ToPort != nil { + iToPort = *securityGroup.IpPermissions[i].ToPort + } + if securityGroup.IpPermissions[j].ToPort != nil { + jToPort = *securityGroup.IpPermissions[j].ToPort + } + + return iToPort < jToPort + }) + + sort.SliceStable(securityGroup.IpPermissionsEgress, func(i int, j int) bool { + if *securityGroup.IpPermissionsEgress[i].IpProtocol < *securityGroup.IpPermissions[j].IpProtocol { + return true + } + if *securityGroup.IpPermissionsEgress[i].IpProtocol > *securityGroup.IpPermissions[j].IpProtocol { + return false + } + + iFromPort := int32(-65536) + jFromPort := int32(-65536) + + if securityGroup.IpPermissionsEgress[i].FromPort != nil { + iFromPort = *securityGroup.IpPermissionsEgress[i].FromPort + } + if securityGroup.IpPermissions[j].FromPort != nil { + jFromPort = *securityGroup.IpPermissions[j].FromPort + } + + if iFromPort < jFromPort { + return true + } + if iFromPort > jFromPort { + return false + } + + iToPort := int32(-65536) + jToPort := int32(-65536) + + if securityGroup.IpPermissionsEgress[i].ToPort != nil { + iToPort = *securityGroup.IpPermissionsEgress[i].ToPort + } + if securityGroup.IpPermissions[j].ToPort != nil { + jToPort = *securityGroup.IpPermissions[j].ToPort + } + + return iToPort < jToPort + }) + + sort.SliceStable(securityGroup.Tags, func(i int, j int) bool { + return *securityGroup.Tags[i].Key < *securityGroup.Tags[j].Key + }) +} + +func teardown() bool { + ok := true + + p := recover() + + teardownOkChannel := make(chan bool) + + for regionName, securityGroup := range regionNameSecurityGroup { + go func(regionName string, securityGroup types.SecurityGroup) { + log.Printf("Deleting security group in %s\n", regionName) + + _, err := client.DeleteSecurityGroup(context.TODO(), &ec2.DeleteSecurityGroupInput{ + GroupId: securityGroup.GroupId, + }, func(options *ec2.Options) { + options.Region = regionName + }) + if err != nil { + log.Printf("Unable to delete security group in %s: %v\n", regionName, err) + + teardownOkChannel <- false + + return + } + + log.Printf("Deleted security group in %s\n", regionName) + + teardownOkChannel <- true + }(regionName, securityGroup) + } + + for range regionNameSecurityGroup { + if !<-teardownOkChannel { + ok = false + } + } + + if p != nil { + panic(p) + } + + return ok +} + +func TestSecurityGroupsManager(t *testing.T) { + runNextTest := true + + t.Run("Step #1 (Authorize rules + Create tags)", func(t *testing.T) { + runNextTest = runTemplateBasedTest(t, "../testdata/step_1.json") + }) + t.Run("Step #2 (Update rules + Update tags)", func(t *testing.T) { + if runNextTest { + runNextTest = runTemplateBasedTest(t, "../testdata/step_2.json") + } + }) + t.Run("Step #3.1 (Revoke rules + Delete tags)", func(t *testing.T) { + if runNextTest { + runNextTest = runTemplateBasedTest(t, "../testdata/step_3.json") + } + }) + t.Run("Step #3.2 (No change)", func(t *testing.T) { + if runNextTest { + runNextTest = runTemplateBasedTest(t, "../testdata/step_3.json") + } + }) + t.Run("Step #4 (Nonexistent)", func(t *testing.T) { + if runNextTest { + b, err := os.ReadFile("../testdata/step_4.json") + if err != nil { + t.Errorf("Unable to read template setup.json: %v", err) + } + + os.Setenv("CONFIGURATION", string(b)) + os.Setenv("DEBUG", "false") + + controller, err := execute() + if err != nil { + runNextTest = false + + t.Error("Unexpected error encountered") + } + + assert.Nil(t, controller.SecurityGroupDeltas[0].AsIsSecurityGroup) + } + }) +} + +func TestMain(m *testing.M) { + defer teardown() + + if ok := setup(); ok { + m.Run() + } +} diff --git a/security-groups-manager/cmd/tabulate.go b/security-groups-manager/cmd/tabulate.go new file mode 100644 index 0000000..9a4f826 --- /dev/null +++ b/security-groups-manager/cmd/tabulate.go @@ -0,0 +1,415 @@ +package main + +import ( + "strconv" + + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +var icmpTypeNumbers = map[int]string{ + 0: "Echo Reply", + 1: "Unassigned", + 2: "Unassigned", + 3: "Destination Unreachable", + 4: "Source Quench (Deprecated)", + 5: "Redirect", + 6: "Alternate Host Address (Deprecated)", + 7: "Unassigned", + 8: "Echo", + 9: "Router Advertisement", + 10: "Router Solicitation", + 11: "Time Exceeded", + 12: "Parameter Problem", + 13: "Timestamp", + 14: "Timestamp Reply", + 15: "Information Request (Deprecated)", + 16: "Information Reply (Deprecated)", + 17: "Address Mask Request (Deprecated)", + 18: "Address Mask Reply (Deprecated)", + 19: "Reserved (for Security)", + 30: "Traceroute (Deprecated)", + 31: "Datagram Conversion Error (Deprecated)", + 32: "Mobile Host Redirect (Deprecated)", + 33: "IPv6 Where-Are-You (Deprecated)", + 34: "IPv6 I-Am-Here (Deprecated)", + 35: "Mobile Registration Request (Deprecated)", + 36: "Mobile Registration Reply (Deprecated)", + 37: "Domain Name Request (Deprecated)", + 38: "Domain Name Reply (Deprecated)", + 39: "SKIP (Deprecated)", + 40: "Photuris", + 41: "ICMP messages utilized by experimental mobility protocols such as Seamoby", + 42: "Extended Echo Request", + 43: "Extended Echo Reply", + 253: "RFC3692-style Experiment 1", + 254: "RFC3692-style Experiment 2", + 255: "Reserved", +} + +var protocols = map[string]string{ + "-1": "All", + "0": "HOPOPT", + "1": "ICMP", + "2": "IGMP", + "3": "GGP", + "4": "IPv4", + "5": "ST", + "6": "TCP", + "7": "CBT", + "8": "EGP", + "9": "IGP", + "10": "BBN-RCC-MON", + "11": "NVP-II", + "12": "PUP", + "13": "ARGUS (deprecated)", + "14": "EMCON", + "15": "XNET", + "16": "CHAOS", + "17": "UDP", + "18": "MUX", + "19": "DCN-MEAS", + "20": "HMP", + "21": "PRM", + "22": "XNS-IDP", + "23": "TRUNK-1", + "24": "TRUNK-2", + "25": "LEAF-1", + "26": "LEAF-2", + "27": "RDP", + "28": "IRTP", + "29": "ISO-TP4", + "30": "NETBLT", + "31": "MFE-NSP", + "32": "MERIT-INP", + "33": "DCCP", + "34": "3PC", + "35": "IDPR", + "36": "XTP", + "37": "DDP", + "38": "IDPR-CMTP", + "39": "TP++", + "40": "IL", + "41": "IPv6", + "42": "SDRP", + "43": "IPv6-Route", + "44": "IPv6-Frag", + "45": "IDRP", + "46": "RSVP", + "47": "GRE", + "48": "DSR", + "49": "BNA", + "50": "ESP", + "51": "AH", + "52": "I-NLSP", + "53": "SWIPE (deprecated)", + "54": "NARP", + "55": "MOBILE", + "56": "TLSP", + "57": "SKIP", + "58": "IPv6-ICMP", + "59": "IPv6-NoNxt", + "60": "IPv6-Opts", + "62": "CFTP", + "64": "SAT-EXPAK", + "65": "KRYPTOLAN", + "66": "RVD", + "67": "IPPC", + "69": "SAT-MON", + "70": "VISA", + "71": "IPCV", + "72": "CPNX", + "73": "CPHB", + "74": "WSN", + "75": "PVP", + "76": "BR-SAT-MON", + "77": "SUN-ND", + "78": "WB-MON", + "79": "WB-EXPAK", + "80": "ISO-IP", + "81": "VMTP", + "82": "SECURE-VMTP", + "83": "VINES", + "84": "TTP / IPTM", + "85": "NSFNET-IGP", + "86": "DGP", + "87": "TCF", + "88": "EIGRP", + "89": "OSPFIGP", + "90": "Sprite-RPC", + "91": "LARP", + "92": "MTP", + "93": "AX.25", + "94": "IPIP", + "95": "MICP (deprecated)", + "96": "SCC-SP", + "97": "ETHERIP", + "98": "ENCAP", + "100": "GMTP", + "101": "IFMP", + "102": "PNNI", + "103": "PIM", + "104": "ARIS", + "105": "SCPS", + "106": "QNX", + "107": "A/N", + "108": "IPComp", + "109": "SNP", + "110": "Compaq-Peer", + "111": "IPX-in-IP", + "112": "VRRP", + "113": "PGM", + "115": "L2TP", + "116": "DDX", + "117": "IATP", + "118": "STP", + "119": "SRP", + "120": "UTI", + "121": "SMP", + "122": "SM (deprecated)", + "123": "PTP", + "124": "ISIS over IPv4", + "125": "FIRE", + "126": "CRTP", + "127": "CRUDP", + "128": "SSCOPMCE", + "129": "IPLT", + "130": "SPS", + "131": "PIPE", + "132": "SCTP", + "133": "FC", + "134": "RSVP-E2E-IGNORE", + "135": "Mobility Header", + "136": "UDPLite", + "137": "MPLS-in-IP", + "138": "manet", + "139": "HIP", + "140": "Shim6", + "141": "WESP", + "142": "ROHC", + "143": "Ethernet", + "255": "Reserved", + "icmp": "ICMP", + "icmpv6": "IPv6-ICMP", + "tcp": "TCP", + "udp": "UDP", +} + +func determinePortRange(ipPermission types.IpPermission) string { + portRange := "All" + + if ipPermission.FromPort != nil && *ipPermission.FromPort != -1 { + if *ipPermission.IpProtocol == "icmp" || *ipPermission.IpProtocol == "icmpv6" { + var ok bool + + portRange, ok = icmpTypeNumbers[int(*ipPermission.FromPort)] + if !ok { + portRange = "Unknown" + } + } else { + portRange = strconv.Itoa(int(*ipPermission.FromPort)) + if ipPermission.ToPort != nil && *ipPermission.FromPort != *ipPermission.ToPort { + portRange += " - " + strconv.Itoa(int(*ipPermission.ToPort)) + } + } + } + + return portRange +} + +func determineProtocol(ipPermission types.IpPermission) string { + protocol, ok := protocols[*ipPermission.IpProtocol] + if !ok { + protocol = "Unknown" + } + + return protocol +} + +func tabulateIpPermissions(ipPermissions []types.IpPermission, securityGroup types.SecurityGroup, header string) string { + ipPermissionsTable := table.NewWriter() + + ipPermissionsTable.SetTitle(header) + ipPermissionsTable.AppendHeader(table.Row{"Protocol", "Port Range", "Source", "Description"}) + ipPermissionsTable.SetColumnConfigs([]table.ColumnConfig{ + { + Number: 1, + AlignHeader: text.AlignCenter, + Align: text.AlignDefault, + }, + { + Number: 2, + AlignHeader: text.AlignCenter, + Align: text.AlignRight, + AutoMerge: true, + }, + { + Number: 3, + AlignHeader: text.AlignCenter, + Align: text.AlignDefault, + }, + { + Number: 4, + AlignHeader: text.AlignCenter, + Align: text.AlignDefault, + }, + }) + ipPermissionsTable.Style().Box = table.StyleBoxRounded + ipPermissionsTable.Style().Format = table.FormatOptions{ + Header: text.FormatDefault, + } + ipPermissionsTable.Style().Title.Align = text.AlignCenter + + for _, ipPermission := range ipPermissions { + protocol := determineProtocol(ipPermission) + portRange := determinePortRange(ipPermission) + + for _, ipRange := range ipPermission.IpRanges { + source := *ipRange.CidrIp + description := "" + if ipRange.Description != nil { + description = *ipRange.Description + } + + ipPermissionsTable.AppendRow(table.Row{ + protocol, + portRange, + source, + description, + }) + } + + for _, ipv6Range := range ipPermission.Ipv6Ranges { + source := *ipv6Range.CidrIpv6 + description := "" + if ipv6Range.Description != nil { + description = *ipv6Range.Description + } + + ipPermissionsTable.AppendRow(table.Row{ + protocol, + portRange, + source, + description, + }) + } + + for _, prefixListId := range ipPermission.PrefixListIds { + source := *prefixListId.PrefixListId + description := "" + if prefixListId.Description != nil { + description = *prefixListId.Description + } + + ipPermissionsTable.AppendRow(table.Row{ + protocol, + portRange, + source, + description, + }) + } + + for _, userIdGroupPair := range ipPermission.UserIdGroupPairs { + source := *userIdGroupPair.GroupId + if userIdGroupPair.UserId != nil && *securityGroup.OwnerId != *userIdGroupPair.UserId { + source = *userIdGroupPair.UserId + "/" + source + } + description := "" + if userIdGroupPair.Description != nil { + description = *userIdGroupPair.Description + } + + ipPermissionsTable.AppendRow(table.Row{ + protocol, + portRange, + source, + description, + }) + } + } + + return ipPermissionsTable.Render() +} + +func tabulateSecurityGroup(securityGroup types.SecurityGroup) string { + securityGroupTable := table.NewWriter() + + securityGroupTable.SetTitle(*securityGroup.GroupName) + securityGroupTable.AppendHeader(table.Row{"VPC ID", "Group ID", "Group Name", "Description", "Owner"}) + securityGroupTable.SetColumnConfigs([]table.ColumnConfig{ + { + Number: 1, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + { + Number: 2, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + { + Number: 3, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + { + Number: 4, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + { + Number: 5, + AlignHeader: text.AlignCenter, + Align: text.AlignCenter, + }, + }) + securityGroupTable.Style().Box = table.StyleBoxRounded + securityGroupTable.Style().Format = table.FormatOptions{ + Header: text.FormatDefault, + } + securityGroupTable.Style().Title.Align = text.AlignCenter + + securityGroupTable.AppendRow(table.Row{ + *securityGroup.VpcId, + *securityGroup.GroupId, + *securityGroup.GroupName, + *securityGroup.Description, + *securityGroup.OwnerId, + }) + + return securityGroupTable.Render() +} + +func tabulateTags(tags []types.Tag, header string) string { + tagsTable := table.NewWriter() + + tagsTable.SetTitle(header) + tagsTable.AppendHeader(table.Row{"Key", "Value"}) + tagsTable.SetColumnConfigs([]table.ColumnConfig{ + { + Number: 1, + AlignHeader: text.AlignCenter, + Align: text.AlignLeft, + }, + { + Number: 2, + AlignHeader: text.AlignCenter, + Align: text.AlignLeft, + }, + }) + tagsTable.Style().Box = table.StyleBoxRounded + tagsTable.Style().Format = table.FormatOptions{ + Header: text.FormatDefault, + } + tagsTable.Style().Title.Align = text.AlignCenter + + for _, tag := range tags { + tagsTable.AppendRow(table.Row{ + *tag.Key, + *tag.Value, + }) + } + + return tagsTable.Render() +} \ No newline at end of file diff --git a/security-groups-manager/testdata/step_1.json b/security-groups-manager/testdata/step_1.json new file mode 100644 index 0000000..9d169b2 --- /dev/null +++ b/security-groups-manager/testdata/step_1.json @@ -0,0 +1,197 @@ +{ + "SecurityGroups": [{{ range $index, $securityGroup := .}}{{if $index}},{{end}} + { + "Description": "{{.Description}}", + "GroupName": "{{.GroupName}}", + "IpPermissions": [ + { + "FromPort": 80, + "Hosts": [ + ], + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "HTTP open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 80, + "UserIdGroupPairs": [] + }, + { + "FromPort": 22, + "Hosts": [ + ], + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "SSH open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 22, + "UserIdGroupPairs": [] + }, + { + "FromPort": -1, + "Hosts": [ + ], + "IpProtocol": "icmpv6", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "ICMPv6 open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": -1, + "UserIdGroupPairs": [] + }, + { + "IpProtocol": "55", + "Hosts": [ + ], + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "MOBILE open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "UserIdGroupPairs": [] + }, + { + "FromPort": 53, + "Hosts": [ + ], + "IpProtocol": "udp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "DNS open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 53, + "UserIdGroupPairs": [] + }, + { + "FromPort": 443, + "Hosts": [ + ], + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "HTTPS open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 443, + "UserIdGroupPairs": [] + }, + { + "FromPort": -1, + "Hosts": [ + ], + "IpProtocol": "icmp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "ICMP open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": -1, + "UserIdGroupPairs": [] + } + ], + "OwnerId": "{{.OwnerId}}", + "GroupId": "{{.GroupId}}", + "IpPermissionsEgress": [ + { + "FromPort": 80, + "Hosts": [ + ], + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "HTTP open to the world" + } + ], + "Ipv6Ranges": [ + { + "CidrIpv6": "::/0", + "Description": "HTTP open to the world" + } + ], + "PrefixListIds": [], + "ToPort": 80, + "UserIdGroupPairs": [] + }, + { + "FromPort": 53, + "Hosts": [ + { + "FQDN": "dns.google", + "Description": "Google Public DNS" + } + ], + "IpProtocol": "udp", + "IpRanges": [ + ], + "Ipv6Ranges": [ + ], + "PrefixListIds": [], + "ToPort": 53, + "UserIdGroupPairs": [] + }, + { + "FromPort": 443, + "Hosts": [ + ], + "IpProtocol": "tcp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "HTTPS open to the world" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 443, + "UserIdGroupPairs": [] + } + ], + "Tags": [ + { + "Key": "Inbound rules count", + "Value": "7" + }, + { + "Key": "Outbound rules count", + "Value": "3" + }, + { + "Key": "Name", + "Value": "{{.GroupName}}" + }, + { + "Key": "Region", + "Value": "{{.GroupName | extractRegion}}" + } + ], + "VpcId": "{{.VpcId}}" + }{{end}} + ] +} diff --git a/security-groups-manager/testdata/step_2.json b/security-groups-manager/testdata/step_2.json new file mode 100644 index 0000000..a9b8c67 --- /dev/null +++ b/security-groups-manager/testdata/step_2.json @@ -0,0 +1,191 @@ +{ + "SecurityGroups": [{{ range $index, $securityGroup := .}}{{if $index}},{{end}} + { + "Description": "{{.Description}}", + "GroupName": "{{.GroupName}}", + "IpPermissions": [ + { + "FromPort": 80, + "Hosts": [ + { + "FQDN": "googlebot.com", + "Description": "Googlebot" + }, + { + "FQDN": "search.msn.com", + "Description": "Bingbot" + } + ], + "IpProtocol": "tcp", + "IpRanges": [ + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 80, + "UserIdGroupPairs": [ + { + "GroupId": "{{.GroupId}}", + "UserId": "{{.OwnerId}}", + "Description": "Allow inbound HTTP from same security group" + } + ] + }, + { + "FromPort": 22, + "Hosts": [ + { + "FQDN": "example.com", + "Description": "Example" + } + ], + "IpProtocol": "tcp", + "IpRanges": [ + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 22, + "UserIdGroupPairs": [] + }, + { + "FromPort": -1, + "IpProtocol": "icmpv6", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow inbound ICMPv6 from anywhere" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": -1, + "UserIdGroupPairs": [] + }, + { + "IpProtocol": "55", + "Hosts": [ + ], + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow inbound MOBILE from anywhere" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "UserIdGroupPairs": [] + }, + { + "FromPort": 20, + "Hosts": [ + ], + "IpProtocol": "tcp", + "IpRanges": [ + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 21, + "UserIdGroupPairs": [ + { + "GroupId": "{{.GroupId}}", + "UserId": "{{.OwnerId}}", + "Description": "Allow inbound FTP from same security group" + } + ] + }, + { + "FromPort": 53, + "Hosts": [ + ], + "IpProtocol": "udp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow inbound DNS from anywhere" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 53, + "UserIdGroupPairs": [] + }, + { + "FromPort": 443, + "Hosts": [ + { + "FQDN": "googlebot.com", + "Description": "Googlebot" + }, + { + "FQDN": "search.msn.com", + "Description": "Bingbot" + } + ], + "IpProtocol": "tcp", + "IpRanges": [ + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": 443, + "UserIdGroupPairs": [ + { + "GroupId": "{{.GroupId}}", + "UserId": "{{.OwnerId}}", + "Description": "Allow inbound HTTP from same security group" + } + ] + }, + { + "FromPort": -1, + "Hosts": [ + ], + "IpProtocol": "icmp", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow inbound ICMP from anywhere" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "ToPort": -1, + "UserIdGroupPairs": [] + } + ], + "OwnerId": "{{.OwnerId}}", + "GroupId": "{{.GroupId}}", + "IpPermissionsEgress": [ + { + "IpProtocol": "-1", + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow outbound traffic to anywhere" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [], + "UserIdGroupPairs": [] + } + ], + "Tags": [ + { + "Key": "Inbound rules count", + "Value": "8" + }, + { + "Key": "Outbound rules count", + "Value": "1" + }, + { + "Key": "Name", + "Value": "{{.GroupName}}" + }, + { + "Key": "Region", + "Value": "{{.GroupName | extractRegion}}" + } + ], + "VpcId": "{{.VpcId}}" + }{{end}} + ] +} diff --git a/security-groups-manager/testdata/step_3.json b/security-groups-manager/testdata/step_3.json new file mode 100644 index 0000000..8fa2931 --- /dev/null +++ b/security-groups-manager/testdata/step_3.json @@ -0,0 +1,15 @@ +{ + "SecurityGroups": [{{ range $index, $securityGroup := .}}{{if $index}},{{end}} + { + "Description": "{{.Description}}", + "GroupName": "{{.GroupName}}", + "IpPermissions": [ + ], + "OwnerId": "{{.OwnerId}}", + "GroupId": "{{.GroupId}}", + "IpPermissionsEgress": [ + ], + "VpcId": "{{.VpcId}}" + }{{end}} + ] +} diff --git a/security-groups-manager/testdata/step_4.json b/security-groups-manager/testdata/step_4.json new file mode 100644 index 0000000..d037641 --- /dev/null +++ b/security-groups-manager/testdata/step_4.json @@ -0,0 +1,13 @@ +{ + "SecurityGroups": [ + { + "Description": "Nonexistent security group", + "GroupName": "SecurityGroupsManager_DoesNotExits_SG", + "IpPermissions": [], + "OwnerId": "243828964935", + "GroupId": "sg-KH79J3vpsuWb7rbr", + "IpPermissionsEgress": [], + "VpcId": "vpc-iaEHJWRoKEvXtDp8" + } + ] +} diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..c56653b --- /dev/null +++ b/template.yaml @@ -0,0 +1,107 @@ +AWSTemplateFormatVersion: 2010-09-09 +Transform: AWS::Serverless-2016-10-31 + +Description: SecurityGroupsManager Serverless Application + +Conditions: + RateExpressionMinutesSingular: !Equals [!Ref RateExpressionMinutes, 1] + +Parameters: + Configuration: + Description: Enter the configuration (In JSON format) + Type: String + + EnableDebugMode: + AllowedValues: + - "true" + - "false" + Default: "false" + Description: To enable DEBUG mode set this parameter to true + Type: String + + RateExpressionMinutes: + Default: 1 + Description: >- + Enter minutes for rate expression (SecurityGroupsManagerLambdaFunction will + be triggered every X minutes) + MinValue: 1 + Type: Number + +Resources: + SecurityGroupsManagerLambdaFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: security-groups-manager/cmd/ + Environment: + Variables: + CONFIGURATION: !Ref Configuration + DEBUG: !Ref EnableDebugMode + Events: + ScheduledEvent: + Type: Schedule + Properties: + Name: SecurityGroupsManagerLambdaFunctionScheduledEvent + Schedule: + !If [ + RateExpressionMinutesSingular, + rate(1 minute), + !Join ["", [rate(, !Ref RateExpressionMinutes, " minutes)"]], + ] + FunctionName: SecurityGroupsManager + Handler: main + Role: !GetAtt SecurityGroupsManagerLambdaFunctionRole.Arn + Runtime: go1.x + Timeout: 30 + + SecurityGroupsManagerLambdaFunctionCloudWatchLogsPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: SecurityGroupsManagerLambdaFunctionCloudWatchLogsPolicy + PolicyDocument: + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: "*" + Version: 2012-10-17 + + SecurityGroupsManagerLambdaFunctionEC2Policy: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: SecurityGroupsManagerLambdaFunctionEC2Policy + PolicyDocument: + Statement: + - Effect: Allow + Action: + - ec2:AuthorizeSecurityGroupEgress + - ec2:AuthorizeSecurityGroupIngress + - ec2:CreateTags + - ec2:DeleteTags + - ec2:DescribeRegions + - ec2:DescribeSecurityGroups + - ec2:RevokeSecurityGroupEgress + - ec2:RevokeSecurityGroupIngress + - ec2:UpdateSecurityGroupRuleDescriptionsEgress + - ec2:UpdateSecurityGroupRuleDescriptionsIngress + Resource: "*" + Version: 2012-10-17 + + SecurityGroupsManagerLambdaFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Version: 2012-10-17 + ManagedPolicyArns: + - !Ref SecurityGroupsManagerLambdaFunctionCloudWatchLogsPolicy + - !Ref SecurityGroupsManagerLambdaFunctionEC2Policy + Path: / + RoleName: SecurityGroupsManagerLambdaFunctionRole