├── .github └── workflows │ ├── infra_pmapper_checks.yml │ └── infra_scout_checks.yml ├── .gitignore ├── LICENSE ├── README.md ├── infracode ├── main.tf ├── modules │ ├── tktk_service_api │ │ ├── main.tf │ │ └── variables.tf │ ├── tktk_service_finances │ │ ├── main.tf │ │ └── variables.tf │ ├── tktk_service_other_personnel │ │ ├── main.tf │ │ └── variables.tf │ ├── tktk_service_support │ │ ├── main.tf │ │ └── variables.tf │ └── tktk_service_website │ │ ├── main.tf │ │ └── variables.tf └── variables.tf ├── mitmproxy └── proxy_aws_to_localstack.py ├── requirements.txt └── testcode ├── __init__.py ├── test_permissions.py └── test_scoutsuite_rails.py /.github/workflows/infra_pmapper_checks.yml: -------------------------------------------------------------------------------- 1 | # GitHub Workflow to execute test cases via Principal Mapper 2 | 3 | name: "PMapper Checks" 4 | 5 | # Run when PR against main comes through with infracode/testcode changes 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - "infracode/**" 12 | - "testcode/**" 13 | types: 14 | - opened 15 | - reopened 16 | - synchronize 17 | workflow_dispatch: 18 | 19 | 20 | jobs: 21 | execute: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout Code 25 | uses: actions/checkout@v2 26 | - name: Install Graphviz 27 | run: "sudo apt-get install -y graphviz jq" 28 | - name: Setup Terraform 29 | uses: hashicorp/setup-terraform@v1 30 | - name: Init Terraform 31 | run: | 32 | cd $GITHUB_WORKSPACE/infracode 33 | terraform init 34 | - name: Validate Config 35 | run: | 36 | cd $GITHUB_WORKSPACE/infracode 37 | terraform validate -no-color 38 | - name: Setup LocalStack 39 | run: | 40 | echo "Install LocalStack, AWS CLI, PMapper via pip" 41 | pip install localstack principalmapper 42 | echo "Pull localstack/localstack from Docker" 43 | docker pull localstack/localstack 44 | echo "Start LocalStack" 45 | localstack start -d 46 | echo "Wait for LocalStack" 47 | localstack wait -t 30 48 | echo "LocalStack running" 49 | - name: Deploy Infra Code 50 | run: | 51 | cd $GITHUB_WORKSPACE/infracode 52 | terraform apply -auto-approve -var "acctid=000000000000" 53 | - name: Make Artifacts Directory 54 | run: | 55 | mkdir -p /tmp/artifacts 56 | - name: Create PMapper Graph 57 | env: 58 | PMAPPER_STORAGE: "/tmp/artifacts" 59 | AWS_ACCESS_KEY_ID: AKIAFAKEFAKEFAKE 60 | AWS_SECRET_ACCESS_KEY: alsofakejustmakesthingswork 61 | AWS_DEFAULT_REGION: us-east-1 62 | AWS_REGION: us-east-1 63 | run: | 64 | pmapper graph create --localstack-endpoint http://localhost:4566 --include-regions us-east-1 --exclude-services autoscaling 65 | pmapper --account 000000000000 visualize 66 | mv 000000000000.svg /tmp/artifacts/visualization.svg 67 | pmapper --account 000000000000 analysis --output-type text > /tmp/artifacts/analysis.md 68 | - name: Generate PMapper Graph Artifact 69 | uses: actions/upload-artifact@v2 70 | with: 71 | name: Account Graph 72 | path: | 73 | /tmp/artifacts/*.svg 74 | /tmp/artifacts/*.md 75 | - name: Execute PMapper Test Cases 76 | continue-on-error: true 77 | env: 78 | PMAPPER_STORAGE: "/tmp/artifacts" 79 | run: | 80 | cd $GITHUB_WORKSPACE/testcode 81 | echo $((python -m unittest -v test_permissions.py) 2> results.txt) 82 | cat results.txt 83 | echo -e 'PMapper Test Results:\n\n```' > header.txt 84 | echo -e '\n```' > footer.txt 85 | cat header.txt results.txt footer.txt > full_output.txt 86 | jq -Rs '{ body: . }' full_output.txt > json_output.txt 87 | id: pmappertests 88 | - name: Update PR 89 | if: ${{ github.event_name == 'pull_request' }} 90 | env: 91 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 92 | run: | 93 | curl -X POST \ 94 | --user ${{ github.actor }}:$GITHUB_TOKEN \ 95 | -H 'Accept: application/vnd.github.v3+json' \ 96 | --data "@$GITHUB_WORKSPACE/testcode/json_output.txt" \ 97 | "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/issues/${{ github.event.pull_request.number }}/comments" 98 | 99 | - name: Process Test Results 100 | run: | 101 | cd $GITHUB_WORKSPACE/testcode 102 | python -c 'import sys; sys.exit(1) if "FAILED" in open("results.txt").read() else sys.exit(0)' 103 | -------------------------------------------------------------------------------- /.github/workflows/infra_scout_checks.yml: -------------------------------------------------------------------------------- 1 | # GitHub Workflow to execute test cases via Scout Suite 2 | 3 | name: "Scout Suite Checks" 4 | 5 | # Run when PR against main comes through with infracode/testcode changes 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - "infracode/**" 12 | - "testcode/**" 13 | types: 14 | - opened 15 | - reopened 16 | - synchronize 17 | workflow_dispatch: 18 | 19 | 20 | jobs: 21 | execute: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout Code 25 | uses: actions/checkout@v2 26 | - name: Install Graphviz 27 | run: "sudo apt-get install -y jq" 28 | - name: Setup Terraform 29 | uses: hashicorp/setup-terraform@v1 30 | - name: Init Terraform 31 | run: | 32 | cd $GITHUB_WORKSPACE/infracode 33 | terraform init 34 | - name: Validate Config 35 | run: | 36 | cd $GITHUB_WORKSPACE/infracode 37 | terraform validate -no-color 38 | - name: Setup LocalStack 39 | run: | 40 | echo "Install LocalStack, AWS CLI, Scout Suite via pip" 41 | pip install localstack scoutsuite mitmproxy 42 | echo "Pull localstack/localstack from Docker" 43 | docker pull localstack/localstack 44 | echo "Start LocalStack" 45 | localstack start -d 46 | echo "Wait for LocalStack" 47 | localstack wait -t 30 48 | echo "LocalStack running" 49 | - name: Deploy Infra Code 50 | run: | 51 | cd $GITHUB_WORKSPACE/infracode 52 | terraform apply -auto-approve -var "acctid=000000000000" 53 | - name: Make Artifacts Directory 54 | run: | 55 | mkdir -p /tmp/artifacts 56 | - name: Launch Mitmproxy 57 | run: | 58 | nohup mitmdump --listen-host 127.0.0.1 --listen-port 8080 -s $GITHUB_WORKSPACE/mitmproxy/proxy_aws_to_localstack.py -k --quiet & 59 | disown 60 | - name: Create Scout Suite Output 61 | env: 62 | AWS_ACCESS_KEY_ID: AKIAFAKEFAKEFAKE 63 | AWS_SECRET_ACCESS_KEY: alsofakejustmakesthingswork 64 | AWS_DEFAULT_REGION: us-east-1 65 | AWS_REGION: us-east-1 66 | HTTP_PROXY: http://127.0.0.1:8080 67 | HTTPS_PROXY: http://127.0.0.1:8080 68 | AWS_CA_BUNDLE: ~/.mitmproxy/mitmproxy-ca-cert.pem 69 | run: | 70 | echo $(scout aws --services ec2 vpc iam s3 --report-dir /tmp/artifacts/scout-dir --no-browser) 71 | - name: Execute Scout Suite Test Cases 72 | continue-on-error: true 73 | run: | 74 | cd $GITHUB_WORKSPACE/testcode 75 | echo $((python -m unittest -v test_scoutsuite_rails.py) 2> results.txt) 76 | cat results.txt 77 | echo -e 'Scout Suite Test Results:\n\n```' > header.txt 78 | echo -e '\n```' > footer.txt 79 | cat header.txt results.txt footer.txt > full_output.txt 80 | jq -Rs '{ body: . }' full_output.txt > json_output.txt 81 | id: scouttests 82 | - name: Update PR 83 | if: ${{ github.event_name == 'pull_request' }} 84 | env: 85 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 86 | run: | 87 | curl -X POST \ 88 | --user ${{ github.actor }}:$GITHUB_TOKEN \ 89 | -H 'Accept: application/vnd.github.v3+json' \ 90 | --data "@$GITHUB_WORKSPACE/testcode/json_output.txt" \ 91 | "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/issues/${{ github.event.pull_request.number }}/comments" 92 | 93 | - name: Process Test Results 94 | run: | 95 | cd $GITHUB_WORKSPACE/testcode 96 | python -c 'import sys; sys.exit(1) if "FAILED" in open("results.txt").read() else sys.exit(0)' 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDEA stuff 132 | .idea/ 133 | 134 | # Terraform background files 135 | *.tfstate 136 | *.tfstate.backup 137 | *.lock.hcl 138 | .terraform/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 NCC Group and Erik Steringer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aerides 2 | 3 | An implementation of infrastructure-as-code scanning using dynamic tooling. 4 | 5 | ## Background 6 | 7 | This project is a demonstration for using Scout Suite and Principal Mapper with Terraform and LocalStack. Scout and 8 | PMapper are "dynamic" tools that operate by interacting with the AWS APIs to retrieve data. These tools do not have 9 | the capability to read/interpret infrastructure-as-code (IaC) files. 10 | 11 | However, by deploying IaC (Terraform HCL in this case) against an instance of LocalStack, then pointing the tools at 12 | LocalStack, we can still perform scanning/testing to identify risks before they make it to production infrastructure. 13 | 14 | ## Implementation 15 | 16 | This repository contains Terraform code that implements parts of a service, called TKTK Service. That includes S3 17 | buckets, VPCs + Security Groups, and IAM Users/Groups/Roles. 18 | 19 | This code can be deployed to LocalStack (see [infracode/main.tf](infracode/main.tf) for details). 20 | 21 | Once deployed, it is possible to use PMapper and Scout Suite to interact with LocalStack. PMapper supports LocalStack 22 | out of the box (via `graph create` with the `--localstack-endpoint` parameter). However, Scout Suite does not have 23 | built-in support. Instead, there's a mitmproxy addon script in 24 | [mitmproxy/proxy_aws_to_localstack.py](mitmproxy/proxy_aws_to_localstack.py). By setting the `HTTP_PROXY`, 25 | `HTTPS_PROXY`, and `AWS_CA_BUNDLE` environment variables to point to a running proxy with that script, it is possible 26 | to point Scout Suite (and any other dynamic tools) to LocalStack. 27 | 28 | After PMapper and Scout Suite run, they leave different outputs that can be handled with test code. In this demo, 29 | there are test cases that check for privlege escalation risks, restrictions on lower-priv users from calling 30 | s3:PutObject, IAM Policies with NotAction fields, and security groups that open ports to the world. This repo has 31 | GitHub Actions that will execute these test cases for Pull Requests. 32 | 33 | ## Experimenting 34 | 35 | To try this out with your own machine, follow these steps (tested on Ubuntu 20.04): 36 | 37 | ### Prerequisites 38 | 39 | * Python 3.8+, using a virtualenv is *highly* recommended 40 | * [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) 41 | * [LocalStack](https://github.com/localstack/localstack) (installed via `pip install localstack`) 42 | * [mitmproxy](https://mitmproxy.org/) (installed via `pip install mitmproxy`, consider using a separate virtualenv) 43 | * [Scout Suite](https://github.com/nccgroup/ScoutSuite) (installed via `pip install scoutsuite`) 44 | * [Principal Mapper](https://github.com/nccgroup/PMapper) (installed via `pip install principalmapper`) 45 | 46 | ### Running 47 | 48 | Clone this repository onto your machine. Navigate into the `Aerides/infracode` directory and run: 49 | 50 | ```bash 51 | localstack start -d # this will take ~30s to spin up 52 | terraform init 53 | terraform apply -var "acctid=000000000000" 54 | ``` 55 | 56 | This will launch LocalStack (daemon mode) and deploy the Terraform code. Now it is possible to run commands and see 57 | the mock infrastructure. For example: 58 | 59 | ```bash 60 | aws configure --profile localstack # set fake access keys, set default region to us-east-1 61 | aws --profile localstack --endpoint-url http://localhost:4566 iam list-users 62 | ``` 63 | 64 | Run PMapper against LocalStack like so: 65 | 66 | ```bash 67 | pmapper --profile localstack graph create --localstack-endpoint http://localhost:4566 --exclude-services autoscaling 68 | pmapper --account 000000000000 visualize # should output 000000000000.svg if graphviz is installed 69 | ``` 70 | 71 | **In a separate shell**, navigate to the `Aerides/mitmproxy` directory and run: 72 | 73 | ```bash 74 | mitmdump -k --listen-host 127.0.0.1 --listen-port 8080 -s proxy_aws_to_localstack.py 75 | ``` 76 | 77 | With `mitmdump` running, go back to your first shell and run Scout Suite while using the proxy like so: 78 | 79 | ```bash 80 | # Consider exposting these variables if you need to run multiple commands 81 | HTTP_PROXY=http://127.0.0.1:8080 \ 82 | HTTPS_PROXY=http://127.0.0.1:8080 \ 83 | AWS_CA_BUNDLE=~/.mitmproxy/mitmproxy-ca-cert.pem \ 84 | scout aws --services iam s3 ec2 vpc --region us-east-1 85 | ``` 86 | 87 | This should generate a Scout Suite report and launch your web browser with its contents. You can try a similar 88 | pattern with other tools. We have been able to successfully use the following tools: 89 | 90 | * [AWS-Inventory](https://github.com/nccgroup/aws-inventory/) 91 | * [CloudMapper](https://github.com/duo-labs/cloudmapper) 92 | * [Prowler](https://github.com/toniblyx/prowler) 93 | * [Cartography](https://github.com/lyft/cartography) 94 | 95 | **Note:** This does not constitute an endorsement of support on the behalf of those projects. Due to mismatches between 96 | LocalStack's responses and the AWS API's responses, these tools run into unexpected errors. You'll have to limit which 97 | regions/services/checks the tools run and limit which test cases you attempt to perform via these tools. 98 | 99 | ## General Implementation of this Technique 100 | 101 | While this repository is hosted on GitHub and uses GitHub Actions, we can use the same technique in other CI 102 | solutions. The general process is: 103 | 104 | 1. Download the IaC onto the host/container/runner that is executing the CI process 105 | 2. Install dependencies: LocalStack, Terraform, and any additional dynamic tools to use for testing (consider creating an image with this already installed) 106 | 3. Initialize LocalStack and run it in the background throughout the remainder of the process (`-d` parameter) 107 | 4. Initialize Terraform 108 | 5. Use Terraform to apply the IaC to LocalStack 109 | 6. (If needed) Initialize mitmproxy and allow it to run in the background throughout the remainder of the process (`nohup` and `disown` on Ubuntu) 110 | 7. Run dynamic tools to gather data from LocalStack, using the proxy where necessary 111 | 8. Run test cases against the data gathered from the dynamic tools, or run scripts that normally call the AWS APIs 112 | 113 | 114 | ## License 115 | 116 | MIT, see [LICENSE](./LICENSE). 117 | -------------------------------------------------------------------------------- /infracode/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | access_key = "fake_access_key" 3 | secret_key = "fake_secret_key" 4 | region = "us-east-1" 5 | s3_force_path_style = true 6 | skip_credentials_validation = true 7 | skip_metadata_api_check = true 8 | skip_requesting_account_id = true 9 | 10 | endpoints { 11 | ec2 = "http://localhost:4566" 12 | iam = "http://localhost:4566" 13 | lambda = "http://localhost:4566" 14 | s3 = "http://localhost:4566" 15 | secretsmanager = "http://localhost:4566" 16 | sts = "http://localhost:4566" 17 | } 18 | } 19 | 20 | module "api" { 21 | source = "./modules/tktk_service_api" 22 | acctid = var.acctid 23 | } 24 | 25 | module "principals" { 26 | source = "./modules/tktk_service_other_personnel" 27 | acctid = var.acctid 28 | } 29 | 30 | module "finances" { 31 | source = "./modules/tktk_service_finances" 32 | acctid = var.acctid 33 | } 34 | 35 | module "support" { 36 | source = "./modules/tktk_service_support" 37 | acctid = var.acctid 38 | } 39 | 40 | module "website" { 41 | source = "./modules/tktk_service_website" 42 | acctid = var.acctid 43 | } 44 | 45 | -------------------------------------------------------------------------------- /infracode/modules/tktk_service_api/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | /* 22 | Infracode for the TKTK Service's API, the "api" component as reflected 23 | in resource tags. The resources that are most necessary to demonstrate 24 | in Aerides are written out and ready to deploy on LocalStack. 25 | 26 | Includes: 27 | * IAM Role (with inline policy) + Instance Profile 28 | * S3 Bucket (with bucket policy) 29 | 30 | Excluded: 31 | * DynamoDB Table 32 | * EC2 AutoScaling Group 33 | * Application Load Balancer 34 | */ 35 | 36 | resource "aws_s3_bucket" "api_logs_bucket" { 37 | bucket = "tktk-service-api-logs" 38 | acl = "private" 39 | policy = jsonencode({ 40 | Version = "2012-10-17" 41 | Statement = [ 42 | { 43 | Effect = "Allow" 44 | Principal = "*" 45 | Action = "s3:GetObject" 46 | Resource = "arn:aws:s3:::tktk-service-api-logs/*" 47 | Condition = { 48 | StringEquals = { 49 | "aws:SourceAccount" = var.acctid, 50 | "aws:PrincipalTag/dept": [ 51 | "developers", 52 | "operators", 53 | "support" 54 | ] 55 | } 56 | } 57 | } 58 | ] 59 | }) 60 | tags = { 61 | component = "api" 62 | } 63 | } 64 | 65 | resource "aws_iam_role" "api_host_role" { 66 | name = "APIEC2BackendHostRole" 67 | tags = { 68 | component = "api" 69 | notexposed = "false" 70 | } 71 | assume_role_policy = jsonencode({ 72 | Version = "2012-10-17" 73 | Statement = [ 74 | { 75 | Effect = "Allow" 76 | Action = "sts:AssumeRole" 77 | Principal = { 78 | Service = "ec2.amazonaws.com" 79 | } 80 | } 81 | ] 82 | }) 83 | inline_policy { 84 | name = "permissions-1" 85 | policy = jsonencode({ 86 | Version = "2012-10-17" 87 | Statement = [ 88 | { 89 | Effect = "Allow" 90 | Action = "s3:PutObject" 91 | Resource = format("%s/*", aws_s3_bucket.api_logs_bucket.arn) 92 | }, 93 | { 94 | Effect = "Allow" 95 | Action = [ 96 | "dynamodb:Query", 97 | "dynamodb:Scan", 98 | "dynamodb:PutItem" 99 | ] 100 | Resource = format("arn:aws:dynamodb:us-east-1:%s:table/api_data", var.acctid) 101 | } 102 | ] 103 | }) 104 | } 105 | } 106 | 107 | resource "aws_iam_instance_profile" "api_host_instance_profile" { 108 | name = "APIEC2BackendHostRoleInstanceProfile" 109 | role = aws_iam_role.api_host_role.name 110 | } 111 | 112 | resource "aws_iam_user" "api_developer_user" { 113 | name = "robert" 114 | tags = { 115 | dept = "developers" 116 | component = "api" 117 | } 118 | } 119 | 120 | resource "aws_iam_user_policy" "api_developer_user_policy" { 121 | user = aws_iam_user.api_developer_user.name 122 | policy = jsonencode({ 123 | Version = "2012-10-17" 124 | Statement = [ 125 | { 126 | Effect = "Allow" 127 | Action = [ 128 | "ec2:*", 129 | "autoscaling:*", 130 | "elasticloadbalancer:*", 131 | "dynamodb:*" 132 | ], 133 | Resource = "*" 134 | }, 135 | { 136 | Effect = "Allow" 137 | Action = "iam:PassRole" 138 | Resource = "arn:aws:iam::*:role/APIEC2BackendHostRole" 139 | Condition = { 140 | "StringEquals" = { 141 | "iam:PassedToService" = "ec2.amazonaws.com" 142 | } 143 | } 144 | } 145 | ] 146 | }) 147 | } 148 | 149 | resource "aws_vpc" "api_vpc" { 150 | cidr_block = "10.0.1.0/24" 151 | tags = { 152 | component = "api" 153 | } 154 | } 155 | 156 | resource "aws_security_group" "api_sg" { 157 | ingress { 158 | description = "Incoming TLS" 159 | protocol = "tcp" 160 | from_port = 443 161 | to_port = 443 162 | cidr_blocks = ["10.0.0.0/8"] 163 | } 164 | 165 | egress { 166 | from_port = 0 167 | protocol = "-1" 168 | to_port = 0 169 | cidr_blocks = ["0.0.0.0/0"] 170 | ipv6_cidr_blocks = ["::/0"] 171 | } 172 | 173 | tags = { 174 | component = "api" 175 | } 176 | } 177 | 178 | /* 179 | NOTE 180 | 181 | The remainder of items in here are just for demonstration, and 182 | show how the rest of this template would theoretically be set up. 183 | */ 184 | 185 | // resource "aws_dynamodb_table" "api_backend_ddb" { } 186 | 187 | // resource "aws_autoscaling_group" "api_backend_hosts" { } 188 | 189 | // resource "aws_alb" "api_load_balancer" { } 190 | 191 | -------------------------------------------------------------------------------- /infracode/modules/tktk_service_api/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # Input variable definitions 22 | 23 | variable "acctid" { 24 | description = "The AWS Account ID for which this is meant to be deployed to" 25 | type = string 26 | } -------------------------------------------------------------------------------- /infracode/modules/tktk_service_finances/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | /* 22 | Infracode for the finance department. For this demo, the finance 23 | resources are tagged with the "finance" component. 24 | */ 25 | 26 | resource "aws_s3_bucket" "service_ledger" { 27 | bucket = "tktk-service-ledger" 28 | acl = "private" 29 | policy = jsonencode({ 30 | Version = "2012-10-17" 31 | Statement = [ 32 | { 33 | Effect = "Deny" 34 | Principal = "*" 35 | Action = "s3:GetObject*" 36 | Resource = "arn:aws:s3:::tktk-service-ledger/*" 37 | Condition = { 38 | StringNotEquals = { 39 | "aws:PrincipalTag/dept" = "finance" 40 | } 41 | } 42 | } 43 | ] 44 | }) 45 | } 46 | 47 | resource "aws_iam_user" "finance_user_warren" { 48 | name = "warren" 49 | tags = { 50 | dept = "finance" 51 | } 52 | } 53 | 54 | resource "aws_iam_user_policy" "finance_user_warren_perms" { 55 | name = "warren-permissions-1" 56 | user = aws_iam_user.finance_user_warren.name 57 | policy = jsonencode({ 58 | Version = "2012-10-17" 59 | Statement = [ 60 | { 61 | Effect = "Allow" 62 | Action = "s3:GetObject*" 63 | Resource = "arn:aws:s3:::tktk-service-ledger/inbox/*" 64 | }, 65 | { 66 | Effect = "Allow" 67 | Action = "s3:PutObject" 68 | Resource = "arn:aws:s3:::tktk-service-ledger/outbox/*" 69 | } 70 | ] 71 | }) 72 | } 73 | 74 | -------------------------------------------------------------------------------- /infracode/modules/tktk_service_finances/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # Input variable definitions 22 | 23 | variable "acctid" { 24 | description = "The AWS Account ID for which this is meant to be deployed to" 25 | type = string 26 | } -------------------------------------------------------------------------------- /infracode/modules/tktk_service_other_personnel/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | /* 22 | Infracode for IAM Users/Roles. For this demo, there will be a few 23 | IAM Users that represent operators/administrators over the infrastructure 24 | and a few IAM Roles for support personnel. 25 | */ 26 | 27 | // region Account Admins 28 | 29 | /* 30 | These IAM Users are the admins of the AWS account, with the AdministratorAccess policy 31 | and the admin dept tag. They have permissions granted via IAM Group membership. 32 | */ 33 | 34 | resource "aws_iam_group" "account_admins" { 35 | name = "admins" 36 | } 37 | 38 | resource "aws_iam_group_policy_attachment" "admin_to_admins" { 39 | group = aws_iam_group.account_admins.name 40 | policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" 41 | } 42 | 43 | resource "aws_iam_user" "admin_erik" { 44 | name = "erik" 45 | tags = { 46 | dept = "admin" 47 | } 48 | } 49 | 50 | resource "aws_iam_user" "admin_mary" { 51 | name = "mary" 52 | tags = { 53 | dept = "admin" 54 | } 55 | } 56 | 57 | resource "aws_iam_group_membership" "admins_memberships" { 58 | name = "admin_group_memberships" 59 | users = [ 60 | aws_iam_user.admin_erik.name, 61 | aws_iam_user.admin_mary.name 62 | ] 63 | group = aws_iam_group.account_admins.name 64 | } 65 | 66 | // endregion Account Admins 67 | 68 | // region Operators 69 | 70 | /* 71 | These IAM Users are operators in the AWS account, with the a custom policy 72 | and the operator dept tag. They have permissions granted via IAM Group membership, which 73 | are limited to EC2/AutoScaling/DDB/ELB 74 | */ 75 | 76 | resource "aws_iam_group" "account_operators" { 77 | name = "operators" 78 | } 79 | 80 | resource "aws_iam_group_policy" "operator_policy" { 81 | name = "operator-policy" 82 | group = aws_iam_group.account_operators.name 83 | 84 | policy = jsonencode({ 85 | Version = "2012-10-17" 86 | Statement = [ 87 | { 88 | Effect = "Allow" 89 | Action = [ 90 | "ec2:*", 91 | "autoscaling:*", 92 | "dynamodb:*", 93 | "elasticloadbalancing:*" 94 | ], 95 | Resource = "*" 96 | }, 97 | { 98 | Effect = "Allow" 99 | Action = "iam:PassRole", 100 | Resource = "arn:aws:iam::*:role/APIEC2BackendHostRole", 101 | Condition = { 102 | "StringEquals" = { 103 | "iam:PassedToService": "ec2.amazonaws.com" 104 | } 105 | } 106 | } 107 | ] 108 | }) 109 | } 110 | 111 | resource "aws_iam_user" "operator_frank" { 112 | name = "frank" 113 | tags = { 114 | dept = "operators" 115 | } 116 | } 117 | 118 | resource "aws_iam_user" "operator_john" { 119 | name = "john" 120 | tags = { 121 | dept = "operators" 122 | } 123 | } 124 | 125 | resource "aws_iam_user" "operator_adam" { 126 | name = "adam" 127 | tags = { 128 | dept = "operators" 129 | } 130 | } 131 | 132 | resource "aws_iam_group_membership" "admin_memberships" { 133 | name = "admin_group_memberships" 134 | users = [ 135 | aws_iam_user.operator_frank.name, 136 | aws_iam_user.operator_john.name, 137 | aws_iam_user.operator_adam.name 138 | ] 139 | group = aws_iam_group.account_operators.name 140 | } 141 | 142 | // endregion Operators 143 | -------------------------------------------------------------------------------- /infracode/modules/tktk_service_other_personnel/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # Input variable definitions 22 | 23 | variable "acctid" { 24 | description = "The AWS Account ID for which this is meant to be deployed to" 25 | type = string 26 | } -------------------------------------------------------------------------------- /infracode/modules/tktk_service_support/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | /* 22 | Infracode for the support staff. For this demo, the support staff access the 23 | account through IAM Role Sessions (Cognito AuthN), while this code defines the IAM Role resource 24 | plus the associated policy. 25 | */ 26 | 27 | resource "aws_iam_role" "support_iam_role" { 28 | name = "support-staff" 29 | assume_role_policy = jsonencode({ 30 | Version = "2012-10-17" 31 | Statement = [ 32 | { 33 | Effect = "Allow" 34 | Principal = { 35 | Federated = "cognito-identity.amazonaws.com" 36 | } 37 | Action = "sts:AssumeRoleWithWebIdentity" 38 | Condition = { 39 | StringEquals = { 40 | "cognito-identity.amazonaws.com:aud" = "us-east:12345678-ffff-ffff-ffff-123456" 41 | } 42 | } 43 | } 44 | ] 45 | }) 46 | tags = { 47 | dept = "support" 48 | } 49 | } 50 | 51 | resource "aws_iam_role_policy" "support_iam_role_policy" { 52 | name = "support-permissions-1" 53 | role = aws_iam_role.support_iam_role.name 54 | policy = jsonencode({ 55 | Version = "2012-10-17" 56 | Statement = [ 57 | { 58 | Effect = "Allow" 59 | Action = [ 60 | "ec2:Describe*", 61 | "autoscaling:Get*", 62 | "elasticloadbalancing:List*", 63 | "elasticloadbalancing:Get*" 64 | ], 65 | Resource = "*" 66 | } 67 | ] 68 | }) 69 | } 70 | 71 | -------------------------------------------------------------------------------- /infracode/modules/tktk_service_support/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # Input variable definitions 22 | 23 | variable "acctid" { 24 | description = "The AWS Account ID for which this is meant to be deployed to" 25 | type = string 26 | } -------------------------------------------------------------------------------- /infracode/modules/tktk_service_website/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | /* 22 | Infracode for a static website (S3). Also includes the user that has 23 | access to update the website. 24 | */ 25 | 26 | resource "aws_s3_bucket" "service_website" { 27 | bucket = "tktk-service-website" 28 | acl = "public-read" 29 | policy = jsonencode({ 30 | Version = "2012-10-17" 31 | Statement = [ 32 | { 33 | Effect = "Allow" 34 | Principal = "*" 35 | Action = "s3:GetObject" 36 | Resource = "arn:aws:s3:::tktk-service-website/*" 37 | } 38 | ] 39 | }) 40 | website { 41 | index_document = "index.html" 42 | error_document = "error.html" 43 | routing_rules = jsonencode([ 44 | { 45 | Condition = { 46 | KeyPrefixEquals = "src/" 47 | } 48 | Redirect = { 49 | ReplaceKeyPrefixWith = "source/" 50 | } 51 | } 52 | ]) 53 | } 54 | tags = { 55 | component = "website" 56 | } 57 | } 58 | 59 | resource "aws_iam_user" "website_user_gabe" { 60 | name = "gabe" 61 | tags = { 62 | dept = "developers" 63 | component = "website" 64 | } 65 | } 66 | 67 | resource "aws_iam_user_policy" "website_user_gabe_policy" { 68 | name = "permissions-1" 69 | user = aws_iam_user.website_user_gabe.name 70 | policy = jsonencode({ 71 | Version = "2012-10-17" 72 | Statement = [ 73 | { 74 | Effect = "Allow" 75 | Action = [ 76 | "s3:GetObject", 77 | "s3:PutObject" 78 | ] 79 | Resource = "arn:aws:s3:::tktk-service-website/*" 80 | } 81 | ] 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /infracode/modules/tktk_service_website/variables.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | # Input variable definitions 22 | 23 | variable "acctid" { 24 | description = "The AWS Account ID for which this is meant to be deployed to" 25 | type = string 26 | } -------------------------------------------------------------------------------- /infracode/variables.tf: -------------------------------------------------------------------------------- 1 | # Input variable definitions 2 | 3 | variable "acctid" { 4 | description = "The AWS Account ID for which this is meant to be deployed to" 5 | type = string 6 | } -------------------------------------------------------------------------------- /mitmproxy/proxy_aws_to_localstack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | The following Python code creates a simple mitmproxy addon. This addon changes the 23 | destination of a proxied HTTP(S) request to http://localhost:4566, the default 24 | endpoint of LocalStack. 25 | 26 | This addon should allow users to run dynamic AWS tooling (Scout Suite) against 27 | LocalStack without needing to modify those tools. This requires installing 28 | mitmproxy, configuring the HTTP_PROXY + HTTPS_PROXY + AWS_CA_BUNDLE environment 29 | variables, and running the tool. 30 | 31 | For example, with Scout Suite: 32 | 33 | ```bash 34 | HTTP_PROXY=http://127.0.0.1:8080 \ 35 | HTTPS_PROXY=http://127.0.0.1:8080 \ 36 | AWS_CA_BUNDLE=~/.mitmproxy/mitmproxy-ca-cert.pem \ 37 | scout aws -r us-east-1 --services iam 38 | ``` 39 | """ 40 | 41 | import mitmproxy.http 42 | 43 | 44 | class Relay: 45 | def __init__(self): 46 | pass 47 | 48 | def request(self, flow: mitmproxy.http.HTTPFlow): 49 | # For S3 API interactions, we wanna translate .s3.[].amazonaws.com 50 | # requests to s3.amazonaws.com/ requests. 51 | # TODO: Currently has the side-effect of making Scout think your buckets are hosted in af-south-1 52 | 53 | host = flow.request.host 54 | if '.s3.' in host: 55 | bucket_name = host.split('.')[0] 56 | flow.request.path = '/{}{}'.format(bucket_name, flow.request.path) 57 | 58 | flow.request.host = '127.0.0.1' 59 | flow.request.port = 4566 60 | flow.request.scheme = b'http' 61 | 62 | def response(self, flow: mitmproxy.http.HTTPFlow): 63 | # For EC2 requests calling DescribeRegions that appear to be trying to filter for 64 | # "not-opted-in" regions, we manipulate the response from LocalStack to ensure 65 | # Scout behaves correctly 66 | 67 | if flow.request.text is not None and 'DescribeRegions' in flow.request.text and 'opt-in-status' in flow.request.text and 'not-opted-in' in flow.request.text: 68 | flow.response.text = flow.response.text.replace('east', 'fake') 69 | flow.response.text = flow.response.text.replace('west', 'fake') 70 | 71 | 72 | addons = [ 73 | Relay() 74 | ] 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | principalmapper>=1.1.4 2 | scoutsuite~=5.10.2 3 | localstack~=0.13.2.1 4 | 5 | # NOTE: Consider installing this separately 6 | # mitmproxy~=7.0.4 -------------------------------------------------------------------------------- /testcode/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncc-erik-steringer/Aerides/6f5c70447cd42667157586303abc9ea1313c4cff/testcode/__init__.py -------------------------------------------------------------------------------- /testcode/test_permissions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import List, Tuple, Optional 22 | import unittest 23 | 24 | from principalmapper.common import * 25 | from principalmapper.querying import query_interface 26 | from principalmapper.querying.presets.privesc import can_privesc 27 | from principalmapper.util.storage import get_default_graph_path 28 | 29 | 30 | class TestAuthorizationBoundaries(unittest.TestCase): 31 | 32 | def setUp(self) -> None: 33 | self.test_graph = Graph.create_graph_from_local_disk(get_default_graph_path('000000000000')) 34 | 35 | def test_no_privesc(self): 36 | """Ensure that nobody can escalate their privileges from non-admin to to admin.""" 37 | privesc_paths = [] # type: List[Tuple[Node, List[Edge]]] 38 | for node in self.test_graph.nodes: 39 | if not node.is_admin: 40 | can_escalate, escalation_path = can_privesc(self.test_graph, node) 41 | if can_escalate: 42 | privesc_paths.append((node, escalation_path)) 43 | 44 | if len(privesc_paths) > 0: 45 | self.fail( 46 | 'Privilege escalation risks detected:\n\n{}'.format( 47 | '\n'.join( 48 | ['* {}'.format(', '.join([x.describe_edge() for x in path])) for node, path in privesc_paths] 49 | ) 50 | ) 51 | ) 52 | 53 | def test_support_cannot_put(self): 54 | """Ensure that the IAM Role named 'support-staff' cannot call s3:PutObject for any of the S3 buckets.""" 55 | 56 | s3_bucket_policies = [] 57 | for policy in self.test_graph.policies: 58 | if 'arn:aws:s3' in policy.arn: 59 | s3_bucket_policies.append(policy) 60 | 61 | support_role = self.test_graph.get_node_by_searchable_name('role/support-staff') 62 | 63 | violations = [] 64 | for s3_bucket_policy in s3_bucket_policies: 65 | test_arn = s3_bucket_policy.arn + '/test_object' 66 | result = query_interface.search_authorization_full( 67 | self.test_graph, 68 | support_role, 69 | 's3:PutObject', 70 | test_arn, 71 | {}, 72 | s3_bucket_policy.policy_doc, 73 | '000000000000' 74 | ) 75 | if result.allowed: 76 | violations.append('{} is allowed to call s3:PutObject for {}'.format( 77 | support_role.searchable_name(), 78 | test_arn 79 | )) 80 | 81 | if len(violations) > 0: 82 | self.fail('Support was allowed to upload files to S3:\n\n{}'.format( 83 | '\n'.join(['* {}'.format(x) for x in violations]) 84 | )) 85 | 86 | def test_support_has_no_edges(self): 87 | """Ensure that the IAM Role named 'support-staff' cannot access any other users or roles in the account.""" 88 | 89 | support_edges = [] # type: List[Edge] 90 | for edge in self.test_graph.edges: 91 | if edge.source.searchable_name() == 'role/support-staff': 92 | support_edges.append(edge) 93 | 94 | if len(support_edges) > 0: 95 | self.fail('The support staff role had access to other users or roles in the account:\n\n{}'.format( 96 | '\n'.join( 97 | [edge.describe_edge() for edge in support_edges] 98 | ) 99 | )) 100 | 101 | def tearDown(self) -> None: 102 | del self.test_graph # free up memory? 103 | 104 | 105 | if __name__ == '__main__': 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /testcode/test_scoutsuite_rails.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Erik Steringer and NCC Group 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import json 22 | from typing import Optional, Union 23 | import unittest 24 | 25 | 26 | def _get_item(ptr, itempath: str) -> Union[str, dict, list, None]: 27 | """Utility function. The ptr param should point at .services then follow the itempath (separated via '.') to 28 | the expected object. Returns None if invalid (tries to avoid Error).""" 29 | root = ptr 30 | for element in itempath.split('.'): 31 | if isinstance(root, dict): 32 | if element in root: 33 | root = root[element] 34 | else: 35 | return None 36 | elif isinstance(root, list): 37 | index = int(element) 38 | if not (0 <= index < len(root)): 39 | return None 40 | root = root[index] 41 | else: 42 | return None 43 | return root 44 | 45 | 46 | class TestScoutSuiteExpected(unittest.TestCase): 47 | 48 | def setUp(self) -> None: 49 | # per https://github.com/nccgroup/ScoutSuite/wiki/Exporting-and-Programmatically-Accessing-the-Report 50 | 51 | with open('/tmp/artifacts/scout-dir/scoutsuite-results/scoutsuite_results_aws-000000000000.js') as fd: 52 | fd.readline() # discard first line 53 | self.scoutdata = json.load(fd) # type: dict 54 | 55 | def test_ec2_no_ports_open_to_all(self): 56 | """Verify that none of the security groups have a port open to 0.0.0.0/0""" 57 | 58 | # start by grabbing a handle to the .services.ec2.findings dict 59 | ptr = _get_item(self.scoutdata, 'services.ec2.findings') 60 | if ptr is None: 61 | self.fail('Expected path services.ec2.findings in Scout Suite data was not found') 62 | 63 | # look at all findings for "port is open", group them up, report 64 | issues = [] 65 | for finding, data in ptr.items(): 66 | if 'ec2-security-group-opens' not in finding or 'port-to-all' not in finding: 67 | continue 68 | 69 | if data['flagged_items'] > 0: 70 | issues.append((finding, data)) 71 | 72 | if len(issues) > 0: 73 | self.fail( 74 | 'ScoutSuite reported the following EC2 Security Group findings:\n\n{}'.format( 75 | '\n\n'.join( 76 | ['{}\n{}'.format(x, '\n'.join(y['items'])) for x, y in issues] 77 | ) 78 | ) 79 | ) 80 | 81 | def test_iam_no_inline_passrole(self): 82 | """Verify there are no inline policies granting iam:PassRole for *""" 83 | 84 | # get the handle 85 | ptr = _get_item(self.scoutdata, 'services.iam.findings') 86 | if ptr is None: 87 | self.fail('Expected path services.iam.findings in Scout Suite data was not found') 88 | 89 | # Review all iam-PassRole findings 90 | finding_names = ( 91 | 'iam-inline-role-policy-allows-iam-PassRole', 92 | 'iam-inline-user-policy-allows-iam-PassRole', 93 | 'iam-inline-group-policy-allows-iam-PassRole' 94 | ) 95 | finding_items = [] 96 | 97 | for finding_name in finding_names: 98 | finding_contents = ptr.get(finding_name) 99 | if finding_contents is not None and finding_contents['flagged_items'] > 0: 100 | finding_items.extend(finding_contents['items']) 101 | 102 | if len(finding_items) > 0: 103 | item_listing = [] 104 | for item in finding_items: 105 | root = self.scoutdata.get('services') 106 | item_ref = _get_item(root, '.'.join(item.split('.')[:3])) # type: Optional[dict] 107 | if item_ref is not None: 108 | item_listing.append(item_ref.get('arn')) 109 | else: 110 | item_listing.append(item) 111 | self.fail( 112 | 'The following IAM Users/Roles/Groups had an inline policy allowing ' 113 | 'iam:PassRole for all resources:\n\n{}'.format( 114 | '\n'.join(['* {}'.format(x) for x in item_listing]) 115 | ) 116 | ) 117 | 118 | def test_iam_no_inline_notaction(self): 119 | """Verify no inline IAM Policies (for Users/Roles/Groups) use the NotAction field""" 120 | 121 | # get the handle 122 | ptr = _get_item(self.scoutdata, 'services.iam.findings') 123 | if ptr is None: 124 | self.fail('Expected path services.iam.findings in Scout Suite data was not found') 125 | 126 | # Review all iam-PassRole findings 127 | finding_names = ( 128 | 'iam-inline-role-policy-allows-NotActions', 129 | 'iam-inline-user-policy-allows-NotActions', 130 | 'iam-inline-group-policy-allows-NotActions' 131 | ) 132 | finding_items = [] 133 | 134 | for finding_name in finding_names: 135 | finding_contents = ptr.get(finding_name) 136 | if finding_contents is not None: 137 | finding_items.extend(finding_contents['items']) 138 | 139 | if len(finding_items) > 0: 140 | item_listing = [] 141 | for item in finding_items: 142 | root = self.scoutdata.get('services') 143 | item_ref = _get_item(root, '.'.join(item.split('.')[:3])) # type: Optional[dict] 144 | if item_ref is not None: 145 | item_listing.append(item_ref.get('arn')) 146 | else: 147 | item_listing.append(item) 148 | self.fail( 149 | 'The following IAM Users/Roles/Groups had an inline policy that uses ' 150 | 'NotAction in a statement:\n\n{}'.format( 151 | '\n'.join(['* {}'.format(x) for x in item_listing]) 152 | ) 153 | ) 154 | 155 | def tearDown(self) -> None: 156 | del self.scoutdata 157 | 158 | 159 | if __name__ == '__main__': 160 | unittest.main() 161 | --------------------------------------------------------------------------------