From ab180f3b23475c88c814f9410d5a439edf23586f Mon Sep 17 00:00:00 2001 From: gruebel Date: Sun, 21 Jul 2024 18:41:50 +0200 Subject: [PATCH] add pre-commit and replace black with ruff-format --- .github/workflows/bump-version.yml | 8 +- .github/workflows/nodejs-test.yml | 2 +- .github/workflows/publish.yml | 24 +- .../workflows/python-dependency-updater.yml | 8 +- .github/workflows/security.yml | 2 +- .github/workflows/test.yml | 21 +- .github/workflows/update-bundle-report.yml | 4 +- .pre-commit-config.yaml | 9 + Makefile | 4 - cloudsplaining/bin/cli.py | 7 +- .../command/create_exclusions_file.py | 4 +- .../create_multi_account_config_file.py | 8 +- cloudsplaining/command/download.py | 18 +- cloudsplaining/command/expand_policy.py | 4 +- cloudsplaining/command/scan_multi_account.py | 56 +-- cloudsplaining/command/scan_policy_file.py | 52 +-- cloudsplaining/output/policy_finding.py | 31 +- .../scan/assume_role_policy_document.py | 4 +- cloudsplaining/scan/authorization_details.py | 20 +- cloudsplaining/scan/group_details.py | 35 +- cloudsplaining/scan/inline_policy.py | 64 +--- cloudsplaining/scan/managed_policy_detail.py | 91 ++--- cloudsplaining/scan/policy_document.py | 67 +--- .../scan/resource_policy_document.py | 8 +- cloudsplaining/scan/role_details.py | 33 +- cloudsplaining/scan/statement_detail.py | 54 +-- cloudsplaining/scan/user_details.py | 38 +- cloudsplaining/shared/aws_login.py | 16 +- cloudsplaining/shared/constants.py | 4 +- cloudsplaining/shared/exceptions.py | 1 + cloudsplaining/shared/exclusions.py | 8 +- cloudsplaining/shared/utils.py | 16 +- cloudsplaining/shared/validation.py | 4 +- examples/jira-tickets/open_jira_ticket.py | 62 +--- examples/scripts/scripting_example.py | 8 +- pyproject.toml | 3 + requirements-dev.txt | 4 +- setup.py | 61 ++-- test/command/test_expand_policy.py | 8 +- test/command/test_scan.py | 9 +- test/command/test_scan_policy_file.py | 327 +++++++----------- test/output/test_policy_finding.py | 232 +++++-------- test/scanning/test_action_links.py | 18 +- test/scanning/test_authorization_details.py | 79 +++-- test/scanning/test_group_detail_list.py | 2 +- test/scanning/test_inline_policy.py | 25 +- test/scanning/test_managed_policy_detail.py | 2 +- test/scanning/test_policy_document.py | 311 +++++++---------- test/scanning/test_statement_detail.py | 107 ++---- test/scanning/test_trust_policies.py | 16 +- test/scanning/test_user_detail_list.py | 12 +- test/shared/test_exclusions.py | 70 +--- test/shared/test_utils.py | 8 +- test/shared/test_validation.py | 4 +- utils/generate_example_iam_data.py | 17 +- utils/generate_example_report.py | 13 +- 56 files changed, 766 insertions(+), 1357 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 65f3c566..565502b8 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -7,7 +7,7 @@ jobs: bump-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: master @@ -20,11 +20,11 @@ jobs: git config --local user.name "GitHub Action" git fetch --tags git pull origin master - latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`) + latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") echo "latest tag: $latest_tag" - new_tag=$(echo $latest_tag | awk -F. -v a="$1" -v b="$2" -v c="$3" '{printf("%d.%d.%d", $1+a, $2+b , $3+1)}') + new_tag=$(echo "$latest_tag" | awk -F. -v a="$1" -v b="$2" -v c="$3" '{printf("%d.%d.%d", $1+a, $2+b , $3+1)}') echo "new tag: $new_tag" - printf "# pylint: disable=missing-module-docstring\n__version__ = '$new_tag'\n""" > $version_file + printf "# pylint: disable=missing-module-docstring\n__version__ = \"%s\"\n""" "$new_tag" > $version_file git commit -m "Bump to ${new_tag}" $version_file || echo "No changes to commit" git push origin diff --git a/.github/workflows/nodejs-test.yml b/.github/workflows/nodejs-test.yml index d79ee5a1..0ecc5462 100644 --- a/.github/workflows/nodejs-test.yml +++ b/.github/workflows/nodejs-test.yml @@ -18,7 +18,7 @@ jobs: node-version: ['16.x'] steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ff54c6a3..65759c0b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,10 +11,10 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: '3.8' @@ -41,9 +41,9 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: '3.8' @@ -68,9 +68,9 @@ jobs: needs: publish-package runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: '3.8' - name: publish brew @@ -82,9 +82,9 @@ jobs: pip install cloudsplaining -U git fetch origin git checkout --track origin/master - latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`) + latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") echo "latest tag: $latest_tag" - git pull origin $latest_tag + git pull origin "$latest_tag" poet -f cloudsplaining > HomebrewFormula/cloudsplaining.rb git add . git commit -m "update brew formula" cloudsplaining/bin/version.py HomebrewFormula/cloudsplaining.rb || echo "No brew changes to commit" @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-latest needs: update-brew steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: master @@ -107,11 +107,11 @@ jobs: git config --local user.name "GitHub Action" git fetch --tags git pull origin master - latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`) + latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)") echo "latest tag: $latest_tag" - new_tag=$(echo $latest_tag | awk -F. -v a="$1" -v b="$2" -v c="$3" '{printf("%d.%d.%d", $1+a, $2+b , $3+1)}') + new_tag=$(echo "$latest_tag" | awk -F. -v a="$1" -v b="$2" -v c="$3" '{printf("%d.%d.%d", $1+a, $2+b , $3+1)}') echo "new tag: $new_tag" - printf "# pylint: disable=missing-module-docstring\n__version__ = '$new_tag'""" > $version_file + printf "# pylint: disable=missing-module-docstring\n__version__ = \"%s\"\n""" "$new_tag" > $version_file git commit -m "Bump to ${new_tag}" $version_file || echo "No changes to commit" git push origin diff --git a/.github/workflows/python-dependency-updater.yml b/.github/workflows/python-dependency-updater.yml index 1e3b7816..bb67e5fc 100644 --- a/.github/workflows/python-dependency-updater.yml +++ b/.github/workflows/python-dependency-updater.yml @@ -10,10 +10,10 @@ jobs: python-dependency-updater: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: '3.8' @@ -21,5 +21,5 @@ jobs: run: | pip install pyupio pip install -r requirements.txt - default_branch=`git remote show origin | grep 'HEAD branch' | cut -d' ' -f5` - pyup --provider github --provider_url https://api.github.com --repo=$GITHUB_REPOSITORY --user-token=${{ secrets.PYUP_GITHUB_ACCESS_TOKEN }} --branch $default_branch --initial + default_branch=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5) + pyup --provider github --provider_url https://api.github.com --repo="$GITHUB_REPOSITORY" --user-token=${{ secrets.PYUP_GITHUB_ACCESS_TOKEN }} --branch "$default_branch" --initial diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7881ae50..ccc5d2f5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -11,7 +11,7 @@ jobs: detect-secrets: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: detect secrets uses: edplato/trufflehog-actions-scan@c36ff9abf0af8290ef23b1b45a36e75c742dd1d8 # v0.9l-beta with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d54e7fa..f0e96042 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,14 +9,24 @@ on: pull_request: jobs: - test: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + with: + python-version: '3.8' # needed for 'pyupgrade' + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + + ci: + needs: pre-commit runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: '3.8' @@ -38,6 +48,7 @@ jobs: make type-check python-version: + needs: pre-commit if: github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 15 @@ -46,8 +57,8 @@ jobs: matrix: python: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: ${{ matrix.python }} allow-prereleases: true diff --git a/.github/workflows/update-bundle-report.yml b/.github/workflows/update-bundle-report.yml index a99e4a28..a14a3a23 100644 --- a/.github/workflows/update-bundle-report.yml +++ b/.github/workflows/update-bundle-report.yml @@ -10,10 +10,10 @@ jobs: update: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup python - uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: '3.8' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..d39edaca --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: https://github.com/rhysd/actionlint + rev: v1.7.1 + hooks: + - id: actionlint-docker + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.3 + hooks: + - id: ruff-format diff --git a/Makefile b/Makefile index 50b1c721..71296338 100644 --- a/Makefile +++ b/Makefile @@ -57,10 +57,6 @@ test: setup-dev security-test: setup-dev bandit -r ./${PROJECT_UNDERSCORE}/ -# Auto format your python files -fmt: setup-dev - black ${PROJECT_UNDERSCORE}/ - # Run Pylint to lint your code lint: setup-dev pylint ${PROJECT_UNDERSCORE}/ diff --git a/cloudsplaining/bin/cli.py b/cloudsplaining/bin/cli.py index 4846a84e..a3c482d7 100755 --- a/cloudsplaining/bin/cli.py +++ b/cloudsplaining/bin/cli.py @@ -5,8 +5,9 @@ # For full license text, see the LICENSE file in the repo root # or https://opensource.org/licenses/BSD-3-Clause """ - Cloudsplaining is an AWS IAM Assessment tool that identifies violations of least privilege and generates a risk-prioritized HTML report with a triage worksheet. +Cloudsplaining is an AWS IAM Assessment tool that identifies violations of least privilege and generates a risk-prioritized HTML report with a triage worksheet. """ + import click from cloudsplaining import command from cloudsplaining.bin.version import __version__ @@ -21,9 +22,7 @@ def cloudsplaining() -> None: cloudsplaining.add_command(command.create_exclusions_file.create_exclusions_file) -cloudsplaining.add_command( - command.create_multi_account_config_file.create_multi_account_config_file -) +cloudsplaining.add_command(command.create_multi_account_config_file.create_multi_account_config_file) cloudsplaining.add_command(command.expand_policy.expand_policy) cloudsplaining.add_command(command.scan.scan) cloudsplaining.add_command(command.scan_multi_account.scan_multi_account) diff --git a/cloudsplaining/command/create_exclusions_file.py b/cloudsplaining/command/create_exclusions_file.py index 401b4a93..20d847bb 100644 --- a/cloudsplaining/command/create_exclusions_file.py +++ b/cloudsplaining/command/create_exclusions_file.py @@ -48,6 +48,4 @@ def create_exclusions_file(output_file: str, verbosity: int) -> None: ) print("\tcloudsplaining download") print("You can use this with the scan command as shown below: ") - print( - "\tcloudsplaining scan --exclusions-file exclusions.yml --input-file default.json" - ) + print("\tcloudsplaining scan --exclusions-file exclusions.yml --input-file default.json") diff --git a/cloudsplaining/command/create_multi_account_config_file.py b/cloudsplaining/command/create_multi_account_config_file.py index ff73fa4d..be106e1d 100644 --- a/cloudsplaining/command/create_multi_account_config_file.py +++ b/cloudsplaining/command/create_multi_account_config_file.py @@ -41,17 +41,13 @@ def create_multi_account_config_file(output_file: str, verbosity: int) -> None: set_log_level(verbosity) if os.path.exists(output_file): - logger.debug( - "%s exists. Removing the file and replacing its contents.", output_file - ) + logger.debug("%s exists. Removing the file and replacing its contents.", output_file) os.remove(output_file) with open(output_file, "a") as file_obj: for line in MULTI_ACCOUNT_CONFIG_TEMPLATE: file_obj.write(line) - utils.print_green( - f"Success! Multi-account config file written to: {os.path.relpath(output_file)}" - ) + utils.print_green(f"Success! Multi-account config file written to: {os.path.relpath(output_file)}") print( f"\nMake sure you edit the {os.path.relpath(output_file)} file and then run the scan-multi-account command, as shown below." ) diff --git a/cloudsplaining/command/download.py b/cloudsplaining/command/download.py index a10fc8ad..44473200 100644 --- a/cloudsplaining/command/download.py +++ b/cloudsplaining/command/download.py @@ -1,5 +1,5 @@ """Runs aws iam get-authorization-details on all accounts specified in the aws credentials file, and stores them in -account-alias.json """ +account-alias.json""" # Copyright (c) 2020, salesforce.com, inc. # All rights reserved. @@ -51,9 +51,7 @@ help="When downloading AWS managed policy documents, also include the non-default policy versions. Note that this will dramatically increase the size of the downloaded file.", ) @click.option("-v", "--verbose", "verbosity", help="Log verbosity level.", count=True) -def download( - profile: str, output: str, include_non_default_policy_versions: bool, verbosity: int -) -> int: +def download(profile: str, output: str, include_non_default_policy_versions: bool, verbosity: int) -> int: """ Runs aws iam get-authorization-details on all accounts specified in the aws credentials file, and stores them in account-alias.json @@ -69,9 +67,7 @@ def download( else: output_filename = os.path.join(output, "default.json") - results = get_account_authorization_details( - session_data, include_non_default_policy_versions - ) + results = get_account_authorization_details(session_data, include_non_default_policy_versions) with open(output_filename, "w") as f: json.dump(results, f, indent=4, default=str) # output_filename.write_text(json.dumps(results, indent=4, default=str)) @@ -119,9 +115,7 @@ def get_account_authorization_details( else: policy_version_list = [] for policy_version in policy.get("PolicyVersionList") or []: - if policy_version.get("VersionId") == policy.get( - "DefaultVersionId" - ): + if policy_version.get("VersionId") == policy.get("DefaultVersionId"): policy_version_list.append(policy_version) break entry = { @@ -131,9 +125,7 @@ def get_account_authorization_details( "Path": policy.get("Path"), "DefaultVersionId": policy.get("DefaultVersionId"), "AttachmentCount": policy.get("AttachmentCount"), - "PermissionsBoundaryUsageCount": policy.get( - "PermissionsBoundaryUsageCount" - ), + "PermissionsBoundaryUsageCount": policy.get("PermissionsBoundaryUsageCount"), "IsAttachable": policy.get("IsAttachable"), "CreateDate": policy.get("CreateDate"), "UpdateDate": policy.get("UpdateDate"), diff --git a/cloudsplaining/command/expand_policy.py b/cloudsplaining/command/expand_policy.py index 02773237..8513da90 100644 --- a/cloudsplaining/command/expand_policy.py +++ b/cloudsplaining/command/expand_policy.py @@ -16,9 +16,7 @@ logger = logging.getLogger(__name__) -@click.command( - short_help="Expand the * Actions in IAM policy files to improve readability" -) +@click.command(short_help="Expand the * Actions in IAM policy files to improve readability") @click.option( "-i", "--input-file", diff --git a/cloudsplaining/command/scan_multi_account.py b/cloudsplaining/command/scan_multi_account.py index 06b1bd27..94b22432 100644 --- a/cloudsplaining/command/scan_multi_account.py +++ b/cloudsplaining/command/scan_multi_account.py @@ -40,9 +40,7 @@ def __init__(self, config: Dict[str, Any], role_name: str) -> None: def _accounts(self) -> Dict[str, str]: accounts: Dict[str, str] = self.config.get("accounts", None) if not accounts: - raise Exception( - "Please supply a list of accounts in the multi-account config file" - ) + raise Exception("Please supply a list of accounts in the multi-account config file") return accounts @@ -119,9 +117,7 @@ def _accounts(self) -> Dict[str, str]: "severity", help="Filter the severity of findings to be reported.", multiple=True, - type=click.Choice( - ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"], case_sensitive=False - ), + type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"], case_sensitive=False), ) def scan_multi_account( config_file: str, @@ -181,9 +177,7 @@ def scan_accounts( """Use this method as a library to scan multiple accounts""" # TODO: Speed improvements? Multithreading? This currently runs sequentially. for target_account_name, target_account_id in multi_account_config.accounts.items(): - print( - f"{OK_GREEN}Scanning account: {target_account_name} (ID: {target_account_id}){END}" - ) + print(f"{OK_GREEN}Scanning account: {target_account_name} (ID: {target_account_id}){END}") results = scan_account( target_account_id=target_account_id, target_role_name=role_name, @@ -202,9 +196,7 @@ def scan_accounts( ) rendered_report = html_report.get_html_report() if not output_directory and not output_bucket: - raise Exception( - "Please supply --output-bucket and/or --output-directory as arguments." - ) + raise Exception("Please supply --output-bucket and/or --output-directory as arguments.") if output_bucket: s3 = cast( "S3ServiceResource", @@ -212,42 +204,24 @@ def scan_accounts( ) # Write the HTML file output_file = f"{target_account_name}.html" - s3.Object(output_bucket, output_file).put( - ACL="bucket-owner-full-control", Body=rendered_report - ) - utils.print_green( - f"Saved the HTML report to: s3://{output_bucket}/{output_file}" - ) + s3.Object(output_bucket, output_file).put(ACL="bucket-owner-full-control", Body=rendered_report) + utils.print_green(f"Saved the HTML report to: s3://{output_bucket}/{output_file}") # Write the JSON data file if write_data_file: output_file = f"{target_account_name}.json" body = json.dumps(results, sort_keys=True, default=str, indent=4) - s3.Object(output_bucket, output_file).put( - ACL="bucket-owner-full-control", Body=body - ) - utils.print_green( - f"Saved the JSON data to: s3://{output_bucket}/{output_file}" - ) + s3.Object(output_bucket, output_file).put(ACL="bucket-owner-full-control", Body=body) + utils.print_green(f"Saved the JSON data to: s3://{output_bucket}/{output_file}") if output_directory: # Write the HTML file - html_output_file = os.path.join( - output_directory, f"{target_account_name}.html" - ) + html_output_file = os.path.join(output_directory, f"{target_account_name}.html") utils.write_file(html_output_file, rendered_report) - utils.print_green( - f"Saved the HTML report to: {os.path.relpath(html_output_file)}" - ) + utils.print_green(f"Saved the HTML report to: {os.path.relpath(html_output_file)}") # Write the JSON data file if write_data_file: - results_data_file = os.path.join( - output_directory, f"{target_account_name}.json" - ) - results_data_filepath = utils.write_results_data_file( - results, results_data_file - ) - utils.print_green( - f"Saved the JSON data to: {os.path.relpath(results_data_filepath)}" - ) + results_data_file = os.path.join(output_directory, f"{target_account_name}.json") + results_data_filepath = utils.write_results_data_file(results, results_data_file) + utils.print_green(f"Saved the JSON data to: {os.path.relpath(results_data_filepath)}") def scan_account( @@ -297,9 +271,7 @@ def download_account_authorization_details( "aws_session_token": aws_session_token, } include_non_default_policy_versions = False - authorization_details = get_account_authorization_details( - session_data, include_non_default_policy_versions - ) + authorization_details = get_account_authorization_details(session_data, include_non_default_policy_versions) return authorization_details diff --git a/cloudsplaining/command/scan_policy_file.py b/cloudsplaining/command/scan_policy_file.py index 87c2a100..e4416f35 100644 --- a/cloudsplaining/command/scan_policy_file.py +++ b/cloudsplaining/command/scan_policy_file.py @@ -28,12 +28,8 @@ END = "\033[0m" -@click.command( - short_help="Scan a single policy file to identify identify missing resource constraints." -) -@click.option( - "-i", "--input-file", type=str, help="Path of the IAM policy file to evaluate." -) +@click.command(short_help="Scan a single policy file to identify identify missing resource constraints.") +@click.option("-i", "--input-file", type=str, help="Path of the IAM policy file to evaluate.") @click.option( "-e", "--exclusions-file", @@ -64,9 +60,7 @@ "severity", help="Filter the severity of findings to be reported.", multiple=True, - type=click.Choice( - ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"], case_sensitive=False - ), + type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"], case_sensitive=False), ) # pylint: disable=redefined-builtin @@ -122,9 +116,7 @@ def scan_policy_file( # Privilege Escalation if results["PrivilegeEscalation"]: if results["PrivilegeEscalation"]["findings"]: - print( - f"{RED}Potential Issue found: Policy is capable of Privilege Escalation{END}" - ) + print(f"{RED}Potential Issue found: Policy is capable of Privilege Escalation{END}") results_exist += 1 for item in results["PrivilegeEscalation"]["findings"]: print(f"- Method: {item.get('type')}") @@ -134,45 +126,29 @@ def scan_policy_file( if results["DataExfiltration"]: if results["DataExfiltration"]["findings"]: results_exist += 1 - print( - f"{RED}Potential Issue found: Policy is capable of Data Exfiltration{END}" - ) - print( - f"{BOLD}Actions{END}: {', '.join(results['DataExfiltration']['findings'])}\n" - ) + print(f"{RED}Potential Issue found: Policy is capable of Data Exfiltration{END}") + print(f"{BOLD}Actions{END}: {', '.join(results['DataExfiltration']['findings'])}\n") # Resource Exposure if results["ResourceExposure"]: if results["ResourceExposure"]["findings"]: results_exist += 1 - print( - f"{RED}Potential Issue found: Policy is capable of Resource Exposure{END}" - ) - print( - f"{BOLD}Actions{END}: {', '.join(results['ResourceExposure']['findings'])}\n" - ) + print(f"{RED}Potential Issue found: Policy is capable of Resource Exposure{END}") + print(f"{BOLD}Actions{END}: {', '.join(results['ResourceExposure']['findings'])}\n") # Service Wildcard if results["ServiceWildcard"]: if results["ServiceWildcard"]["findings"]: results_exist += 1 - print( - f"{RED}Potential Issue found: Policy allows ALL Actions from a service (like service:*){END}" - ) - print( - f"{BOLD}Actions{END}: {', '.join(results['ServiceWildcard']['findings'])}\n" - ) + print(f"{RED}Potential Issue found: Policy allows ALL Actions from a service (like service:*){END}") + print(f"{BOLD}Actions{END}: {', '.join(results['ServiceWildcard']['findings'])}\n") # Credentials Exposure if results["CredentialsExposure"]: if results["CredentialsExposure"]["findings"]: results_exist += 1 - print( - f"{RED}Potential Issue found: Policy allows actions that return credentials{END}" - ) - print( - f"{BOLD}Actions{END}: {', '.join(results['CredentialsExposure']['findings'])}\n" - ) + print(f"{RED}Potential Issue found: Policy allows actions that return credentials{END}") + print(f"{BOLD}Actions{END}: {', '.join(results['CredentialsExposure']['findings'])}\n") if not high_priority_only: if results["InfrastructureModification"]: @@ -182,9 +158,7 @@ def scan_policy_file( print( f"{RED}Potential Issue found: Policy is capable of Unrestricted Infrastructure Modification{END}" ) - print( - f"{BOLD}Actions{END}: {', '.join(results['InfrastructureModification']['findings'])}" - ) + print(f"{BOLD}Actions{END}: {', '.join(results['InfrastructureModification']['findings'])}") if results_exist == 0: print("There were no results found.") diff --git a/cloudsplaining/output/policy_finding.py b/cloudsplaining/output/policy_finding.py index ec85b211..cf2d721b 100644 --- a/cloudsplaining/output/policy_finding.py +++ b/cloudsplaining/output/policy_finding.py @@ -45,9 +45,7 @@ def __init__( self.exclusions = exclusions self.always_exclude_actions = exclusions.exclude_actions - self.missing_resource_constraints_for_modify_actions = ( - self._missing_resource_constraints_for_modify_actions() - ) + self.missing_resource_constraints_for_modify_actions = self._missing_resource_constraints_for_modify_actions() self.severity = [] if severity is None else severity def _missing_resource_constraints_for_modify_actions(self) -> List[str]: @@ -57,9 +55,7 @@ def _missing_resource_constraints_for_modify_actions(self) -> List[str]: logger.debug("Evaluating statement: %s", statement.json) if statement.effect == "Allow" and not statement.has_condition: actions_missing_resource_constraints.update( - statement.missing_resource_constraints_for_modify_actions( - self.exclusions - ) + statement.missing_resource_constraints_for_modify_actions(self.exclusions) ) return sorted(actions_missing_resource_constraints) @@ -140,9 +136,7 @@ def results(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ServiceWildcard"], "findings": ( self.service_wildcard - if ISSUE_SEVERITY["ServiceWildcard"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ServiceWildcard"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -152,9 +146,7 @@ def results(self) -> Dict[str, Any]: "description": RISK_DEFINITION["PrivilegeEscalation"], "findings": ( self.privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -163,9 +155,7 @@ def results(self) -> Dict[str, Any]: "description": RISK_DEFINITION["DataExfiltration"], "findings": ( self.data_exfiltration - if ISSUE_SEVERITY["DataExfiltration"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["DataExfiltration"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -174,9 +164,7 @@ def results(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ResourceExposure"], "findings": ( self.resource_exposure - if ISSUE_SEVERITY["ResourceExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ResourceExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -185,9 +173,7 @@ def results(self) -> Dict[str, Any]: "description": RISK_DEFINITION["CredentialsExposure"], "findings": ( self.credentials_exposure - if ISSUE_SEVERITY["CredentialsExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["CredentialsExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -196,8 +182,7 @@ def results(self) -> Dict[str, Any]: "description": RISK_DEFINITION["InfrastructureModification"], "findings": ( self.missing_resource_constraints_for_modify_actions - if ISSUE_SEVERITY["InfrastructureModification"] - in [x.lower() for x in self.severity] + if ISSUE_SEVERITY["InfrastructureModification"] in [x.lower() for x in self.severity] or not self.severity else [] ), diff --git a/cloudsplaining/scan/assume_role_policy_document.py b/cloudsplaining/scan/assume_role_policy_document.py index 43fda890..7c892531 100644 --- a/cloudsplaining/scan/assume_role_policy_document.py +++ b/cloudsplaining/scan/assume_role_policy_document.py @@ -43,9 +43,7 @@ def role_assumable_by_compute_services(self) -> List[str]: assumable_by_compute_services = [] for statement in self.statements: if statement.role_assumable_by_compute_services: - assumable_by_compute_services.extend( - statement.role_assumable_by_compute_services - ) + assumable_by_compute_services.extend(statement.role_assumable_by_compute_services) return assumable_by_compute_services diff --git a/cloudsplaining/scan/authorization_details.py b/cloudsplaining/scan/authorization_details.py index 5defc92f..707008d5 100644 --- a/cloudsplaining/scan/authorization_details.py +++ b/cloudsplaining/scan/authorization_details.py @@ -48,9 +48,7 @@ def __init__( self.auth_json = auth_json if not isinstance(exclusions, Exclusions): - raise Exception( - "For exclusions, please provide an object of the Exclusions type" - ) + raise Exception("For exclusions, please provide an object of the Exclusions type") self.exclusions = exclusions self.flag_conditional_statements = flag_conditional_statements self.flag_resource_arn_statements = flag_resource_arn_statements @@ -121,18 +119,10 @@ def links(self) -> Dict[str, str | None]: # Let's create a set of unique_action_names that are in InfrastructureModification # First, let's get them from ManagedPolicyDetails # Then, the inline policies from GroupDetails, RoleDetails, and UserDetails - unique_action_names.update( - self.group_detail_list.all_infrastructure_modification_actions_by_inline_policies - ) - unique_action_names.update( - self.role_detail_list.all_infrastructure_modification_actions_by_inline_policies - ) - unique_action_names.update( - self.user_detail_list.all_infrastructure_modification_actions_by_inline_policies - ) - unique_action_names.update( - self.policies.all_infrastructure_modification_actions - ) + unique_action_names.update(self.group_detail_list.all_infrastructure_modification_actions_by_inline_policies) + unique_action_names.update(self.role_detail_list.all_infrastructure_modification_actions_by_inline_policies) + unique_action_names.update(self.user_detail_list.all_infrastructure_modification_actions_by_inline_policies) + unique_action_names.update(self.policies.all_infrastructure_modification_actions) all_action_links = get_all_action_links() diff --git a/cloudsplaining/scan/group_details.py b/cloudsplaining/scan/group_details.py index 3ee0e59f..0796ad08 100644 --- a/cloudsplaining/scan/group_details.py +++ b/cloudsplaining/scan/group_details.py @@ -78,9 +78,7 @@ def get_all_allowed_actions_for_group(self, name: str) -> Optional[List[str]]: return group_detail.all_allowed_actions return None - def get_all_iam_statements_for_group( - self, name: str - ) -> Optional[List[StatementDetail]]: + def get_all_iam_statements_for_group(self, name: str) -> Optional[List[StatementDetail]]: """Returns a list of all StatementDetail objects across all the policies assigned to the group""" for group_detail in self.groups: if group_detail.group_name == name: @@ -163,10 +161,7 @@ def __init__( policy_name = policy_detail.get("PolicyName") policy_document = policy_detail.get("PolicyDocument") policy_id = get_non_provider_id(json.dumps(policy_document)) - if not ( - exclusions.is_policy_excluded(policy_name) - or exclusions.is_policy_excluded(policy_id) - ): + if not (exclusions.is_policy_excluded(policy_name) or exclusions.is_policy_excluded(policy_id)): # NOTE: The Exclusions were not here before the #254 fix (which was an unfiled bug I just discovered) so the presence of this might break some older unit tests. Might need to fix that. inline_policy = InlinePolicy( policy_detail, @@ -189,12 +184,8 @@ def __init__( or exclusions.is_policy_excluded(get_policy_name(arn)) ): try: - attached_managed_policy_details = ( - policy_details.get_policy_detail(arn) - ) - self.attached_managed_policies.append( - attached_managed_policy_details - ) + attached_managed_policy_details = policy_details.get_policy_detail(arn) + self.attached_managed_policies.append(attached_managed_policy_details) except NotFoundException as e: utils.print_red(f"\tError in group {self.group_name}: {e}") @@ -240,19 +231,13 @@ def all_iam_statements(self) -> List[StatementDetail]: @property def attached_managed_policies_json(self) -> Dict[str, Dict[str, Any]]: """Return JSON representation of attached managed policies""" - policies = { - policy.policy_id: policy.json_large - for policy in self.attached_managed_policies - } + policies = {policy.policy_id: policy.json_large for policy in self.attached_managed_policies} return policies @property def attached_managed_policies_pointer_json(self) -> Dict[str, str]: """Return metadata on attached managed policies so you can look it up in the policies section later.""" - policies = { - policy.policy_id: policy.policy_name - for policy in self.attached_managed_policies - } + policies = {policy.policy_id: policy.policy_name for policy in self.attached_managed_policies} return policies @property @@ -286,17 +271,13 @@ def all_infrastructure_modification_actions_by_inline_policies(self) -> List[str @property def inline_policies_json(self) -> Dict[str, Dict[str, Any]]: """Return JSON representation of attached inline policies""" - policies = { - policy.policy_id: policy.json_large for policy in self.inline_policies - } + policies = {policy.policy_id: policy.json_large for policy in self.inline_policies} return policies @property def inline_policies_pointer_json(self) -> Dict[str, str]: """Return metadata on attached inline policies so you can look it up in the policies section later.""" - policies = { - policy.policy_id: policy.policy_name for policy in self.inline_policies - } + policies = {policy.policy_id: policy.policy_name for policy in self.inline_policies} return policies @property diff --git a/cloudsplaining/scan/inline_policy.py b/cloudsplaining/scan/inline_policy.py index 448ceb3f..857fe312 100644 --- a/cloudsplaining/scan/inline_policy.py +++ b/cloudsplaining/scan/inline_policy.py @@ -64,10 +64,7 @@ def set_iam_data(self, iam_data: Dict[str, Dict[Any, Any]]) -> None: def _is_excluded(self, exclusions: Exclusions) -> bool: """Determine whether the policy name or policy ID is excluded""" - return bool( - exclusions.is_policy_excluded(self.policy_name) - or exclusions.is_policy_excluded(self.policy_id) - ) + return bool(exclusions.is_policy_excluded(self.policy_name) or exclusions.is_policy_excluded(self.policy_id)) def getFindingLinks(self, findings: List[Dict[str, Any]]) -> Dict[str, str]: links = {} @@ -86,13 +83,9 @@ def getAttached(self) -> Dict[str, List[Any]]: inlinePolicies = {} if self.is_excluded: return {} - inlinePolicies.update( - self.iam_data[principalType][principalID]["inline_policies"] - ) + inlinePolicies.update(self.iam_data[principalType][principalID]["inline_policies"]) if self.policy_id in inlinePolicies: - attached[principalType].append( - self.iam_data[principalType][principalID]["name"] - ) + attached[principalType].append(self.iam_data[principalType][principalID]["name"]) return attached @property @@ -108,16 +101,12 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["PrivilegeEscalation"], "findings": ( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), "links": self.getFindingLinks( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -126,9 +115,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["DataExfiltration"], "findings": ( self.policy_document.allows_data_exfiltration_actions - if ISSUE_SEVERITY["DataExfiltration"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["DataExfiltration"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -137,9 +124,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ResourceExposure"], "findings": ( self.policy_document.permissions_management_without_constraints - if ISSUE_SEVERITY["ResourceExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ResourceExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -148,9 +133,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ServiceWildcard"], "findings": ( self.policy_document.service_wildcard - if ISSUE_SEVERITY["ServiceWildcard"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ServiceWildcard"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -159,9 +142,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["CredentialsExposure"], "findings": ( self.policy_document.credentials_exposure - if ISSUE_SEVERITY["CredentialsExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["CredentialsExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -182,16 +163,12 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["PrivilegeEscalation"], "findings": ( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), "links": self.getFindingLinks( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -200,9 +177,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["DataExfiltration"], "findings": ( self.policy_document.allows_data_exfiltration_actions - if ISSUE_SEVERITY["DataExfiltration"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["DataExfiltration"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -211,9 +186,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ResourceExposure"], "findings": ( self.policy_document.permissions_management_without_constraints - if ISSUE_SEVERITY["ResourceExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ResourceExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -222,9 +195,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ServiceWildcard"], "findings": ( self.policy_document.service_wildcard - if ISSUE_SEVERITY["ServiceWildcard"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ServiceWildcard"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -233,9 +204,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["CredentialsExposure"], "findings": ( self.policy_document.credentials_exposure - if ISSUE_SEVERITY["CredentialsExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["CredentialsExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -244,8 +213,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["InfrastructureModification"], "findings": ( self.policy_document.infrastructure_modification - if ISSUE_SEVERITY["InfrastructureModification"] - in [x.lower() for x in self.severity] + if ISSUE_SEVERITY["InfrastructureModification"] in [x.lower() for x in self.severity] or not self.severity else [] ), diff --git a/cloudsplaining/scan/managed_policy_detail.py b/cloudsplaining/scan/managed_policy_detail.py index 7f7a6e6c..0a67a78f 100644 --- a/cloudsplaining/scan/managed_policy_detail.py +++ b/cloudsplaining/scan/managed_policy_detail.py @@ -58,9 +58,9 @@ def __init__( this_policy_id = policy_detail["PolicyId"] this_policy_path = policy_detail["Path"] # Always exclude the AWS service role policies - if is_name_excluded( - this_policy_path, "aws-service-role*" - ) or is_name_excluded(this_policy_path, "/aws-service-role*"): + if is_name_excluded(this_policy_path, "aws-service-role*") or is_name_excluded( + this_policy_path, "/aws-service-role*" + ): logger.debug( "The %s Policy with the policy ID %s is excluded because it is " "an immutable AWS Service role with a path of %s", @@ -122,20 +122,14 @@ def json_large(self) -> Dict[str, Dict[str, Any]]: @property def json_large_aws_managed(self) -> Dict[str, Dict[str, Any]]: """Get all JSON results""" - result = { - policy.policy_id: policy.json_large - for policy in self.policy_details - if policy.managed_by == "AWS" - } + result = {policy.policy_id: policy.json_large for policy in self.policy_details if policy.managed_by == "AWS"} return result @property def json_large_customer_managed(self) -> Dict[str, Dict[str, Any]]: """Get all JSON results""" result = { - policy.policy_id: policy.json_large - for policy in self.policy_details - if policy.managed_by == "Customer" + policy.policy_id: policy.json_large for policy in self.policy_details if policy.managed_by == "Customer" } return result @@ -174,9 +168,7 @@ def __init__( self.path = policy_detail["Path"] self.default_version_id = policy_detail.get("DefaultVersionId") self.attachment_count = policy_detail.get("AttachmentCount") - self.permissions_boundary_usage_count = policy_detail.get( - "PermissionsBoundaryUsageCount" - ) + self.permissions_boundary_usage_count = policy_detail.get("PermissionsBoundaryUsageCount") self.is_attachable = policy_detail.get("IsAttachable") self.create_date = policy_detail.get("CreateDate") self.update_date = policy_detail.get("UpdateDate") @@ -224,9 +216,7 @@ def _policy_document(self) -> PolicyDocument: flag_resource_arn_statements=self.flag_resource_arn_statements, flag_conditional_statements=self.flag_conditional_statements, ) - raise Exception( - "Managed Policy ARN %s has no default Policy Document version", self.arn - ) + raise Exception("Managed Policy ARN %s has no default Policy Document version", self.arn) # This will help with the Exclusions mechanism. Get the full path of the policy, including the name. @property @@ -268,21 +258,11 @@ def getAttached(self) -> Dict[str, Any]: if self.is_excluded: return {} if self.managed_by == "AWS": - managedPolicies.update( - self.iam_data[principalType][principalID][ - "aws_managed_policies" - ] - ) + managedPolicies.update(self.iam_data[principalType][principalID]["aws_managed_policies"]) elif self.managed_by == "Customer": - managedPolicies.update( - self.iam_data[principalType][principalID][ - "customer_managed_policies" - ] - ) + managedPolicies.update(self.iam_data[principalType][principalID]["customer_managed_policies"]) if self.policy_id in managedPolicies: - attached[principalType].append( - self.iam_data[principalType][principalID]["name"] - ) + attached[principalType].append(self.iam_data[principalType][principalID]["name"]) return attached @property @@ -305,16 +285,12 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["PrivilegeEscalation"], "findings": ( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), "links": self.getFindingLinks( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -323,9 +299,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["DataExfiltration"], "findings": ( self.policy_document.allows_data_exfiltration_actions - if ISSUE_SEVERITY["DataExfiltration"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["DataExfiltration"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -334,9 +308,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ResourceExposure"], "findings": ( self.policy_document.permissions_management_without_constraints - if ISSUE_SEVERITY["ResourceExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ResourceExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -345,9 +317,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ServiceWildcard"], "findings": ( self.policy_document.service_wildcard - if ISSUE_SEVERITY["ServiceWildcard"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ServiceWildcard"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -356,9 +326,7 @@ def json(self) -> Dict[str, Any]: "description": RISK_DEFINITION["CredentialsExposure"], "findings": ( self.policy_document.credentials_exposure - if ISSUE_SEVERITY["CredentialsExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["CredentialsExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -386,16 +354,12 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["PrivilegeEscalation"], "findings": ( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), "links": self.getFindingLinks( self.policy_document.allows_privilege_escalation - if ISSUE_SEVERITY["PrivilegeEscalation"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["PrivilegeEscalation"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -404,9 +368,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["DataExfiltration"], "findings": ( self.policy_document.allows_data_exfiltration_actions - if ISSUE_SEVERITY["DataExfiltration"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["DataExfiltration"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -415,9 +377,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ResourceExposure"], "findings": ( self.policy_document.permissions_management_without_constraints - if ISSUE_SEVERITY["ResourceExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ResourceExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -426,9 +386,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["ServiceWildcard"], "findings": ( self.policy_document.service_wildcard - if ISSUE_SEVERITY["ServiceWildcard"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["ServiceWildcard"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -437,9 +395,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["CredentialsExposure"], "findings": ( self.policy_document.credentials_exposure - if ISSUE_SEVERITY["CredentialsExposure"] - in [x.lower() for x in self.severity] - or not self.severity + if ISSUE_SEVERITY["CredentialsExposure"] in [x.lower() for x in self.severity] or not self.severity else [] ), }, @@ -448,8 +404,7 @@ def json_large(self) -> Dict[str, Any]: "description": RISK_DEFINITION["InfrastructureModification"], "findings": ( self.policy_document.infrastructure_modification - if ISSUE_SEVERITY["InfrastructureModification"] - in [x.lower() for x in self.severity] + if ISSUE_SEVERITY["InfrastructureModification"] in [x.lower() for x in self.severity] or not self.severity else [] ), diff --git a/cloudsplaining/scan/policy_document.py b/cloudsplaining/scan/policy_document.py index 5ef362d6..bc8d3685 100644 --- a/cloudsplaining/scan/policy_document.py +++ b/cloudsplaining/scan/policy_document.py @@ -85,9 +85,7 @@ def filter_deny_statements(self, allowed_actions: Set[str]) -> Set[str]: for statement in self.statements: if statement.effect_deny: if statement.expanded_actions: - allowed_actions = allowed_actions.difference( - statement.expanded_actions - ) + allowed_actions = allowed_actions.difference(statement.expanded_actions) return allowed_actions @property @@ -113,18 +111,10 @@ def all_allowed_unrestrictable_actions(self) -> List[str]: """Output all IAM actions that cannot be restricted by resource constraints""" allowed_actions = set() for statement in self.statements: - if ( - statement.effect_allow - and not statement.has_condition - and statement.unrestrictable_actions - ): + if statement.effect_allow and not statement.has_condition and statement.unrestrictable_actions: allowed_actions.update(statement.unrestrictable_actions) # Fix Issue #254 - Allow flagging risky actions even when there are resource constraints - if ( - statement.effect_allow - and not statement.has_condition - and self.flag_resource_arn_statements - ): + if statement.effect_allow and not statement.has_condition and self.flag_resource_arn_statements: allowed_actions.update(statement.unrestrictable_actions) allowed_actions = self.filter_deny_statements(allowed_actions) return list(allowed_actions) @@ -135,9 +125,7 @@ def infrastructure_modification(self) -> List[str]: actions_missing_resource_constraints = [] for statement in self.statements: if statement.effect == "Allow" and not statement.has_condition: - for action in statement.missing_resource_constraints_for_modify_actions( - self.exclusions - ): + for action in statement.missing_resource_constraints_for_modify_actions(self.exclusions): if action.lower() not in self.exclusions.exclude_actions: actions_missing_resource_constraints.append(action) actions_missing_resource_constraints.sort() @@ -165,12 +153,8 @@ def allows_privilege_escalation(self) -> List[Dict[str, Any]]: """ # if severity escalations = [] - all_allowed_unrestricted_actions_lowercase = set( - x.lower() for x in self.all_allowed_unrestricted_actions - ) - all_allowed_unrestricted_actions_lowercase.update( - [x.lower() for x in self.all_allowed_unrestrictable_actions] - ) + all_allowed_unrestricted_actions_lowercase = set(x.lower() for x in self.all_allowed_unrestricted_actions) + all_allowed_unrestricted_actions_lowercase.update([x.lower() for x in self.all_allowed_unrestrictable_actions]) for escalation_type, actions in PRIVILEGE_ESCALATION_METHODS.items(): if set(actions).issubset(all_allowed_unrestricted_actions_lowercase): # if set(PRIVILEGE_ESCALATION_METHODS[key]).issubset(all_allowed_actions_lowercase): @@ -184,13 +168,8 @@ def permissions_management_without_constraints(self) -> List[str]: do not have resource constraints""" result = [] for statement in self.statements: - if ( - statement.effect == "Allow" - and statement.permissions_management_actions_without_constraints - ): - result.extend( - statement.permissions_management_actions_without_constraints - ) + if statement.effect == "Allow" and statement.permissions_management_actions_without_constraints: + result.extend(statement.permissions_management_actions_without_constraints) return result @property @@ -199,10 +178,7 @@ def write_actions_without_constraints(self) -> List[str]: do not have resource constraints""" result = [] for statement in self.statements: - if ( - statement.effect == "Allow" - and statement.write_actions_without_constraints - ): + if statement.effect == "Allow" and statement.write_actions_without_constraints: result.extend(statement.write_actions_without_constraints) return result @@ -212,28 +188,19 @@ def tagging_actions_without_constraints(self) -> List[str]: do not have resource constraints""" result = [] for statement in self.statements: - if ( - statement.effect == "Allow" - and statement.tagging_actions_without_constraints - ): + if statement.effect == "Allow" and statement.tagging_actions_without_constraints: result.extend(statement.tagging_actions_without_constraints) return result - def allows_specific_actions_without_constraints( - self, specific_actions: List[str] - ) -> List[str]: + def allows_specific_actions_without_constraints(self, specific_actions: List[str]) -> List[str]: """Determine whether or not a list of specific IAM Actions are allowed without resource constraints.""" allowed: Set[str] = set() if not isinstance(specific_actions, list): raise Exception("Please supply a list of actions.") # grab the lowercase representation for unrestricted and unrestrictable actions - unrestricted_actions_lower = { - a.lower(): a for a in self.all_allowed_unrestricted_actions - } - unrestrictable_actions_lower = { - a.lower(): a for a in self.all_allowed_unrestrictable_actions - } + unrestricted_actions_lower = {a.lower(): a for a in self.all_allowed_unrestricted_actions} + unrestrictable_actions_lower = {a.lower(): a for a in self.all_allowed_unrestrictable_actions} # Doing this for loop so we can get results that use the official CamelCase actions, and # the results don't fail if given lowercase input. @@ -252,9 +219,7 @@ def allows_specific_actions_without_constraints( def allows_data_exfiltration_actions(self) -> List[str]: """If any 'Data exfiltration' actions are allowed without resource constraints, return those actions.""" results = [] - for action in self.allows_specific_actions_without_constraints( - READ_ONLY_DATA_EXFILTRATION_ACTIONS - ): + for action in self.allows_specific_actions_without_constraints(READ_ONLY_DATA_EXFILTRATION_ACTIONS): if action.lower() not in self.exclusions.exclude_actions: results.append(action) return results @@ -264,9 +229,7 @@ def credentials_exposure(self) -> List[str]: """Determine if the action returns credentials""" # https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a results = [] - for action in self.allows_specific_actions_without_constraints( - ACTIONS_THAT_RETURN_CREDENTIALS - ): + for action in self.allows_specific_actions_without_constraints(ACTIONS_THAT_RETURN_CREDENTIALS): if action.lower() not in self.exclusions.exclude_actions: results.append(action) return results diff --git a/cloudsplaining/scan/resource_policy_document.py b/cloudsplaining/scan/resource_policy_document.py index 620cd4fb..4d634e50 100644 --- a/cloudsplaining/scan/resource_policy_document.py +++ b/cloudsplaining/scan/resource_policy_document.py @@ -166,13 +166,9 @@ def _conditions(self) -> list[tuple[str, Any]]: key_lower = key.lower() if key_lower in CONDITION_KEY_CATEGORIES: if isinstance(value, list): - conditions.extend( - (CONDITION_KEY_CATEGORIES[key_lower], v) for v in value - ) + conditions.extend((CONDITION_KEY_CATEGORIES[key_lower], v) for v in value) else: - conditions.append( - (CONDITION_KEY_CATEGORIES[key_lower], value) - ) + conditions.append((CONDITION_KEY_CATEGORIES[key_lower], value)) return conditions diff --git a/cloudsplaining/scan/role_details.py b/cloudsplaining/scan/role_details.py index a8d05059..5fe1b503 100644 --- a/cloudsplaining/scan/role_details.py +++ b/cloudsplaining/scan/role_details.py @@ -43,9 +43,7 @@ def __init__( self.roles = [] if not isinstance(exclusions, Exclusions): - raise Exception( - "For exclusions, please provide an object of the Exclusions type" - ) + raise Exception("For exclusions, please provide an object of the Exclusions type") self.exclusions = exclusions # Fix Issue #254 - Allow flagging risky actions even when there are resource constraints self.flag_conditional_statements = flag_conditional_statements @@ -89,9 +87,7 @@ def get_all_allowed_actions_for_role(self, name: str) -> Optional[List[str]]: return role_detail.all_allowed_actions return None - def get_all_iam_statements_for_role( - self, name: str - ) -> Optional[List[StatementDetail]]: + def get_all_iam_statements_for_role(self, name: str) -> Optional[List[StatementDetail]]: """Returns a list of all StatementDetail objects across all the policies assigned to the role""" for role_detail in self.roles: if role_detail.role_name == name: @@ -178,9 +174,7 @@ def __init__( self.assume_role_policy_document = None assume_role_policy = role_detail.get("AssumeRolePolicyDocument") if assume_role_policy: - self.assume_role_policy_document = AssumeRolePolicyDocument( - assume_role_policy - ) + self.assume_role_policy_document = AssumeRolePolicyDocument(assume_role_policy) # TODO: Create a class for InstanceProfileList self.instance_profile_list = role_detail.get("InstanceProfileList", []) @@ -193,10 +187,7 @@ def __init__( policy_name = policy_detail.get("PolicyName") policy_document = policy_detail.get("PolicyDocument") policy_id = get_non_provider_id(json.dumps(policy_document)) - if not ( - exclusions.is_policy_excluded(policy_name) - or exclusions.is_policy_excluded(policy_id) - ): + if not (exclusions.is_policy_excluded(policy_name) or exclusions.is_policy_excluded(policy_id)): inline_policy = InlinePolicy( policy_detail, exclusions=exclusions, @@ -218,12 +209,8 @@ def __init__( or exclusions.is_policy_excluded(get_policy_name(arn)) ): try: - attached_managed_policy_details = ( - policy_details.get_policy_detail(arn) - ) - self.attached_managed_policies.append( - attached_managed_policy_details - ) + attached_managed_policy_details = policy_details.get_policy_detail(arn) + self.attached_managed_policies.append(attached_managed_policy_details) except NotFoundException as e: utils.print_red(f"\tError in role {self.role_name}: {e}") @@ -314,17 +301,13 @@ def all_infrastructure_modification_actions_by_inline_policies(self) -> List[str @property def inline_policies_json(self) -> Dict[str, Dict[str, Any]]: """Return JSON representation of attached inline policies""" - policies = { - policy.policy_id: policy.json_large for policy in self.inline_policies - } + policies = {policy.policy_id: policy.json_large for policy in self.inline_policies} return policies @property def inline_policies_pointer_json(self) -> Dict[str, str]: """Return metadata on attached inline policies so you can look it up in the policies section later.""" - policies = { - policy.policy_id: policy.policy_name for policy in self.inline_policies - } + policies = {policy.policy_id: policy.policy_name for policy in self.inline_policies} return policies @property diff --git a/cloudsplaining/scan/statement_detail.py b/cloudsplaining/scan/statement_detail.py index 0fcbd1b0..02ec3d96 100644 --- a/cloudsplaining/scan/statement_detail.py +++ b/cloudsplaining/scan/statement_detail.py @@ -58,9 +58,7 @@ def __init__( self.has_condition = self._has_condition() self.restrictable_actions = remove_wildcard_only_actions(self.expanded_actions) - self.unrestrictable_actions = list( - set(self.expanded_actions or []) - set(self.restrictable_actions) - ) + self.unrestrictable_actions = list(set(self.expanded_actions or []) - set(self.restrictable_actions)) self.has_resource_constraints = self._has_resource_constraints() def _actions(self) -> List[str]: @@ -111,9 +109,7 @@ def _not_action_effective_actions(self) -> Optional[List[str]]: if not self.not_action: return None - not_actions_expanded_lowercase = [ - a.lower() for a in determine_actions_to_expand(self.not_action) - ] + not_actions_expanded_lowercase = [a.lower() for a in determine_actions_to_expand(self.not_action)] # Effect: Allow && Resource != "*" if not self.has_resource_wildcard and self.effect_allow: @@ -136,9 +132,7 @@ def _not_action_effective_actions(self) -> Optional[List[str]]: # If it's in NotActions, then it is not an action we want effective_actions = [ - action - for action in ALL_ACTIONS - if action.lower() not in not_actions_expanded_lowercase + action for action in ALL_ACTIONS if action.lower() not in not_actions_expanded_lowercase ] effective_actions.sort() @@ -206,12 +200,8 @@ def permissions_management_actions_without_constraints(self) -> List[str]: """Where applicable, returns a list of 'Permissions management' IAM actions in the statement that do not have resource constraints""" result = [] - if ( - not self.has_resource_constraints or self.flag_resource_arn_statements - ) and not self.has_condition: - result = remove_actions_not_matching_access_level( - self.restrictable_actions, "Permissions management" - ) + if (not self.has_resource_constraints or self.flag_resource_arn_statements) and not self.has_condition: + result = remove_actions_not_matching_access_level(self.restrictable_actions, "Permissions management") result.sort() return result @@ -220,12 +210,8 @@ def write_actions_without_constraints(self) -> List[str]: """Where applicable, returns a list of 'Write' level IAM actions in the statement that do not have resource constraints""" result = [] - if ( - not self.has_resource_constraints or self.flag_resource_arn_statements - ) and not self.has_condition: - result = remove_actions_not_matching_access_level( - self.restrictable_actions, "Write" - ) + if (not self.has_resource_constraints or self.flag_resource_arn_statements) and not self.has_condition: + result = remove_actions_not_matching_access_level(self.restrictable_actions, "Write") result.sort() return result @@ -235,20 +221,15 @@ def tagging_actions_without_constraints(self) -> List[str]: do not have resource constraints""" result = [] if not self.has_resource_constraints: - result = remove_actions_not_matching_access_level( - self.restrictable_actions, "Tagging" - ) + result = remove_actions_not_matching_access_level(self.restrictable_actions, "Tagging") return result - def missing_resource_constraints( - self, exclusions: Exclusions = DEFAULT_EXCLUSIONS - ) -> List[str]: + def missing_resource_constraints(self, exclusions: Exclusions = DEFAULT_EXCLUSIONS) -> List[str]: """Return a list of any actions - regardless of access level - allowed by the statement that do not leverage resource constraints.""" if not isinstance(exclusions, Exclusions): raise Exception( # pragma: no cover - "The provided exclusions is not the Exclusions object type. " - "Please use the Exclusions object." + "The provided exclusions is not the Exclusions object type. " "Please use the Exclusions object." ) actions_missing_resource_constraints = [] if len(self.resources) == 1 and self.resources[0] == "*": @@ -262,9 +243,7 @@ def missing_resource_constraints( result.sort() return result - def missing_resource_constraints_for_modify_actions( - self, exclusions: Exclusions = DEFAULT_EXCLUSIONS - ) -> List[str]: + def missing_resource_constraints_for_modify_actions(self, exclusions: Exclusions = DEFAULT_EXCLUSIONS) -> List[str]: """ Determine whether or not any actions at the 'Write', 'Permissions management', or 'Tagging' access levels are allowed by the statement without resource constraints. @@ -273,8 +252,7 @@ def missing_resource_constraints_for_modify_actions( """ if not isinstance(exclusions, Exclusions): raise Exception( # pragma: no cover - "The provided exclusions is not the Exclusions object type. " - "Please use the Exclusions object." + "The provided exclusions is not the Exclusions object type. " "Please use the Exclusions object." ) # This initially includes read-only and modify level actions if exclusions.include_actions: @@ -282,9 +260,7 @@ def missing_resource_constraints_for_modify_actions( else: always_look_for_actions = [] - actions_missing_resource_constraints = self.missing_resource_constraints( - exclusions - ) + actions_missing_resource_constraints = self.missing_resource_constraints(exclusions) always_actions_found = [] for action in actions_missing_resource_constraints: @@ -292,9 +268,7 @@ def missing_resource_constraints_for_modify_actions( always_actions_found.append(action) modify_actions_missing_constraints = set() - modify_actions_missing_constraints.update( - remove_read_level_actions(actions_missing_resource_constraints) - ) + modify_actions_missing_constraints.update(remove_read_level_actions(actions_missing_resource_constraints)) modify_actions_missing_constraints.update(always_actions_found) return list(modify_actions_missing_constraints) diff --git a/cloudsplaining/scan/user_details.py b/cloudsplaining/scan/user_details.py index 2fc04085..317b80c4 100644 --- a/cloudsplaining/scan/user_details.py +++ b/cloudsplaining/scan/user_details.py @@ -74,9 +74,7 @@ def get_all_allowed_actions_for_user(self, name: str) -> Optional[List[str]]: return user_detail.all_allowed_actions return None - def get_all_iam_statements_for_user( - self, name: str - ) -> Optional[List[StatementDetail]]: + def get_all_iam_statements_for_user(self, name: str) -> Optional[List[StatementDetail]]: """Returns a list of all StatementDetail objects across all the policies assigned to the user""" for user_detail in self.users: if user_detail.user_name == name: @@ -176,10 +174,7 @@ def __init__( policy_name = policy_detail.get("PolicyName") policy_document = policy_detail.get("PolicyDocument") policy_id = get_non_provider_id(json.dumps(policy_document)) - if not ( - exclusions.is_policy_excluded(policy_name) - or exclusions.is_policy_excluded(policy_id) - ): + if not (exclusions.is_policy_excluded(policy_name) or exclusions.is_policy_excluded(policy_id)): inline_policy = InlinePolicy( policy_detail, exclusions=exclusions, @@ -201,12 +196,8 @@ def __init__( or exclusions.is_policy_excluded(get_policy_name(arn)) ): try: - attached_managed_policy_details = ( - policy_details.get_policy_detail(arn) - ) - self.attached_managed_policies.append( - attached_managed_policy_details - ) + attached_managed_policy_details = policy_details.get_policy_detail(arn) + self.attached_managed_policies.append(attached_managed_policy_details) except NotFoundException as e: utils.print_red(f"\tError in user {self.user_name}: {e}") @@ -223,9 +214,7 @@ def _is_excluded(self, exclusions: Exclusions) -> bool: or exclusions.is_principal_excluded(self.path, "User") ) - def _add_group_details( - self, group_list: List[str], all_group_details: GroupDetailList - ) -> None: + def _add_group_details(self, group_list: List[str], all_group_details: GroupDetailList) -> None: for group in group_list: this_group_detail = all_group_details.get_group_detail(group) if this_group_detail: @@ -258,18 +247,13 @@ def all_iam_statements(self) -> List[StatementDetail]: @property def attached_managed_policies_json(self) -> Dict[str, Dict[str, Any]]: """Return JSON representation of attached managed policies""" - policies = { - policy.policy_id: policy.json for policy in self.attached_managed_policies - } + policies = {policy.policy_id: policy.json for policy in self.attached_managed_policies} return policies @property def attached_managed_policies_pointer_json(self) -> Dict[str, str]: """Return JSON representation of attached managed policies - but just with pointers to the Policy ID""" - policies = { - policy.policy_id: policy.policy_name - for policy in self.attached_managed_policies - } + policies = {policy.policy_id: policy.policy_name for policy in self.attached_managed_policies} return policies @property @@ -303,17 +287,13 @@ def all_infrastructure_modification_actions_by_inline_policies(self) -> List[str @property def inline_policies_json(self) -> Dict[str, Dict[str, Any]]: """Return JSON representation of attached inline policies""" - policies = { - policy.policy_id: policy.json_large for policy in self.inline_policies - } + policies = {policy.policy_id: policy.json_large for policy in self.inline_policies} return policies @property def inline_policies_pointer_json(self) -> Dict[str, str]: """Return metadata on attached inline policies so you can look it up in the policies section later.""" - policies = { - policy.policy_id: policy.policy_name for policy in self.inline_policies - } + policies = {policy.policy_id: policy.policy_name for policy in self.inline_policies} return policies @property diff --git a/cloudsplaining/shared/aws_login.py b/cloudsplaining/shared/aws_login.py index 8986f9e0..7d6d9cdd 100644 --- a/cloudsplaining/shared/aws_login.py +++ b/cloudsplaining/shared/aws_login.py @@ -17,9 +17,7 @@ logger = logging.getLogger(__name__) -def get_boto3_client( - service: str, profile: Optional[str] = None, region: str = "us-east-1" -) -> BaseClient: +def get_boto3_client(service: str, profile: Optional[str] = None, region: str = "us-east-1") -> BaseClient: """Get a boto3 client for a given service""" logging.getLogger("botocore").setLevel(logging.CRITICAL) session_data = {"region_name": region} @@ -36,15 +34,11 @@ def get_boto3_client( ) else: client = session.client(service, config=config) - logger.debug( - f"{client.meta.endpoint_url} in {client.meta.region_name}: boto3 client login successful" - ) + logger.debug(f"{client.meta.endpoint_url} in {client.meta.region_name}: boto3 client login successful") return client -def get_boto3_resource( - service: str, profile: Optional[str] = None, region: str = "us-east-1" -) -> ServiceResource: +def get_boto3_resource(service: str, profile: Optional[str] = None, region: str = "us-east-1") -> ServiceResource: """Get a boto3 resource for a given service""" logging.getLogger("botocore").setLevel(logging.CRITICAL) session_data = {"region_name": region} @@ -67,9 +61,7 @@ def get_current_account_id(sts_client: STSClient) -> str: def get_available_regions(service: str) -> List[str]: """AWS exposes their list of regions as an API. Gather the list.""" regions: List[str] = boto3.session.Session().get_available_regions(service) - logger.debug( - "The service %s does not have available regions. Returning us-east-1 as default" - ) + logger.debug("The service %s does not have available regions. Returning us-east-1 as default") if not regions: regions = ["us-east-1"] return regions diff --git a/cloudsplaining/shared/constants.py b/cloudsplaining/shared/constants.py index 00426260..d311e975 100644 --- a/cloudsplaining/shared/constants.py +++ b/cloudsplaining/shared/constants.py @@ -13,9 +13,7 @@ logger = logging.getLogger(__name__) -PACKAGE_DIR = str( - os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) -) +PACKAGE_DIR = str(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) EXCLUSIONS_FILE = str(os.path.join(PACKAGE_DIR, "shared", "default-exclusions.yml")) if EXCLUSIONS_FILE: diff --git a/cloudsplaining/shared/exceptions.py b/cloudsplaining/shared/exceptions.py index 04fbe172..f38db380 100644 --- a/cloudsplaining/shared/exceptions.py +++ b/cloudsplaining/shared/exceptions.py @@ -1,3 +1,4 @@ class NotFoundException(Exception): "Raised when something was not found" + pass diff --git a/cloudsplaining/shared/exclusions.py b/cloudsplaining/shared/exclusions.py index 137ac481..65bab528 100644 --- a/cloudsplaining/shared/exclusions.py +++ b/cloudsplaining/shared/exclusions.py @@ -18,9 +18,7 @@ class Exclusions: """Contains the exclusions configuration as an object""" - def __init__( - self, exclusions_config: Dict[str, List[str]] = DEFAULT_EXCLUSIONS_CONFIG - ) -> None: + def __init__(self, exclusions_config: Dict[str, List[str]] = DEFAULT_EXCLUSIONS_CONFIG) -> None: check_exclusions_schema(exclusions_config) self.config = exclusions_config self.include_actions = self._include_actions() @@ -112,9 +110,7 @@ def is_principal_excluded(self, principal: str, principal_type: str) -> bool: elif principal_type == "Role": return is_name_excluded(principal.lower(), self.roles) else: # pragma: no cover - raise Exception( - "Please supply User, Group, or Role as the principal argument." - ) + raise Exception("Please supply User, Group, or Role as the principal argument.") def get_allowed_actions(self, requested_actions: List[str]) -> List[str]: """Given a list of actions, it will evaluate those actions against the exclusions configuration and return a diff --git a/cloudsplaining/shared/utils.py b/cloudsplaining/shared/utils.py index f83ee17a..e2b6ccb6 100644 --- a/cloudsplaining/shared/utils.py +++ b/cloudsplaining/shared/utils.py @@ -59,15 +59,9 @@ def remove_read_level_actions(actions_list: List[str]) -> List[str]: """Given a set of actions, return that list of actions, but only with actions at the 'Write', 'Tagging', or 'Permissions management' levels """ - modify_actions: List[str] = remove_actions_not_matching_access_level( - actions_list, "Write" - ) - modify_actions.extend( - remove_actions_not_matching_access_level(actions_list, "Permissions management") - ) - modify_actions.extend( - remove_actions_not_matching_access_level(actions_list, "Tagging") - ) + modify_actions: List[str] = remove_actions_not_matching_access_level(actions_list, "Write") + modify_actions.extend(remove_actions_not_matching_access_level(actions_list, "Permissions management")) + modify_actions.extend(remove_actions_not_matching_access_level(actions_list, "Tagging")) return modify_actions @@ -124,9 +118,7 @@ def is_aws_managed(arn: str) -> bool: # pragma: no cover -def write_results_data_file( - results: Dict[str, Dict[str, Any]], raw_data_file: str -) -> str: +def write_results_data_file(results: Dict[str, Dict[str, Any]], raw_data_file: str) -> str: """ Writes the raw data file containing all the results for an AWS account diff --git a/cloudsplaining/shared/validation.py b/cloudsplaining/shared/validation.py index 630f1920..c156de47 100644 --- a/cloudsplaining/shared/validation.py +++ b/cloudsplaining/shared/validation.py @@ -61,9 +61,7 @@ def check_exclusions_schema(cfg: Dict[str, List[str]]) -> bool: if result: return result else: - raise Exception( - "The required format of the exclusions template is incorrect. Please try again." - ) + raise Exception("The required format of the exclusions template is incorrect. Please try again.") def check_authorization_details_schema(cfg: Dict[str, List[Any]]) -> bool: diff --git a/examples/jira-tickets/open_jira_ticket.py b/examples/jira-tickets/open_jira_ticket.py index 6c297c2b..41d9b8d9 100644 --- a/examples/jira-tickets/open_jira_ticket.py +++ b/examples/jira-tickets/open_jira_ticket.py @@ -18,55 +18,30 @@ """ -@click.command( - short_help='Open a JIRA ticket with your Cloudsplaining report findings.' -) +@click.command(short_help="Open a JIRA ticket with your Cloudsplaining report findings.") # By default, the client will connect to a JIRA instance started from the Atlassian Plugin SDK # (see https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK for details). +@click.option("--project", required=True, help="The 3-4 character JIRA Project key.") @click.option( - '--project', - required=True, - help="The 3-4 character JIRA Project key." -) -@click.option( - '--auth-file', - required=True, - type=click.Path(exists=True), - help='Path to the account authorization details JSON file.' -) -@click.option( - '--report-file', + "--auth-file", required=True, type=click.Path(exists=True), - help='Path to the HTML Report file.' + help="Path to the account authorization details JSON file.", ) +@click.option("--report-file", required=True, type=click.Path(exists=True), help="Path to the HTML Report file.") +@click.option("--triage-file", required=True, help="Path to the Cloudsplaining Triage worksheet.") +@click.option("--data-file", required=True, type=click.Path(exists=True), help="Path to the JSON Data file.") @click.option( - '--triage-file', - required=True, - help='Path to the Cloudsplaining Triage worksheet.' -) -@click.option( - '--data-file', - required=True, - type=click.Path(exists=True), - help='Path to the JSON Data file.' -) -@click.option( - '--server', + "--server", # default="https://jira.atlassian.com", required=True, type=str, - help='The JIRA server.' + help="The JIRA server.", ) def open_jira_ticket(project, auth_file, report_file, triage_file, data_file, server): jira = jira_login(server) issue = jira.create_issue( - project=project, - summary=ISSUE_SUMMARY, - description=ISSUE_DESCRIPTION, - issuetype={ - 'name': 'Bug' - } + project=project, summary=ISSUE_SUMMARY, description=ISSUE_DESCRIPTION, issuetype={"name": "Bug"} ) for file in [auth_file, report_file, triage_file, data_file]: add_attachment(jira, issue, file) @@ -80,20 +55,14 @@ def open_jira_ticket(project, auth_file, report_file, triage_file, data_file, se def jira_login(server): username = input("Enter your JIRA username") - password = getpass.getpass(prompt='Enter your JIRA password.') + password = getpass.getpass(prompt="Enter your JIRA password.") options = { "server": server, } # Supporting HTTP BASIC Auth right now. # You can extend this script to support Cookie-based, OAuth or Kerberos.""" # Docs: https://jira.readthedocs.io/en/master/examples.html#authentication - auth_jira = JIRA( - options=options, - basic_auth=( - username, - password - ) - ) + auth_jira = JIRA(options=options, basic_auth=(username, password)) return auth_jira @@ -110,18 +79,17 @@ def add_attachment(jira, issue, attachment_path): jira.add_attachment(issue=issue, attachment=attachment_path) # read and upload a file (note binary mode for opening, it's important): - with open(attachment_path, 'rb') as f: + with open(attachment_path, "rb") as f: jira.add_attachment(issue=issue, attachment=f) print(f"Uploaded: {attachment_path}") def list_attachments(issue): for attachment in issue.fields.attachment: - print("Name: '{filename}', size: {size}".format( - filename=attachment.filename, size=attachment.size)) + print("Name: '{filename}', size: {size}".format(filename=attachment.filename, size=attachment.size)) # to read content use `get` method: print("Content: '{}'".format(attachment.get())) -if __name__ == '__main__': +if __name__ == "__main__": open_jira_ticket() diff --git a/examples/scripts/scripting_example.py b/examples/scripts/scripting_example.py index 4f861cd4..0e964cd1 100644 --- a/examples/scripts/scripting_example.py +++ b/examples/scripts/scripting_example.py @@ -2,6 +2,7 @@ """ Demonstrates how to use Cloudsplaining as a library. Using this method you can get the HTML report back as a string. """ + from cloudsplaining.command.scan import scan_account_authorization_details from cloudsplaining.shared.exclusions import DEFAULT_EXCLUSIONS import click @@ -12,10 +13,7 @@ short_help="Shows how to use Cloudsplaining's scan_account_authorization_details method to get the HTML results as a string" ) @click.option( - '--file', - required=True, - type=click.Path(exists=True), - help='Path to the account authorization details JSON file.' + "--file", required=True, type=click.Path(exists=True), help="Path to the account authorization details JSON file." ) def scripting_example(file): with open(file) as f: @@ -27,5 +25,5 @@ def scripting_example(file): print(rendered_html_report) -if __name__ == '__main__': +if __name__ == "__main__": scripting_example() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..fb2d39e2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.ruff] +line-length = 120 +target-version = "py38" diff --git a/requirements-dev.txt b/requirements-dev.txt index c6485800..f19a1d43 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ +# CI +pre-commit==3.5.0 # 3.6+ requires Python 3.9 # Linter pylint==2.17.5 # Unit testing @@ -5,8 +7,6 @@ pytest==8.2.2 coverage==7.6.0 # Security testing bandit==1.7.7 -# Formatter -black==24.4.2 # type check mypy==1.11.0 boto3-stubs-lite[iam,s3,sts]==1.33.13 diff --git a/setup.py b/setup.py index a1b88184..6b8e7acb 100644 --- a/setup.py +++ b/setup.py @@ -8,49 +8,36 @@ import re HERE = os.path.abspath(os.path.dirname(__file__)) -VERSION_RE = re.compile(r'''__version__ = ['"]([0-9.]+)['"]''') -TESTS_REQUIRE = [ - 'coverage', - 'nose', - 'pytest' -] +VERSION_RE = re.compile(r"""__version__ = ['"]([0-9.]+)['"]""") +TESTS_REQUIRE = ["coverage", "nose", "pytest"] REQUIRED_PACKAGES = [ - 'boto3', - 'botocore', - 'cached-property', - 'click', - 'click_option_group', - 'jinja2', - 'markdown', - 'policy_sentry>=0.13.0,<0.14', - 'pyyaml', - 'schema', + "boto3", + "botocore", + "cached-property", + "click", + "click_option_group", + "jinja2", + "markdown", + "policy_sentry>=0.13.0,<0.14", + "pyyaml", + "schema", ] PROJECT_URLS = { "Documentation": "https://policy-sentry.readthedocs.io/", "Example Report": "https://opensource.salesforce.com/cloudsplaining", "Code": "https://github.com/salesforce/cloudsplaining/", "Twitter": "https://twitter.com/kmcquade3", - "Red Team Report": "https://opensource.salesforce.com/policy_sentry" + "Red Team Report": "https://opensource.salesforce.com/policy_sentry", } def get_version(): - init = open( - os.path.join( - HERE, - "cloudsplaining", - "bin", - 'version.py' - ) - ).read() + init = open(os.path.join(HERE, "cloudsplaining", "bin", "version.py")).read() return VERSION_RE.search(init).group(1) def get_description(): - return open( - os.path.join(os.path.abspath(HERE), "README.md"), encoding="utf-8" - ).read() + return open(os.path.join(os.path.abspath(HERE), "README.md"), encoding="utf-8").read() setuptools.setup( @@ -63,23 +50,23 @@ def get_description(): long_description=get_description(), long_description_content_type="text/markdown", url="https://github.com/salesforce/cloudsplaining", - packages=setuptools.find_packages(exclude=['test*', 'tmp*']), + packages=setuptools.find_packages(exclude=["test*", "tmp*"]), tests_require=TESTS_REQUIRE, install_requires=REQUIRED_PACKAGES, project_urls=PROJECT_URLS, classifiers=[ - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], entry_points={"console_scripts": "cloudsplaining=cloudsplaining.bin.cli:main"}, zip_safe=True, - keywords='aws iam roles policy policies privileges security', - python_requires='>=3.8', + keywords="aws iam roles policy policies privileges security", + python_requires=">=3.8", ) diff --git a/test/command/test_expand_policy.py b/test/command/test_expand_policy.py index 73c3986c..770542a9 100644 --- a/test/command/test_expand_policy.py +++ b/test/command/test_expand_policy.py @@ -11,7 +11,9 @@ def setUp(self): def test_click_expand_policy_wildcards(self): """cloudsplaining.command.expand_policy: expand_policy with wildcards example should return exit code 0""" - examples_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir, "examples")) + examples_directory = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir, "examples") + ) input_file = os.path.join(examples_directory, "policies", "wildcards.json") command = f"--input-file {input_file}" args = shlex.split(command) @@ -21,7 +23,9 @@ def test_click_expand_policy_wildcards(self): def test_click_expand_policy_explicit_actions(self): """cloudsplaining.command.expand_policy: expand_policy with explicit actions example should return exit code 0""" - examples_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir, "examples")) + examples_directory = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir, "examples") + ) input_file = os.path.join(examples_directory, "policies", "explicit-actions.json") command = f"--input-file {input_file}" args = shlex.split(command) diff --git a/test/command/test_scan.py b/test/command/test_scan.py index abda5548..54425532 100644 --- a/test/command/test_scan.py +++ b/test/command/test_scan.py @@ -11,13 +11,12 @@ def setUp(self): def test_scan_example_file_with_click(self): """cloudsplaining.command.scan: scan should return exit code 0""" - examples_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir, "examples")) + examples_directory = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir, "examples") + ) input_file = os.path.join(examples_directory, "files", "example.json") exclusions_file = os.path.join(examples_directory, "example-exclusions.yml") - command = f"--input-file {input_file} " \ - f"--exclusions-file {exclusions_file} " \ - "--skip-open-report " \ - "-v" + command = f"--input-file {input_file} " f"--exclusions-file {exclusions_file} " "--skip-open-report " "-v" args = shlex.split(command) response = self.runner.invoke(cli=scan, args=args) # print(response.output) diff --git a/test/command/test_scan_policy_file.py b/test/command/test_scan_policy_file.py index 86294b7c..db775e05 100644 --- a/test/command/test_scan_policy_file.py +++ b/test/command/test_scan_policy_file.py @@ -25,62 +25,60 @@ def test_policy_file(self): "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", - "ecr:PutImage" + "ecr:PutImage", ], - "Resource": "*" + "Resource": "*", }, - { + { "Sid": "AllowManageOwnAccessKeys", "Effect": "Allow", "Action": [ "iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:ListAccessKeys", - "iam:UpdateAccessKey" + "iam:UpdateAccessKey", ], - "Resource": "arn:aws:iam::*:user/${aws:username}" - } - ] + "Resource": "arn:aws:iam::*:user/${aws:username}", + }, + ], } expected_results = { - "ServicesAffected": [ - "ecr" - ], + "ServicesAffected": ["ecr"], "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [] - }, + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings":[] - }, + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": [], + }, "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [] - }, + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": [], + }, "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] - }, + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], + }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [ - "ecr:GetAuthorizationToken" - ]}, + "findings": ["ecr:GetAuthorizationToken"], + }, "InfrastructureModification": { "severity": "low", "description": "", "findings": [ - "ecr:CompleteLayerUpload", - "ecr:InitiateLayerUpload", - "ecr:PutImage", - "ecr:UploadLayerPart" - ]} + "ecr:CompleteLayerUpload", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart", + ], + }, } results = scan_policy(example_policy) # print(json.dumps(results, indent=4)) @@ -92,63 +90,42 @@ def test_excluded_actions_scan_policy_file(self): test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:GetObject", - "iam:CreateAccessKey" - ], - "Resource": "*" - }, - ] + {"Effect": "Allow", "Action": ["s3:GetObject", "iam:CreateAccessKey"], "Resource": "*"}, + ], } results = scan_policy(test_policy) expected_results = { - "ServicesAffected": [ - "iam", - "s3" - ], + "ServicesAffected": ["iam", "s3"], "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [ - { - "type": "CreateAccessKey", - "actions": [ - "iam:createaccesskey" - ] - } - ]}, + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [{"type": "CreateAccessKey", "actions": ["iam:createaccesskey"]}], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings": [ - "iam:CreateAccessKey" - ]}, + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": ["iam:CreateAccessKey"], + }, "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings":[ - "s3:GetObject" - ]}, + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": ["s3:GetObject"], + }, "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [ - "iam:CreateAccessKey" - ]}, + "findings": ["iam:CreateAccessKey"], + }, "InfrastructureModification": { "severity": "low", "description": "", - "findings":[ - "iam:CreateAccessKey", - "s3:GetObject" - ]} + "findings": ["iam:CreateAccessKey", "s3:GetObject"], + }, } # print(json.dumps(results, indent=4)) self.maxDiff = None @@ -159,61 +136,37 @@ def test_excluded_actions_scan_policy_file_v2(self): test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:GetObject", - "iam:CreateAccessKey" - ], - "Resource": "*" - }, - ] + {"Effect": "Allow", "Action": ["s3:GetObject", "iam:CreateAccessKey"], "Resource": "*"}, + ], } expected_results = { "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] - }, - "ServicesAffected": [ - "iam", - "s3" - ], + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], + }, + "ServicesAffected": ["iam", "s3"], "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [ - { - "type": "CreateAccessKey", - "actions": [ - "iam:createaccesskey" - ] - } - ]}, + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [{"type": "CreateAccessKey", "actions": ["iam:createaccesskey"]}], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings":[ - "iam:CreateAccessKey" - ]}, + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": ["iam:CreateAccessKey"], + }, "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [ - "s3:GetObject" - ]}, + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": ["s3:GetObject"], + }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [ - "iam:CreateAccessKey" - ]}, - "InfrastructureModification": { - "severity": "low", - "description": "", - "findings":[ - "iam:CreateAccessKey" - ]} + "findings": ["iam:CreateAccessKey"], + }, + "InfrastructureModification": {"severity": "low", "description": "", "findings": ["iam:CreateAccessKey"]}, } exclusions_cfg_custom = {} results = scan_policy(test_policy, exclusions_cfg_custom) @@ -225,71 +178,63 @@ def test_gh_109_full_access_policy(self): test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": "*", - "Resource": "*" - }, - ] + {"Effect": "Allow", "Action": "*", "Resource": "*"}, + ], } exclusions_cfg_custom = {} results = scan_policy(test_policy, exclusions_cfg_custom) - self.assertTrue(len(results.get("ServiceWildcard")['findings']) > 150) + self.assertTrue(len(results.get("ServiceWildcard")["findings"]) > 150) self.assertTrue(len(results.get("ServicesAffected")) > 150) test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": ["*"], - "Resource": ["*"] - }, - ] + {"Effect": "Allow", "Action": ["*"], "Resource": ["*"]}, + ], } results = scan_policy(test_policy, exclusions_cfg_custom) # print(json.dumps(results, indent=4)) - self.assertTrue(len(results.get("ServiceWildcard")['findings']) > 150) + self.assertTrue(len(results.get("ServiceWildcard")["findings"]) > 150) self.assertTrue(len(results.get("ServicesAffected")) > 150) def test_checkov_gh_990_condition_restricted_action(self): test_policy = { "Version": "2012-10-17", - "Statement": [{ - "Sid": "RestrictedWithConditions", - "Effect": "Allow", - "Action": "s3:GetObject", - "Resource": "*", - "Condition": { - "IpAddress": { - "aws:SourceIp": "192.0.2.0/24" + "Statement": [ + { + "Sid": "RestrictedWithConditions", + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": "*", + "Condition": { + "IpAddress": {"aws:SourceIp": "192.0.2.0/24"}, + "NotIpAddress": {"aws:SourceIp": "192.0.2.188/32"}, }, - "NotIpAddress": { - "aws:SourceIp": "192.0.2.188/32" - } } - }] + ], } exclusions_cfg_custom = {} results = scan_policy(test_policy, exclusions_cfg_custom) print(json.dumps(results, indent=4)) - self.assertListEqual(results.get("InfrastructureModification")['findings'], []) - self.assertListEqual(results.get("DataExfiltration")['findings'], []) + self.assertListEqual(results.get("InfrastructureModification")["findings"], []) + self.assertListEqual(results.get("DataExfiltration")["findings"], []) self.assertListEqual(results.get("ServicesAffected"), []) test_policy_without_condition = { "Version": "2012-10-17", - "Statement": [{ - "Sid": "Unrestricted", - "Effect": "Allow", - "Action": "s3:GetObject", - "Resource": "*", - }] + "Statement": [ + { + "Sid": "Unrestricted", + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": "*", + } + ], } results = scan_policy(test_policy_without_condition, exclusions_cfg_custom) print(json.dumps(results, indent=4)) - self.assertListEqual(results.get("InfrastructureModification")['findings'], []) - self.assertListEqual(results.get("DataExfiltration")['findings'], ["s3:GetObject"]) + self.assertListEqual(results.get("InfrastructureModification")["findings"], []) + self.assertListEqual(results.get("DataExfiltration")["findings"], ["s3:GetObject"]) self.assertListEqual(results.get("ServicesAffected"), ["s3"]) def test_gh_254_all_risky_actions_scan_policy(self): @@ -299,11 +244,8 @@ def test_gh_254_all_risky_actions_scan_policy(self): # Privilege Escalation { "Effect": "Allow", - "Action": [ - "iam:UpdateAssumeRolePolicy", - "sts:AssumeRole" - ], - "Resource": "arn:aws:iam::111122223333:role/MyRole" + "Action": ["iam:UpdateAssumeRolePolicy", "sts:AssumeRole"], + "Resource": "arn:aws:iam::111122223333:role/MyRole", }, # Data Exfiltration { @@ -311,7 +253,7 @@ def test_gh_254_all_risky_actions_scan_policy(self): "Action": [ "s3:GetObject", ], - "Resource": "arn:aws:s3:::mybucket/*" + "Resource": "arn:aws:s3:::mybucket/*", }, # Resource Expsoure { @@ -319,7 +261,7 @@ def test_gh_254_all_risky_actions_scan_policy(self): "Action": [ "s3:PutBucketAcl", ], - "Resource": "arn:aws:s3:::mybucket" + "Resource": "arn:aws:s3:::mybucket", }, # Credentials Exposure { @@ -327,7 +269,7 @@ def test_gh_254_all_risky_actions_scan_policy(self): "Action": [ "iam:UpdateAccessKey", ], - "Resource": "arn:aws:iam::111122223333:user/MyUser" + "Resource": "arn:aws:iam::111122223333:user/MyUser", }, # Infrastructure Modification { @@ -335,67 +277,54 @@ def test_gh_254_all_risky_actions_scan_policy(self): "Action": [ "ec2:AuthorizeSecurityGroupIngress", ], - "Resource": "arn:aws:ec2:us-east-1:111122223333:security-group/sg-12345678" - } - ] + "Resource": "arn:aws:ec2:us-east-1:111122223333:security-group/sg-12345678", + }, + ], } - results = scan_policy(policy_with_resource_constraints, flag_resource_arn_statements=True, flag_conditional_statements=True) + results = scan_policy( + policy_with_resource_constraints, flag_resource_arn_statements=True, flag_conditional_statements=True + ) expected_results = { "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, - "ServicesAffected": [ - "ec2", - "iam", - "s3", - "sts" - ], - "PrivilegeEscalation": { + "ServicesAffected": ["ec2", "iam", "s3", "sts"], + "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', "findings": [ - { - "type": "UpdateRolePolicyToAssumeIt", - "actions": [ - "iam:updateassumerolepolicy", - "sts:assumerole" - ] - } - ]}, + {"type": "UpdateRolePolicyToAssumeIt", "actions": ["iam:updateassumerolepolicy", "sts:assumerole"]} + ], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings":[ - "iam:UpdateAssumeRolePolicy", - "s3:PutBucketAcl", - "iam:UpdateAccessKey" - ]}, + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": ["iam:UpdateAssumeRolePolicy", "s3:PutBucketAcl", "iam:UpdateAccessKey"], + }, "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [ - "s3:GetObject" - ]}, - "CredentialsExposure":{ + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": ["s3:GetObject"], + }, + "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [ - "iam:UpdateAccessKey", - "sts:AssumeRole" - ]}, + "findings": ["iam:UpdateAccessKey", "sts:AssumeRole"], + }, "InfrastructureModification": { "severity": "low", "description": "", "findings": [ - "ec2:AuthorizeSecurityGroupIngress", - "iam:UpdateAccessKey", - "iam:UpdateAssumeRolePolicy", - "s3:GetObject", - "s3:PutBucketAcl", - "sts:AssumeRole" - ]} + "ec2:AuthorizeSecurityGroupIngress", + "iam:UpdateAccessKey", + "iam:UpdateAssumeRolePolicy", + "s3:GetObject", + "s3:PutBucketAcl", + "sts:AssumeRole", + ], + }, } # print(json.dumps(results, indent=4)) diff --git a/test/output/test_policy_finding.py b/test/output/test_policy_finding.py index 14e742df..4199397a 100644 --- a/test/output/test_policy_finding.py +++ b/test/output/test_policy_finding.py @@ -9,61 +9,43 @@ class TestPolicyFinding(unittest.TestCase): def test_policy_finding_for_data_exfiltration(self): test_policy = { "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:GetObject" - ], - "Resource": "*" - } - ] + "Statement": [{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*"}], } policy_document = PolicyDocument(test_policy) # (1) If the user is a member of an excluded group, return True - exclusions_cfg = dict( - users=["obama"], - groups=["exclude-group"], - roles=["MyRole"], - policies=["exclude-policy"] - ) + exclusions_cfg = dict(users=["obama"], groups=["exclude-group"], roles=["MyRole"], policies=["exclude-policy"]) exclusions = Exclusions(exclusions_cfg) policy_finding = PolicyFinding(policy_document, exclusions) results = policy_finding.results expected_results = { "ServicesAffected": ["s3"], - "PrivilegeEscalation": { + "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [] + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [], }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings":[] + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": [], }, - "DataExfiltration": { + "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [ - "s3:GetObject" - ]}, - "ServiceWildcard":{ + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": ["s3:GetObject"], + }, + "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [] - }, - "InfrastructureModification": { - "severity": "low", - "description": "", - "findings":[] - }, + "findings": [], + }, + "InfrastructureModification": {"severity": "low", "description": "", "findings": []}, } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results) @@ -71,15 +53,7 @@ def test_policy_finding_for_data_exfiltration(self): def test_policy_finding_for_resource_exposure(self): test_policy = { "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:PutObjectAcl" - ], - "Resource": "*" - } - ] + "Statement": [{"Effect": "Allow", "Action": ["s3:PutObjectAcl"], "Resource": "*"}], } policy_document = PolicyDocument(test_policy) @@ -90,36 +64,32 @@ def test_policy_finding_for_resource_exposure(self): results = policy_finding.results expected_results = { "ServicesAffected": ["s3"], - "PrivilegeEscalation": { + "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [] + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [], }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings": ["s3:PutObjectAcl"] + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": ["s3:PutObjectAcl"], }, - "DataExfiltration": { + "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [] - }, - "ServiceWildcard":{ + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": [], + }, + "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [] - }, - "InfrastructureModification": { - "severity": "low", - "description": "", - "findings": ["s3:PutObjectAcl"] - }, + "findings": [], + }, + "InfrastructureModification": {"severity": "low", "description": "", "findings": ["s3:PutObjectAcl"]}, } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results) @@ -127,15 +97,7 @@ def test_policy_finding_for_resource_exposure(self): def test_policy_finding_for_privilege_escalation(self): test_policy = { "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "iam:CreatePolicyVersion" - ], - "Resource": "*" - } - ] + "Statement": [{"Effect": "Allow", "Action": ["iam:CreatePolicyVersion"], "Resource": "*"}], } policy_document = PolicyDocument(test_policy) @@ -146,40 +108,41 @@ def test_policy_finding_for_privilege_escalation(self): results = policy_finding.results expected_results = { "ServicesAffected": ["iam"], - "PrivilegeEscalation": { + "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', "findings": [ - { - "type": "CreateNewPolicyVersion", - "actions": ["iam:createpolicyversion"], - } - ]}, + { + "type": "CreateNewPolicyVersion", + "actions": ["iam:createpolicyversion"], + } + ], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings": ["iam:CreatePolicyVersion"] + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": ["iam:CreatePolicyVersion"], }, - "DataExfiltration": { + "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [] - }, - "ServiceWildcard":{ + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": [], + }, + "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [] - }, + "findings": [], + }, "InfrastructureModification": { "severity": "low", "description": "", - "findings": ["iam:CreatePolicyVersion"] - }, + "findings": ["iam:CreatePolicyVersion"], + }, } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results) @@ -193,56 +156,47 @@ def test_finding_actions_excluded(self): "Action": [ # "s3:GetObject", "logs:CreateLogStream", - "logs:PutLogEvents" + "logs:PutLogEvents", ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) # (1) EXCLUDE actions - exclusions_cfg = { - "exclude-actions": [ - "logs:CreateLogStream", - "logs:PutLogEvents" - ] - } + exclusions_cfg = {"exclude-actions": ["logs:CreateLogStream", "logs:PutLogEvents"]} exclusions = Exclusions(exclusions_cfg) policy_finding = PolicyFinding(policy_document, exclusions) results = policy_finding.results expected_results = { "ServicesAffected": [], - "PrivilegeEscalation": { + "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [] - }, + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings": [] + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": [], }, - "DataExfiltration": { + "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [] - }, - "ServiceWildcard":{ + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": [], + }, + "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [] - }, - "InfrastructureModification": { - "severity": "low", - "description": "", - "findings": [] - }, + "findings": [], + }, + "InfrastructureModification": {"severity": "low", "description": "", "findings": []}, } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results) @@ -255,38 +209,36 @@ def test_finding_actions_excluded(self): results = policy_finding.results expected_results = { "ServicesAffected": ["logs"], - "PrivilegeEscalation": { + "PrivilegeEscalation": { "severity": "high", - "description": "

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

", - "findings": [] - }, + "description": '

These policies allow a combination of IAM actions that allow a principal with these permissions to escalate their privileges - for example, by creating an access key for another IAM user, or modifying their own permissions. This research was pioneered by Spencer Gietzen at Rhino Security Labs. Remediation guidance can be found here.

', + "findings": [], + }, "ResourceExposure": { "severity": "high", - "description": "

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

", - "findings": [] + "description": '

Resource Exposure actions allow modification of Permissions to resource-based policies or otherwise can expose AWS resources to the public via similar actions that can lead to resource exposure - for example, the ability to modify AWS Resource Access Manager.

', + "findings": [], }, - "DataExfiltration": { + "DataExfiltration": { "severity": "medium", - "description": "

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

", - "findings": [] - }, - "ServiceWildcard":{ + "description": '

Policies with Data Exfiltration potential allow certain read-only IAM actions without resource constraints, such as s3:GetObject, ssm:GetParameter*, or secretsmanager:GetSecretValue.

', + "findings": [], + }, + "ServiceWildcard": { "severity": "medium", - "description": "

\"Service Wildcard\" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

", - "findings": [] + "description": '

"Service Wildcard" is the unofficial way of referring to IAM policy statements that grant access to ALL actions under a service - like s3:*. Prioritizing the remediation of policies with this characteristic can help to efficiently reduce the total count of issues in the Cloudsplaining report.

', + "findings": [], }, "CredentialsExposure": { "severity": "high", "description": "

Credentials Exposure actions return credentials as part of the API response , such as ecr:GetAuthorizationToken, iam:UpdateAccessKey, and others. The full list is maintained here: https://gist.github.com/kmcquade/33860a617e651104d243c324ddf7992a

", - "findings": [] - }, + "findings": [], + }, "InfrastructureModification": { "severity": "low", "description": "", - "findings": [ - "logs:CreateLogStream", - "logs:PutLogEvents" - ]}, + "findings": ["logs:CreateLogStream", "logs:PutLogEvents"], + }, } # print(json.dumps(results, indent=4)) self.assertDictEqual(results, expected_results) diff --git a/test/scanning/test_action_links.py b/test/scanning/test_action_links.py index 625c5afa..7f260449 100644 --- a/test/scanning/test_action_links.py +++ b/test/scanning/test_action_links.py @@ -21,7 +21,6 @@ class TestActionLinks(unittest.TestCase): - def test_infrastructure_modification_actions(self): policy_details = ManagedPolicyDetails(auth_details_json.get("Policies")) infra_mod_actions = sorted(policy_details.all_infrastructure_modification_actions) @@ -33,10 +32,7 @@ def test_group_details_infra_mod_actions(self): group_detail_list = GroupDetailList(group_details_json_input, policy_details) results = group_detail_list.all_infrastructure_modification_actions_by_inline_policies print(json.dumps(results, indent=4)) - expected_results = [ - "s3:GetObject", - "s3:PutObjectAcl" - ] + expected_results = ["s3:GetObject", "s3:PutObjectAcl"] self.assertListEqual(results, expected_results) self.assertTrue(len(results) >= 2) @@ -53,7 +49,7 @@ def test_role_details_infra_mod_actions(self): "iam:CreateInstanceProfile", "iam:PassRole", "s3:GetObject", - "secretsmanager:GetSecretValue" + "secretsmanager:GetSecretValue", ] print(json.dumps(results, indent=4)) self.assertListEqual(results, expected_results) @@ -66,16 +62,10 @@ def test_user_details_infra_mod_actions(self): group_detail_list = GroupDetailList(group_details_json_input, policy_details) user_detail_list = UserDetailList( - user_details=user_details_json_input, - policy_details=policy_details, - all_group_details=group_detail_list + user_details=user_details_json_input, policy_details=policy_details, all_group_details=group_detail_list ) results = user_detail_list.all_infrastructure_modification_actions_by_inline_policies - expected_results = [ - "s3:GetObject", - "s3:PutObject", - "s3:PutObjectAcl" - ] + expected_results = ["s3:GetObject", "s3:PutObject", "s3:PutObjectAcl"] print(json.dumps(results, indent=4)) self.assertListEqual(results, expected_results) diff --git a/test/scanning/test_authorization_details.py b/test/scanning/test_authorization_details.py index 02e88c9a..fffd1dc2 100644 --- a/test/scanning/test_authorization_details.py +++ b/test/scanning/test_authorization_details.py @@ -18,12 +18,9 @@ def get_authorization_details_with_example_policy(policy_document_dict: dict) -> "CreateDate": "2019-12-18 19:10:08+00:00", "GroupList": [], "AttachedManagedPolicies": [ - { - "PolicyName": "SomePolicy", - "PolicyArn": "arn:aws:iam::111122223333:policy/SomePolicy" - } + {"PolicyName": "SomePolicy", "PolicyArn": "arn:aws:iam::111122223333:policy/SomePolicy"} ], - "Tags": [] + "Tags": [], } ], "GroupDetailList": [], @@ -45,11 +42,11 @@ def get_authorization_details_with_example_policy(policy_document_dict: dict) -> "Document": policy_document_dict, "VersionId": "v9", "IsDefaultVersion": True, - "CreateDate": "2020-01-29 23:23:12+00:00" + "CreateDate": "2020-01-29 23:23:12+00:00", } - ] + ], } - ] + ], } return authz_details @@ -62,11 +59,8 @@ def setUp(self) -> None: # Privilege Escalation { "Effect": "Allow", - "Action": [ - "iam:UpdateAssumeRolePolicy", - "sts:AssumeRole" - ], - "Resource": "arn:aws:iam::111122223333:role/MyRole" + "Action": ["iam:UpdateAssumeRolePolicy", "sts:AssumeRole"], + "Resource": "arn:aws:iam::111122223333:role/MyRole", }, # Data Exfiltration { @@ -74,7 +68,7 @@ def setUp(self) -> None: "Action": [ "s3:GetObject", ], - "Resource": "arn:aws:s3:::mybucket/*" + "Resource": "arn:aws:s3:::mybucket/*", }, # Resource Expsoure { @@ -82,7 +76,7 @@ def setUp(self) -> None: "Action": [ "s3:PutBucketAcl", ], - "Resource": "arn:aws:s3:::mybucket" + "Resource": "arn:aws:s3:::mybucket", }, # Credentials Exposure { @@ -90,7 +84,7 @@ def setUp(self) -> None: "Action": [ "iam:UpdateAccessKey", ], - "Resource": "arn:aws:iam::111122223333:user/MyUser" + "Resource": "arn:aws:iam::111122223333:user/MyUser", }, # Infrastructure Modification { @@ -98,26 +92,55 @@ def setUp(self) -> None: "Action": [ "ec2:AuthorizeSecurityGroupIngress", ], - "Resource": "arn:aws:ec2:us-east-1:111122223333:security-group/sg-12345678" - } - ] + "Resource": "arn:aws:ec2:us-east-1:111122223333:security-group/sg-12345678", + }, + ], } - self.authz_json = get_authorization_details_with_example_policy(policy_document_dict=self.policy_with_resource_constraints) + self.authz_json = get_authorization_details_with_example_policy( + policy_document_dict=self.policy_with_resource_constraints + ) def test_authorization_details_with_resource_constraints(self): """Case: Authorization details with resource constraints, WITHOUT --flag-all-risky-actions""" authz_details = AuthorizationDetails(auth_json=self.authz_json) self.assertListEqual(authz_details.policies.policy_details[0].policy_document.credentials_exposure, []) self.assertListEqual(authz_details.policies.policy_details[0].policy_document.allows_privilege_escalation, []) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.allows_data_exfiltration_actions, []) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.permissions_management_without_constraints, []) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.allows_data_exfiltration_actions, [] + ) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.permissions_management_without_constraints, [] + ) self.assertListEqual(authz_details.policies.policy_details[0].policy_document.infrastructure_modification, []) def test_authorization_details_flag_all_risky_actions(self): """Case: Authorization details with resource constraints, WITH --flag-all-risky-actions""" - authz_details = AuthorizationDetails(auth_json=self.authz_json, flag_resource_arn_statements=True, flag_conditional_statements=True) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.credentials_exposure, ['iam:UpdateAccessKey', 'sts:AssumeRole']) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.allows_privilege_escalation, [{'type': 'UpdateRolePolicyToAssumeIt', 'actions': ['iam:updateassumerolepolicy', 'sts:assumerole']}]) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.allows_data_exfiltration_actions, ['s3:GetObject']) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.permissions_management_without_constraints, ['iam:UpdateAssumeRolePolicy', 's3:PutBucketAcl', 'iam:UpdateAccessKey']) - self.assertListEqual(authz_details.policies.policy_details[0].policy_document.infrastructure_modification, ['ec2:AuthorizeSecurityGroupIngress', 'iam:UpdateAccessKey', 'iam:UpdateAssumeRolePolicy', 's3:GetObject', 's3:PutBucketAcl', 'sts:AssumeRole']) + authz_details = AuthorizationDetails( + auth_json=self.authz_json, flag_resource_arn_statements=True, flag_conditional_statements=True + ) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.credentials_exposure, + ["iam:UpdateAccessKey", "sts:AssumeRole"], + ) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.allows_privilege_escalation, + [{"type": "UpdateRolePolicyToAssumeIt", "actions": ["iam:updateassumerolepolicy", "sts:assumerole"]}], + ) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.allows_data_exfiltration_actions, ["s3:GetObject"] + ) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.permissions_management_without_constraints, + ["iam:UpdateAssumeRolePolicy", "s3:PutBucketAcl", "iam:UpdateAccessKey"], + ) + self.assertListEqual( + authz_details.policies.policy_details[0].policy_document.infrastructure_modification, + [ + "ec2:AuthorizeSecurityGroupIngress", + "iam:UpdateAccessKey", + "iam:UpdateAssumeRolePolicy", + "s3:GetObject", + "s3:PutBucketAcl", + "sts:AssumeRole", + ], + ) diff --git a/test/scanning/test_group_detail_list.py b/test/scanning/test_group_detail_list.py index 13816e0c..10117018 100644 --- a/test/scanning/test_group_detail_list.py +++ b/test/scanning/test_group_detail_list.py @@ -54,7 +54,7 @@ def test_group_detail_list_allowed_actions_lookup(self): policy_details = ManagedPolicyDetails(auth_details_json.get("Policies")) group_detail_list = GroupDetailList(group_details_json_input, policy_details) # print(group_detail_list.group_names) - actions = group_detail_list.get_all_allowed_actions_for_group('biden') + actions = group_detail_list.get_all_allowed_actions_for_group("biden") self.assertTrue("s3:GetObject" in actions) # privileges = group_detail_list.get_all_iam_statements_for_group('biden') diff --git a/test/scanning/test_inline_policy.py b/test/scanning/test_inline_policy.py index 6e65914a..90adae7b 100644 --- a/test/scanning/test_inline_policy.py +++ b/test/scanning/test_inline_policy.py @@ -19,21 +19,18 @@ class TestInlinePolicyDetail(unittest.TestCase): def test_inline_policies(self): inline_policy_detail = { - "PolicyName": "InlinePolicyForBidenGroup", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": [ - "s3:GetObject", - "s3:PutObjectAcl" + "PolicyName": "InlinePolicyForBidenGroup", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObjectAcl"], + "Resource": "*", + } ], - "Resource": "*" - } - ] - } + }, } inline_policy = InlinePolicy(inline_policy_detail) results = inline_policy.json diff --git a/test/scanning/test_managed_policy_detail.py b/test/scanning/test_managed_policy_detail.py index 368bbd15..8c01c440 100644 --- a/test/scanning/test_managed_policy_detail.py +++ b/test/scanning/test_managed_policy_detail.py @@ -48,7 +48,7 @@ def test_managed_policies(self): "ANPAJLIB4VSBVO47ZSBB6", "ANPAJNPP7PPPPMJRV2SA4", "ANPAJWVDLG5RPST6PHQ3A", - "ANPAJYRXTHIB4FOVS3ZXS" + "ANPAJYRXTHIB4FOVS3ZXS", ] self.assertListEqual(list(results.keys()), expected_keys) diff --git a/test/scanning/test_policy_document.py b/test/scanning/test_policy_document.py index cb3bc67a..a38813e6 100644 --- a/test/scanning/test_policy_document.py +++ b/test/scanning/test_policy_document.py @@ -9,22 +9,14 @@ def test_policy_document_return_json(self): test_policy = { "Version": "2012-10-17", "Statement": [ + {"Effect": "Allow", "Action": ["ecr:PutImage"], "Resource": "*"}, { - "Effect": "Allow", - "Action": [ - "ecr:PutImage" - ], - "Resource": "*" - }, - { "Sid": "AllowManageOwnAccessKeys", "Effect": "Allow", - "Action": [ - "iam:CreateAccessKey" - ], - "Resource": "arn:aws:iam::*:user/${aws:username}" - } - ] + "Action": ["iam:CreateAccessKey"], + "Resource": "arn:aws:iam::*:user/${aws:username}", + }, + ], } policy_document = PolicyDocument(test_policy) result = policy_document.json @@ -35,31 +27,21 @@ def test_policy_document_return_statement_results(self): test_policy = { "Version": "2012-10-17", "Statement": [ + {"Effect": "Allow", "Action": ["ssm:GetParameters", "ecr:PutImage"], "Resource": "*"}, { - "Effect": "Allow", - "Action": [ - "ssm:GetParameters", - "ecr:PutImage" - ], - "Resource": "*" - }, - { "Sid": "AllowManageOwnAccessKeys", "Effect": "Allow", - "Action": [ - "iam:CreateAccessKey" - ], - "Resource": "arn:aws:iam::*:user/${aws:username}" - } - ] + "Action": ["iam:CreateAccessKey"], + "Resource": "arn:aws:iam::*:user/${aws:username}", + }, + ], } policy_document = PolicyDocument(test_policy) actions_missing_resource_constraints = [] # Read only for statement in policy_document.statements: - actions_missing_resource_constraints.extend( - statement.missing_resource_constraints()) - self.assertCountEqual(actions_missing_resource_constraints, ['ssm:GetParameters', 'ecr:PutImage']) + actions_missing_resource_constraints.extend(statement.missing_resource_constraints()) + self.assertCountEqual(actions_missing_resource_constraints, ["ssm:GetParameters", "ecr:PutImage"]) # Modify only # modify_actions_missing_resource_constraints = [] @@ -72,40 +54,28 @@ def test_policy_document_return_statement_results(self): modify_actions_missing_resource_constraints = [] for statement in policy_document.statements: modify_actions_missing_resource_constraints.extend( - statement.missing_resource_constraints_for_modify_actions()) - self.assertCountEqual(modify_actions_missing_resource_constraints, ['ecr:PutImage', 'ssm:GetParameters']) + statement.missing_resource_constraints_for_modify_actions() + ) + self.assertCountEqual(modify_actions_missing_resource_constraints, ["ecr:PutImage", "ssm:GetParameters"]) def test_policy_document_all_allowed_actions(self): """scan.policy_document.all_allowed_actions""" test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ssm:GetParameters", - "ecr:PutImage" - ], - "Resource": "*" - }, + {"Effect": "Allow", "Action": ["ssm:GetParameters", "ecr:PutImage"], "Resource": "*"}, { "Sid": "AllowManageOwnAccessKeys", "Effect": "Allow", - "Action": [ - "iam:CreateAccessKey" - ], - "Resource": "arn:aws:iam::*:user/${aws:username}" - } - ] + "Action": ["iam:CreateAccessKey"], + "Resource": "arn:aws:iam::*:user/${aws:username}", + }, + ], } policy_document = PolicyDocument(test_policy) result = policy_document.all_allowed_actions - expected_result = [ - "ecr:PutImage", - "ssm:GetParameters", - "iam:CreateAccessKey" - ] + expected_result = ["ecr:PutImage", "ssm:GetParameters", "iam:CreateAccessKey"] self.assertCountEqual(result, expected_result) def test_all_allowed_unrestriced_deny(self): @@ -118,28 +88,20 @@ def test_all_allowed_unrestriced_deny(self): "Action": "*", "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) result = policy_document.all_allowed_unrestricted_actions - self.assertEqual([],result) + self.assertEqual([], result) def test_policy_document_all_allowed_actions_deny(self): """scan.policy_document.all_allowed_actions""" test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": "*", - "Resource": "*" - }, - { - "Effect": "Deny", - "Action": "aws-portal:*", - "Resource": "*" - } - ] + {"Effect": "Allow", "Action": "*", "Resource": "*"}, + {"Effect": "Deny", "Action": "aws-portal:*", "Resource": "*"}, + ], } policy_document = PolicyDocument(test_policy) result = policy_document.all_allowed_actions @@ -159,9 +121,9 @@ def test_allows_privilege_escalation(self): "dynamodb:CreateTable", "dynamodb:PutItem", ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) results = policy_document.allows_privilege_escalation @@ -173,17 +135,13 @@ def test_allows_privilege_escalation(self): "lambda:createfunction", "lambda:createeventsourcemapping", "dynamodb:createtable", - "dynamodb:putitem" - ] + "dynamodb:putitem", + ], }, { "type": "PassExistingRoleToNewLambdaThenTriggerWithExistingDynamo", - "actions": [ - "iam:passrole", - "lambda:createfunction", - "lambda:createeventsourcemapping" - ] - } + "actions": ["iam:passrole", "lambda:createfunction", "lambda:createeventsourcemapping"], + }, ] self.assertListEqual(results, expected_result) @@ -202,11 +160,11 @@ def test_allows_specific_actions(self): "ssm:GetParametersByPath", "secretsmanager:GetSecretValue", "s3:PutObject", - "ec2:CreateTags" + "ec2:CreateTags", ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) results = policy_document.allows_specific_actions_without_constraints(["iam:PassRole"]) @@ -221,7 +179,7 @@ def test_allows_specific_actions(self): "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath", - "secretsmanager:GetSecretValue" + "secretsmanager:GetSecretValue", ] results = policy_document.allows_specific_actions_without_constraints(high_priority_read_only_actions) self.assertCountEqual(results, high_priority_read_only_actions) @@ -238,17 +196,18 @@ def test_allows_specific_actions(self): with self.assertRaises(Exception): results = policy_document.allows_specific_actions_without_constraints("iam:passrole") - def test_policy_document_not_action_deny_gh_23(self): test_policy = { "Version": "2012-10-17", - "Statement": [{ - "Sid": "DenyAllUsersNotUsingMFA", - "Effect": "Deny", - "NotAction": "iam:*", - "Resource": "*", - "Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}} - }] + "Statement": [ + { + "Sid": "DenyAllUsersNotUsingMFA", + "Effect": "Deny", + "NotAction": "iam:*", + "Resource": "*", + "Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}, + } + ], } policy_document = PolicyDocument(test_policy) allowed_actions = [] @@ -271,19 +230,12 @@ def test_policy_document_contains_statement_using_not_action(self): "NotAction": "iam:*", "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) results = policy_document.contains_statement_using_not_action - expected_results = [ - { - "Sid": "Something", - "Effect": "Allow", - "NotAction": "iam:*", - "Resource": "*" - } - ] + expected_results = [{"Sid": "Something", "Effect": "Allow", "NotAction": "iam:*", "Resource": "*"}] # print(json.dumps(results, indent=4)) self.assertListEqual(results, expected_results) @@ -298,24 +250,22 @@ def test_gh_106_excluded_actions_should_not_show_in_results(self): "Action": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", - "autoscaling:UpdateAutoScalingGroup" + "autoscaling:UpdateAutoScalingGroup", ], "Resource": "*", } - ] + ], } exclusions_cfg = { - "policies": [ - "aws-service-role*" - ], + "policies": ["aws-service-role*"], "roles": ["aws-service-role*"], "users": [""], "include-actions": ["s3:GetObject"], "exclude-actions": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", - "autoscaling:UpdateAutoScalingGroup" - ] + "autoscaling:UpdateAutoScalingGroup", + ], } exclusions = Exclusions(exclusions_cfg) policy_document = PolicyDocument(test_policy, exclusions) @@ -323,16 +273,14 @@ def test_gh_106_excluded_actions_should_not_show_in_results(self): self.assertEqual(policy_document.infrastructure_modification, []) exclusions_cfg_2 = { - "policies": [ - "aws-service-role*" - ], + "policies": ["aws-service-role*"], "roles": ["aws-service-role*"], "users": [""], "include-actions": ["s3:GetObject"], "exclude-actions": [ "autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup", - ] + ], } exclusions_2 = Exclusions(exclusions_cfg_2) policy_document_2 = PolicyDocument(test_policy, exclusions_2) @@ -343,25 +291,31 @@ def test_gh_106_excluded_actions_should_not_show_in_results(self): def test_condition_is_a_restricted_action(self): test_policy = { "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": "cloudwatch:PutMetricData", - "Resource": "*", - "Condition": {"StringEquals": {"cloudwatch:namespace": "Namespace"}} - }] + "Statement": [ + { + "Effect": "Allow", + "Action": "cloudwatch:PutMetricData", + "Resource": "*", + "Condition": {"StringEquals": {"cloudwatch:namespace": "Namespace"}}, + } + ], } policy_document = PolicyDocument(test_policy) self.assertListEqual(policy_document.all_allowed_unrestrictable_actions, []) test_policy_without_condition = { "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": "cloudwatch:PutMetricData", - "Resource": "*", - }] + "Statement": [ + { + "Effect": "Allow", + "Action": "cloudwatch:PutMetricData", + "Resource": "*", + } + ], } policy_document_without_condition = PolicyDocument(test_policy_without_condition) - self.assertListEqual(policy_document_without_condition.all_allowed_unrestrictable_actions, ["cloudwatch:PutMetricData"]) + self.assertListEqual( + policy_document_without_condition.all_allowed_unrestrictable_actions, ["cloudwatch:PutMetricData"] + ) def test_actions_without_constraints_deny(self): test_policy = { @@ -376,11 +330,11 @@ def test_actions_without_constraints_deny(self): "s3:PutBucketAcl", "s3:GetObject", "s3:PutObject", - "s3:CreateBucket" + "s3:CreateBucket", ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) results = policy_document.permissions_management_without_constraints @@ -396,15 +350,8 @@ def test_gh_190_allow_xray_wildcard_permissions(self): test_policy = { "Version": "2012-10-17", "Statement": [ - { - "Effect": "Allow", - "Action": [ - "xray:PutTraceSegments", - "xray:PutTelemetryRecords" - ], - "Resource": "*" - } - ] + {"Effect": "Allow", "Action": ["xray:PutTraceSegments", "xray:PutTelemetryRecords"], "Resource": "*"} + ], } policy_document = PolicyDocument(test_policy) results = policy_document.write_actions_without_constraints @@ -429,16 +376,16 @@ def test_gh_193_AmazonEC2ContainerRegistryReadOnly(self): "ecr:GetLifecyclePolicy", "ecr:GetLifecyclePolicyPreview", "ecr:ListTagsForResource", - "ecr:DescribeImageScanFindings" + "ecr:DescribeImageScanFindings", ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy) self.assertTrue("ecr:GetAuthorizationToken" not in policy_document.all_allowed_unrestricted_actions) self.assertListEqual(policy_document.write_actions_without_constraints, []) - self.assertListEqual(policy_document.credentials_exposure, ['ecr:GetAuthorizationToken']) + self.assertListEqual(policy_document.credentials_exposure, ["ecr:GetAuthorizationToken"]) def test_gh_254_flag_risky_actions_with_resource_constraints_privilege_escalation(self): # Privilege Escalation: https://cloudsplaining.readthedocs.io/en/latest/glossary/privilege-escalation/#updating-an-assumerole-policy @@ -447,19 +394,15 @@ def test_gh_254_flag_risky_actions_with_resource_constraints_privilege_escalatio "Statement": [ { "Effect": "Allow", - "Action": [ - "iam:UpdateAssumeRolePolicy", - "sts:AssumeRole" - ], - "Resource": "arn:aws:iam::111122223333:role/MyRole" + "Action": ["iam:UpdateAssumeRolePolicy", "sts:AssumeRole"], + "Resource": "arn:aws:iam::111122223333:role/MyRole", } - ] + ], } policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=True) - expected_result = [{ - 'type': 'UpdateRolePolicyToAssumeIt', - 'actions': ['iam:updateassumerolepolicy', 'sts:assumerole'] - }] + expected_result = [ + {"type": "UpdateRolePolicyToAssumeIt", "actions": ["iam:updateassumerolepolicy", "sts:assumerole"]} + ] self.assertDictEqual(policy_document.allows_privilege_escalation[0], expected_result[0]) policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=False) self.assertListEqual(policy_document.allows_privilege_escalation, []) @@ -474,9 +417,9 @@ def test_gh_254_flag_risky_actions_with_resource_constraints_resource_exposure(s "Action": [ "s3:PutBucketAcl", ], - "Resource": "arn:aws:s3:::mybucket" + "Resource": "arn:aws:s3:::mybucket", } - ] + ], } policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=True) self.assertListEqual(policy_document.permissions_management_without_constraints, ["s3:PutBucketAcl"]) @@ -493,12 +436,12 @@ def test_gh_254_flag_risky_actions_with_resource_constraints_credentials_exposur "Action": [ "iam:UpdateAccessKey", ], - "Resource": "arn:aws:iam::111122223333:user/MyUser" + "Resource": "arn:aws:iam::111122223333:user/MyUser", } - ] + ], } policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=True) - self.assertListEqual(policy_document.credentials_exposure, ['iam:UpdateAccessKey']) + self.assertListEqual(policy_document.credentials_exposure, ["iam:UpdateAccessKey"]) policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=False) self.assertListEqual(policy_document.credentials_exposure, []) @@ -512,12 +455,12 @@ def test_gh_254_flag_risky_actions_with_resource_constraints_data_exfiltration(s "Action": [ "s3:GetObject", ], - "Resource": "arn:aws:s3:::mybucket/*" + "Resource": "arn:aws:s3:::mybucket/*", } - ] + ], } policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=True) - self.assertListEqual(policy_document.allows_data_exfiltration_actions, ['s3:GetObject']) + self.assertListEqual(policy_document.allows_data_exfiltration_actions, ["s3:GetObject"]) policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=False) self.assertListEqual(policy_document.allows_data_exfiltration_actions, []) @@ -531,15 +474,15 @@ def test_gh_254_flag_risky_actions_with_resource_constraints_infrastructure_modi "Action": [ "ec2:AuthorizeSecurityGroupIngress", ], - "Resource": "arn:aws:ec2:us-east-1:111122223333:security-group/sg-12345678" + "Resource": "arn:aws:ec2:us-east-1:111122223333:security-group/sg-12345678", } - ] + ], } policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=True) - self.assertListEqual(policy_document.infrastructure_modification, ['ec2:AuthorizeSecurityGroupIngress']) + self.assertListEqual(policy_document.infrastructure_modification, ["ec2:AuthorizeSecurityGroupIngress"]) policy_document = PolicyDocument(test_policy, flag_resource_arn_statements=False) self.assertListEqual(policy_document.infrastructure_modification, []) - + def test_gh_278_all_issue_types_respect_conditions_on_policy(self): test_policy_all_issues_no_conditions = { "Version": "2012-10-17", @@ -557,33 +500,33 @@ def test_gh_278_all_issue_types_respect_conditions_on_policy(self): "ec2:RunInstances", "iam:PassRole", # Resource Exposure - "ec2:CreateNetworkInterfacePermission" + "ec2:CreateNetworkInterfacePermission", ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document = PolicyDocument(test_policy_all_issues_no_conditions) self.assertListEqual(policy_document.credentials_exposure, ["ec2:GetPasswordData"]) self.assertListEqual(policy_document.allows_data_exfiltration_actions, ["s3:GetObject"]) - self.assertListEqual(policy_document.infrastructure_modification, [ - "ec2:AuthorizeSecurityGroupIngress", - "ec2:CreateNetworkInterfacePermission", - "ec2:RunInstances", - "iam:PassRole", - "s3:GetObject" - ]) - self.assertListEqual(policy_document.allows_privilege_escalation, [{ - "actions": [ - "iam:passrole", - "ec2:runinstances" + self.assertListEqual( + policy_document.infrastructure_modification, + [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:CreateNetworkInterfacePermission", + "ec2:RunInstances", + "iam:PassRole", + "s3:GetObject", ], - "type": "CreateEC2WithExistingIP" - }]) - self.assertListEqual(policy_document.permissions_management_without_constraints, [ - "ec2:CreateNetworkInterfacePermission", - "iam:PassRole" - ]) + ) + self.assertListEqual( + policy_document.allows_privilege_escalation, + [{"actions": ["iam:passrole", "ec2:runinstances"], "type": "CreateEC2WithExistingIP"}], + ) + self.assertListEqual( + policy_document.permissions_management_without_constraints, + ["ec2:CreateNetworkInterfacePermission", "iam:PassRole"], + ) test_policy_service_wildcard = { "Version": "2012-10-17", @@ -594,9 +537,9 @@ def test_gh_278_all_issue_types_respect_conditions_on_policy(self): # Service Wildcard "kinesis:*" ], - "Resource": "*" + "Resource": "*", } - ] + ], } policy_document_service_wildcard = PolicyDocument(test_policy_service_wildcard) self.assertListEqual(policy_document_service_wildcard.service_wildcard, ["kinesis"]) @@ -619,16 +562,12 @@ def test_gh_278_all_issue_types_respect_conditions_on_policy(self): # Resource Exposure "ec2:CreateNetworkInterfacePermission", # Service Wildcard - "kinesis:*" + "kinesis:*", ], "Resource": "*", - "Condition": { - "StringEquals": { - "aws:PrincipalAccount": "123456789012" - } - } + "Condition": {"StringEquals": {"aws:PrincipalAccount": "123456789012"}}, } - ] + ], } policy_document_condition = PolicyDocument(test_policy_all_issues_conditions) self.assertListEqual(policy_document_condition.credentials_exposure, []) @@ -636,4 +575,4 @@ def test_gh_278_all_issue_types_respect_conditions_on_policy(self): self.assertListEqual(policy_document_condition.infrastructure_modification, []) self.assertListEqual(policy_document_condition.allows_privilege_escalation, []) self.assertListEqual(policy_document_condition.permissions_management_without_constraints, []) - self.assertListEqual(policy_document.service_wildcard, []) \ No newline at end of file + self.assertListEqual(policy_document.service_wildcard, []) diff --git a/test/scanning/test_statement_detail.py b/test/scanning/test_statement_detail.py index ab65f626..fcdc3c73 100644 --- a/test/scanning/test_statement_detail.py +++ b/test/scanning/test_statement_detail.py @@ -10,16 +10,16 @@ def test_statement(self): "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ - "iam:CreateInstanceProfile", - "iam:ListInstanceProfilesForRole", - "iam:PassRole", - "ec2:DescribeIamInstanceProfileAssociations", - "iam:GetInstanceProfile", - "ec2:DisassociateIamInstanceProfile", - "ec2:AssociateIamInstanceProfile", - "iam:AddRoleToInstanceProfile" + "iam:CreateInstanceProfile", + "iam:ListInstanceProfilesForRole", + "iam:PassRole", + "ec2:DescribeIamInstanceProfileAssociations", + "iam:GetInstanceProfile", + "ec2:DisassociateIamInstanceProfile", + "ec2:AssociateIamInstanceProfile", + "iam:AddRoleToInstanceProfile", ], - "Resource": "*" + "Resource": "*", } statement = StatementDetail(this_statement) # print(statement.actions) @@ -32,17 +32,10 @@ def test_statement(self): "iam:GetInstanceProfile", "ec2:DisassociateIamInstanceProfile", "ec2:AssociateIamInstanceProfile", - "iam:AddRoleToInstanceProfile" + "iam:AddRoleToInstanceProfile", ] self.assertListEqual(statement.actions, expected_result) - this_statement = { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": [ - "ecr:*" - ], - "Resource": "*" - } + this_statement = {"Sid": "VisualEditor0", "Effect": "Allow", "Action": ["ecr:*"], "Resource": "*"} statement = StatementDetail(this_statement) # print(statement.expanded_actions) @@ -55,11 +48,11 @@ def test_services_in_use(self): "iam:AddRoleToInstanceProfile", "ec2:DescribeIamInstanceProfileAssociations", ], - "Resource": "*" + "Resource": "*", } statement = StatementDetail(this_statement) result = statement.services_in_use - expected_result = ['ec2', 'iam'] + expected_result = ["ec2", "iam"] # print(result) self.assertListEqual(result, expected_result) @@ -75,12 +68,12 @@ def test_missing_resource_constraints(self): # This one is wildcard OR "secret" "secretsmanager:putsecretvalue", ], - "Resource": "*" + "Resource": "*", } statement = StatementDetail(this_statement) result = statement.missing_resource_constraints() # print(result) - self.assertCountEqual(result, ['secretsmanager:CreateSecret', 'secretsmanager:PutSecretValue']) + self.assertCountEqual(result, ["secretsmanager:CreateSecret", "secretsmanager:PutSecretValue"]) def test_missing_resource_constraints_for_modify_actions(self): this_statement = { @@ -93,28 +86,29 @@ def test_missing_resource_constraints_for_modify_actions(self): # This one is wildcard OR "secret" "secretsmanager:putsecretvalue", # This one is wildcard OR object - "s3:GetObject" + "s3:GetObject", ], - "Resource": "*" + "Resource": "*", } statement = StatementDetail(this_statement) result = statement.missing_resource_constraints() # print(result) - self.assertCountEqual(result, ['s3:GetObject', 'secretsmanager:PutSecretValue']) + self.assertCountEqual(result, ["s3:GetObject", "secretsmanager:PutSecretValue"]) result = statement.missing_resource_constraints_for_modify_actions() print(result) - self.assertCountEqual(result, ['s3:GetObject', 'secretsmanager:PutSecretValue']) + self.assertCountEqual(result, ["s3:GetObject", "secretsmanager:PutSecretValue"]) def test_missing_resource_constraints_for_modify_actions_with_override(self): import logging import sys + logger = logging.getLogger(__name__) root = logging.getLogger() root.setLevel(logging.DEBUG) handler = logging.StreamHandler(sys.stdout) handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) root.addHandler(handler) this_statement = { @@ -127,37 +121,27 @@ def test_missing_resource_constraints_for_modify_actions_with_override(self): # This one is wildcard OR "secret" "secretsmanager:putsecretvalue", # This one is wildcard OR object - "s3:GetObject" + "s3:GetObject", ], - "Resource": "*" + "Resource": "*", } statement = StatementDetail(this_statement) results = statement.missing_resource_constraints_for_modify_actions(DEFAULT_EXCLUSIONS) # print(results) - self.assertCountEqual(results, ['s3:GetObject', 'secretsmanager:PutSecretValue']) + self.assertCountEqual(results, ["s3:GetObject", "secretsmanager:PutSecretValue"]) def test_statement_details_for_action_as_string_instead_of_list(self): # Case: when the "Action" is a string - this_statement = { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": "s3:GetObject", - "Resource": "*" - } + this_statement = {"Sid": "VisualEditor0", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*"} # always_look_for_actions = ["s3:GetObject"] statement = StatementDetail(this_statement) results = statement.missing_resource_constraints_for_modify_actions(DEFAULT_EXCLUSIONS) - self.assertListEqual(results, ['s3:GetObject']) + self.assertListEqual(results, ["s3:GetObject"]) def test_statement_details_for_not_resource(self): # Case: when "NotResource" is not included with "Action" as a string - this_statement = { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": "s3:GetObject", - "NotResource": "*" - } + this_statement = {"Sid": "VisualEditor0", "Effect": "Allow", "Action": "s3:GetObject", "NotResource": "*"} statement = StatementDetail(this_statement) results = statement.missing_resource_constraints_for_modify_actions() self.assertListEqual(results, []) @@ -177,9 +161,7 @@ def test_statement_details_for_allow_not_action(self): "cloud9:List*", "cloud9:Update*", ], - "Resource": [ - "arn:aws:cloud9:us-east-1:123456789012:environment:some-resource-id" - ] + "Resource": ["arn:aws:cloud9:us-east-1:123456789012:environment:some-resource-id"], } statement = StatementDetail(this_statement) results = statement.not_action_effective_actions @@ -191,7 +173,7 @@ def test_statement_details_for_allow_not_action(self): "cloud9:ActivateEC2Remote", "cloud9:ModifyTemporaryCredentialsOnEnvironmentEC2", "cloud9:TagResource", - "cloud9:UntagResource" + "cloud9:UntagResource", ] for action in expected_actions: self.assertTrue(action in results) @@ -205,9 +187,7 @@ def test_statement_details_for_allow_not_action(self): "NotAction": [ "iam:*", ], - "Resource": [ - "*" - ] + "Resource": ["*"], } statement = StatementDetail(this_statement) results = statement.not_action_effective_actions @@ -230,9 +210,7 @@ def test_statement_details_for_allow_not_action(self): "Action": [ "iam:*", ], - "Resource": [ - "arn:aws:cloud9:us-east-1:123456789012:environment:some-resource-id" - ] + "Resource": ["arn:aws:cloud9:us-east-1:123456789012:environment:some-resource-id"], } statement = StatementDetail(this_statement) results = statement.not_action_effective_actions @@ -246,9 +224,7 @@ def test_statement_details_for_allow_not_action(self): "Action": [ "iam:*", ], - "Resource": [ - "*" - ] + "Resource": ["*"], } statement = StatementDetail(this_statement) results = statement.not_action_effective_actions @@ -261,9 +237,7 @@ def test_statement_details_for_has_not_resource_with_allow(self): "Action": [ "*", ], - "NotResource": [ - "arn:aws:s3:::HRBucket" - ] + "NotResource": ["arn:aws:s3:::HRBucket"], } statement = StatementDetail(this_statement) results = statement.has_not_resource_with_allow @@ -276,10 +250,7 @@ def test_statement_with_arn_plus_wildcard(self): "Action": [ "*", ], - "Resource": [ - "arn:aws:s3:::HRBucket", - "*" - ] + "Resource": ["arn:aws:s3:::HRBucket", "*"], } statement = StatementDetail(this_statement) results = statement.has_resource_wildcard @@ -296,11 +267,9 @@ def test_actions_without_constraints(self): "s3:PutBucketAcl", "s3:GetObject", "s3:PutObject", - "s3:CreateBucket" + "s3:CreateBucket", ], - "Resource": [ - "*" - ] + "Resource": ["*"], } statement = StatementDetail(this_statement) results = statement.permissions_management_actions_without_constraints @@ -316,9 +285,7 @@ def test_gh_193_has_resource_constraints(self): "Action": [ "ecr:GetAuthorizationToken", ], - "Resource": [ - "*" - ] + "Resource": ["*"], } statement = StatementDetail(this_statement) self.assertListEqual(statement.unrestrictable_actions, ["ecr:GetAuthorizationToken"]) diff --git a/test/scanning/test_trust_policies.py b/test/scanning/test_trust_policies.py index ecc9fafb..c19bfe0f 100644 --- a/test/scanning/test_trust_policies.py +++ b/test/scanning/test_trust_policies.py @@ -82,13 +82,15 @@ def test_assume_role_statement_principal_formats(self): assume_role_statement_07 = AssumeRoleStatement(statement07) assume_role_statement_08 = AssumeRoleStatement(statement08) - self.assertListEqual(assume_role_statement_02.principals, ['arn:aws:iam::012345678910:root']) - self.assertListEqual(assume_role_statement_03.principals, ['arn:aws:iam::012345678910:root']) - self.assertListEqual(assume_role_statement_04.principals, ['arn:aws:iam::012345678910:root']) - self.assertListEqual(assume_role_statement_05.principals, ['accounts.google.com']) - self.assertListEqual(assume_role_statement_06.principals, ['cognito-identity.amazonaws.com', 'www.amazon.com']) - self.assertListEqual(assume_role_statement_07.principals, ['arn:aws:iam::012345678910:root', 'lambda.amazonaws.com']) - self.assertListEqual(assume_role_statement_08.principals, ['lambda.amazonaws.com']) + self.assertListEqual(assume_role_statement_02.principals, ["arn:aws:iam::012345678910:root"]) + self.assertListEqual(assume_role_statement_03.principals, ["arn:aws:iam::012345678910:root"]) + self.assertListEqual(assume_role_statement_04.principals, ["arn:aws:iam::012345678910:root"]) + self.assertListEqual(assume_role_statement_05.principals, ["accounts.google.com"]) + self.assertListEqual(assume_role_statement_06.principals, ["cognito-identity.amazonaws.com", "www.amazon.com"]) + self.assertListEqual( + assume_role_statement_07.principals, ["arn:aws:iam::012345678910:root", "lambda.amazonaws.com"] + ) + self.assertListEqual(assume_role_statement_08.principals, ["lambda.amazonaws.com"]) self.assertListEqual(assume_role_statement_02.role_assumable_by_compute_services, []) self.assertListEqual(assume_role_statement_03.role_assumable_by_compute_services, []) diff --git a/test/scanning/test_user_detail_list.py b/test/scanning/test_user_detail_list.py index 5f9f6329..003a649d 100644 --- a/test/scanning/test_user_detail_list.py +++ b/test/scanning/test_user_detail_list.py @@ -46,18 +46,14 @@ def test_user_detail_attached_managed_policies(self): }, "path": "/", "customer_managed_policies": {}, - "aws_managed_policies": { - "ANPAI3R4QMOG6Q5A4VWVG": "AmazonRDSFullAccess" - }, - "is_excluded": False + "aws_managed_policies": {"ANPAI3R4QMOG6Q5A4VWVG": "AmazonRDSFullAccess"}, + "is_excluded": False, } }, "path": "/", "customer_managed_policies": {}, - "aws_managed_policies": { - "ANPAI6E2CYYMI4XI7AA5K": "AWSLambdaFullAccess" - }, - "is_excluded": False + "aws_managed_policies": {"ANPAI6E2CYYMI4XI7AA5K": "AWSLambdaFullAccess"}, + "is_excluded": False, } results = user_detail.json # print(json.dumps(results, indent=4)) diff --git a/test/shared/test_exclusions.py b/test/shared/test_exclusions.py index 893802ac..6858f960 100644 --- a/test/shared/test_exclusions.py +++ b/test/shared/test_exclusions.py @@ -8,56 +8,42 @@ class ExclusionsNewTestCase(unittest.TestCase): def test_new_exclusions_approach(self): exclusions_cfg = { - "policies": [ - "aws-service-role*" - ], + "policies": ["aws-service-role*"], "roles": ["aws-service-role*"], "users": [""], "include-actions": ["s3:GetObject"], - "exclude-actions": ["kms:Decrypt"] + "exclude-actions": ["kms:Decrypt"], } exclusions = Exclusions(exclusions_cfg) - test_actions_list = [ - "s3:GetObject", - "kms:decrypt", - "ssm:GetParameter", - "ec2:DescribeInstances" - ] + test_actions_list = ["s3:GetObject", "kms:decrypt", "ssm:GetParameter", "ec2:DescribeInstances"] result = exclusions.get_allowed_actions(test_actions_list) - self.assertCountEqual(result, ['s3:GetObject', 'ssm:GetParameter', 'ec2:DescribeInstances']) + self.assertCountEqual(result, ["s3:GetObject", "ssm:GetParameter", "ec2:DescribeInstances"]) class ExclusionsTestCase(unittest.TestCase): def test_exclusions_exact_match(self): """test_exclusions_exact_match: If there is an exact match in the exclusions list""" - exclusions_list = [ - "Beyonce" - ] + exclusions_list = ["Beyonce"] policy_name = "Beyonce" result = is_name_excluded(policy_name, exclusions_list) self.assertTrue(result) def test_exclusions_prefix_match(self): """test_exclusions_prefix_match: Test exclusions function with prefix wildcard logic.""" - exclusions_list = [ - "ThePerfectManDoesntExi*" - ] + exclusions_list = ["ThePerfectManDoesntExi*"] policy_name = "ThePerfectManDoesntExist" result = is_name_excluded(policy_name, exclusions_list) self.assertTrue(result) def test_exclusions_suffix_match(self): """test_exclusions_suffix_match: Test exclusions function with suffix wildcard logic.""" - exclusions_list = [ - "*ish" - ] + exclusions_list = ["*ish"] policy_name = "Secure-ish" result = is_name_excluded(policy_name, exclusions_list) self.assertTrue(result) class AuthorizationsFileComponentsExclusionsTestCase(unittest.TestCase): - def test_exclusions_for_service_roles(self): """test_exclusions_for_service_roles: Ensuring that exclusions config of service roles are specifically skipped, as designed""" @@ -76,21 +62,19 @@ def test_exclusions_for_service_roles(self): "Statement": [ { "Effect": "Allow", - "Principal": { - "Service": "cloudwatch-crossaccount.amazonaws.com" - }, - "Action": "sts:AssumeRole" + "Principal": {"Service": "cloudwatch-crossaccount.amazonaws.com"}, + "Action": "sts:AssumeRole", } - ] + ], }, "InstanceProfileList": [], "RolePolicyList": [], "AttachedManagedPolicies": [ { "PolicyName": "CloudWatch-CrossAccountAccess", - "PolicyArn": "arn:aws:iam::aws:policy/aws-service-role/CloudWatch-CrossAccountAccess" + "PolicyArn": "arn:aws:iam::aws:policy/aws-service-role/CloudWatch-CrossAccountAccess", } - ] + ], }, ], "Policies": [ @@ -111,31 +95,17 @@ def test_exclusions_for_service_roles(self): "Document": { "Version": "2012-10-17", # This is fake, I'm just trying to trigger a response - "Statement": [ - { - "Action": [ - "iam:*" - ], - "Resource": [ - "*" - ], - "Effect": "Allow" - } - ] + "Statement": [{"Action": ["iam:*"], "Resource": ["*"], "Effect": "Allow"}], }, "VersionId": "v1", "IsDefaultVersion": True, - "CreateDate": "2019-07-23 09:59:27+00:00" + "CreateDate": "2019-07-23 09:59:27+00:00", } - ] + ], } - ] - } - exclusions_cfg = { - "policies": [ - "aws-service-role*" - ] + ], } + exclusions_cfg = {"policies": ["aws-service-role*"]} exclusions = Exclusions(exclusions_cfg) authorization_details = AuthorizationDetails(authz_file, exclusions) results = authorization_details.results @@ -147,9 +117,7 @@ def test_exclusions_for_service_roles(self): "aws_managed_policies": {}, "customer_managed_policies": {}, "inline_policies": {}, - "exclusions": { - "policies": ["aws-service-role*"] - }, - "links": {} + "exclusions": {"policies": ["aws-service-role*"]}, + "links": {}, } self.assertDictEqual(results, expected_results) diff --git a/test/shared/test_utils.py b/test/shared/test_utils.py index 026c5cb6..d067216b 100644 --- a/test/shared/test_utils.py +++ b/test/shared/test_utils.py @@ -16,11 +16,7 @@ def test_remove_wildcard_only_actions(self): self.assertListEqual(results, ["secretsmanager:PutSecretValue"]) def test_remove_read_level_actions(self): - actions = [ - "ssm:GetParameters", - "ecr:PutImage" - ] + actions = ["ssm:GetParameters", "ecr:PutImage"] result = remove_read_level_actions(actions) - expected_result = ['ecr:PutImage'] + expected_result = ["ecr:PutImage"] self.assertListEqual(result, expected_result) - diff --git a/test/shared/test_validation.py b/test/shared/test_validation.py index dd3fe07a..b7103c28 100644 --- a/test/shared/test_validation.py +++ b/test/shared/test_validation.py @@ -24,9 +24,7 @@ def test_check_authorization_details_schema(self): def test_exclusions_error(self): """shared.validation.check_exclusions_schema: Make sure an exception is raised if the format is incorrect""" exclusions_cfg = { - "fake": [ - "MyRole" - ], + "fake": ["MyRole"], } with self.assertRaises(Exception): check_exclusions_schema(exclusions_cfg) diff --git a/utils/generate_example_iam_data.py b/utils/generate_example_iam_data.py index 89bf9112..a99884f5 100755 --- a/utils/generate_example_iam_data.py +++ b/utils/generate_example_iam_data.py @@ -3,13 +3,15 @@ import sys import os from pathlib import Path + sys.path.append(str(Path(os.path.dirname(__file__)).parent)) import json from cloudsplaining.shared.validation import check_authorization_details_schema from cloudsplaining.scan.authorization_details import AuthorizationDetails -account_authorization_details_file = os.path.abspath(os.path.join( +account_authorization_details_file = os.path.abspath( + os.path.join( os.path.dirname(__file__), os.path.pardir, "examples", @@ -21,7 +23,8 @@ with open(account_authorization_details_file) as json_file: account_authorization_details_cfg = json.load(json_file) -results_file = os.path.abspath(os.path.join( +results_file = os.path.abspath( + os.path.join( os.path.dirname(__file__), "example-iam-data.json", ) @@ -44,11 +47,9 @@ def generate_example_iam_data(): def replace_sample_data_js(results): - sample_data_js_file = os.path.abspath(os.path.join( - os.path.dirname(__file__), - os.path.pardir, - "cloudsplaining", "output", "src", "sampleData.js" - )) + sample_data_js_file = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.path.pardir, "cloudsplaining", "output", "src", "sampleData.js") + ) content = f"""var sample_iam_data = {json.dumps(results, indent=4)} @@ -63,7 +64,7 @@ def replace_sample_data_js(results): f.write(content) -if __name__ == '__main__': +if __name__ == "__main__": results = generate_example_iam_data() print("Replacing sampleData.js content with the most recent content") replace_sample_data_js(results) diff --git a/utils/generate_example_report.py b/utils/generate_example_report.py index a0de7b5d..32994ed8 100755 --- a/utils/generate_example_report.py +++ b/utils/generate_example_report.py @@ -2,6 +2,7 @@ import sys import os from pathlib import Path + sys.path.append(str(Path(os.path.dirname(__file__)).parent)) from cloudsplaining.output.report import HTMLReport from cloudsplaining.command.scan import scan_account_authorization_details @@ -22,7 +23,8 @@ # with open(account_authorization_details_file) as json_file: # account_authorization_details_cfg = json.load(json_file) -results_file = os.path.abspath(os.path.join( +results_file = os.path.abspath( + os.path.join( os.path.dirname(__file__), "example-iam-data.json", ) @@ -40,12 +42,7 @@ def generate_example_report(): # results_file, DEFAULT_EXCLUSIONS, account_name="example", output_directory=os.getcwd() # ) minimize = False - html_report = HTMLReport( - account_id=account_id, - account_name=account_name, - results=results, - minimize=minimize - ) + html_report = HTMLReport(account_id=account_id, account_name=account_name, results=results, minimize=minimize) rendered_report = html_report.get_html_report() # html_output_file = os.path.join(output_directory, f"index.html") @@ -69,5 +66,5 @@ def generate_example_report(): webbrowser.open(url, new=2) -if __name__ == '__main__': +if __name__ == "__main__": generate_example_report()