Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scan Multiple AWS accounts via AssumeRole #172

Merged
merged 10 commits into from
Apr 7, 2021
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ iam-findings-example.json
private/*
current.json
TODO.md
/private-multi-account-config.yml

venv
Pipfile.lock
Expand Down
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,22 @@ Cloudsplaining identifies violations of least privilege in AWS IAM policies and

## Installation

* Homebrew
#### Homebrew

```bash
brew tap salesforce/cloudsplaining https://github.com/salesforce/cloudsplaining
brew install cloudsplaining
```

* Pip3
#### Pip3

```bash
pip3 install --user cloudsplaining
```

* Now you should be able to execute `cloudsplaining` from command line by running `cloudsplaining --help`.

* Shell completion
#### Shell completion

To enable Bash completion, put this in your `.bashrc`:

Expand Down Expand Up @@ -279,6 +279,56 @@ Now when you run the `scan` command, you can use the exclusions file like this:
cloudsplaining scan --exclusions-file exclusions.yml --input-file examples/files/example.json --output examples/files/
```

### Scanning Multiple AWS Accounts

If your IAM user or IAM role has `sts:AssumeRole` permissions to a common IAM role across multiple AWS accounts, you can use the `scan-multi-account` command.

This diagram depicts how the process works:

![Diagram for scanning multiple AWS accounts with Cloudsplaining](docs/_images/scan-multiple-accounts.png)


> Note: If you are new to setting up cross-account access, check out [the official AWS Tutorial on Delegating access across AWS accounts using IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html). That can help you set up the architecture above.


* First, you'll need to create the multi-account config file. Run the following command:

```bash
cloudsplaining create-multi-account-config-file \
-o multi-account-config.yml
```

* This will generate a file called `multi-account-config.yml` with the following contents:

```yaml
accounts:
default_account: 123456789012
prod: 123456789013
test: 123456789014
```

!!! note
Observe how the format of the file above includes `account_name: accountID`. Edit the file contents to match your desired account name and account ID. Include as many account IDs as you like.


For the next step, let's say that:
* We have a role in the target accounts that is called `CommonSecurityRole`.
* The credentials for your IAM user are under the AWS Credentials profile called `scanning-user`.
* That user has `sts:AssumeRole` permissions to assume the `CommonSecurityRole` in all your target accounts specified in the YAML file we created previously.
* You want to save the output to an S3 bucket called `my-results-bucket`

Using the data above, you can run the following command:

```bash
cloudsplaining scan-multi-account \
-c multi-account-config.yml \
--profile scanning-user \
--role-name CommonSecurityRole \
--output-bucket my-results-bucket
```

> Note that if you run the above without the `--profile` flag, it will execute in the standard [AWS Credentials order of precedence](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default) (i.e., Environment variables, credentials profiles, ECS container credentials, then finally EC2 Instance Profile credentials).


## Cheatsheet

Expand Down Expand Up @@ -316,7 +366,7 @@ This is likely an issue with your PATH. Your PATH environment variable is not co
export PATH=$HOME/Library/Python/3.7/bin/:$PATH
```

**I followed the installation instructions but I am receiving a `ModuleNotFoundError` that says `No module named policy_sentry.analysis.expand`. What should I do?**
**I followed the installation instructions, but I am receiving a `ModuleNotFoundError` that says `No module named policy_sentry.analysis.expand`. What should I do?**

Try upgrading to the latest version of Cloudsplaining. This error was fixed in version 0.0.10.

Expand Down
47 changes: 47 additions & 0 deletions cloudsplaining/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,50 @@
def change_log_level(log_level):
""""Change log level of module logger"""
logger.setLevel(log_level)


def set_stream_logger(name="cloudsplaining", level=logging.DEBUG, format_string=None): # pylint: disable=redefined-outer-name
"""
Add a stream handler for the given name and level to the logging module.
By default, this logs all cloudsplaining messages to ``stdout``.
>>> import cloudsplaining
>>> cloudsplaining.set_stream_logger('cloudsplaining.scan', logging.INFO)
:type name: string
:param name: Log name
:type level: int
:param level: Logging level, e.g. ``logging.INFO``
:type format_string: str
:param format_string: Log message format
"""
# remove existing handlers. since NullHandler is added by default
handlers = logging.getLogger(name).handlers
for handler in handlers: # pylint: disable=redefined-outer-name
logging.getLogger(name).removeHandler(handler)
if format_string is None:
format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s"
logger = logging.getLogger(name) # pylint: disable=redefined-outer-name
logger.setLevel(level)
handler = logging.StreamHandler() # pylint: disable=redefined-outer-name
handler.setLevel(level)
formatter = logging.Formatter(format_string) # pylint: disable=redefined-outer-name
handler.setFormatter(formatter)
logger.addHandler(handler)


def set_log_level(verbose):
"""
Set Log Level based on click's count argument.

Default log level to critical; otherwise, set to: warning for -v, info for -vv, debug for -vvv

:param verbose: integer for verbosity count.
:return:
"""
if verbose == 1:
set_stream_logger(level=getattr(logging, "WARNING"))
elif verbose == 2:
set_stream_logger(level=getattr(logging, "INFO"))
elif verbose >= 3:
set_stream_logger(level=getattr(logging, "DEBUG"))
else:
set_stream_logger(level=getattr(logging, "CRITICAL"))
2 changes: 2 additions & 0 deletions cloudsplaining/bin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ def cloudsplaining():


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.expand_policy.expand_policy)
cloudsplaining.add_command(command.scan.scan)
cloudsplaining.add_command(command.scan_multi_account.scan_multi_account)
cloudsplaining.add_command(command.scan_policy_file.scan_policy_file)
cloudsplaining.add_command(command.download.download)

Expand Down
2 changes: 1 addition & 1 deletion cloudsplaining/bin/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# pylint: disable=missing-module-docstring
__version__ = "0.3.2"
__version__ = "0.4.0"
2 changes: 2 additions & 0 deletions cloudsplaining/command/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# pylint: disable=missing-module-docstring
from cloudsplaining.command import create_exclusions_file
from cloudsplaining.command import create_multi_account_config_file
from cloudsplaining.command import expand_policy
from cloudsplaining.command import download
from cloudsplaining.command import scan
from cloudsplaining.command import scan_multi_account
from cloudsplaining.command import scan_policy_file
3 changes: 2 additions & 1 deletion cloudsplaining/command/create_exclusions_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import click
from cloudsplaining.shared.constants import EXCLUSIONS_TEMPLATE
from cloudsplaining import change_log_level
from cloudsplaining.shared import utils

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -48,7 +49,7 @@ def create_exclusions_file(output_file, verbose):
with open(filename, "a") as file_obj:
for line in EXCLUSIONS_TEMPLATE:
file_obj.write(line)
print(f"Exclusions template file written to: {filename}")
utils.print_green(f"Success! Exclusions template file written to: {filename}")
print(
"Make sure you download your account authorization details before running the scan. Set your AWS access keys as environment variables then run: "
)
Expand Down
65 changes: 65 additions & 0 deletions cloudsplaining/command/create_multi_account_config_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
Create YML Template files for the exclusions template command.
This way, users don't have to remember exactly how to phrase the yaml files, since this command creates it for them.
"""
# Copyright (c) 2020, salesforce.com, inc.
# All rights reserved.
# Licensed under the BSD 3-Clause license.
# For full license text, see the LICENSE file in the repo root
# or https://opensource.org/licenses/BSD-3-Clause
import os
from pathlib import Path
import logging
import click
from cloudsplaining.shared.constants import MULTI_ACCOUNT_CONFIG_TEMPLATE
from cloudsplaining import change_log_level
from cloudsplaining.shared import utils

logger = logging.getLogger(__name__)
OK_GREEN = "\033[92m"
END = "\033[0m"


@click.command(
context_settings=dict(max_content_width=160),
short_help="Creates a YML file to be used for multi-account scanning",
)
@click.option(
"--output-file",
"-o",
"output_file",
type=click.Path(exists=False),
default=os.path.join(os.getcwd(), "multi-account-config.yml"),
required=True,
help="Relative path to output file where we want to store the multi account config template.",
)
@click.option(
"--verbose",
"-v",
type=click.Choice(
["critical", "error", "warning", "info", "debug"], case_sensitive=False
),
)
def create_multi_account_config_file(output_file, verbose):
"""
Creates a YML file to be used as a multi-account config template, so users can scan many different accounts.
"""
if verbose:
log_level = getattr(logging, verbose.upper())
change_log_level(log_level)
filename = Path(output_file).resolve()

if os.path.exists(filename):
logger.debug("%s exists. Removing the file and replacing its contents.", filename)
os.remove(filename)

with open(filename, "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(filename)}")
print(
f"\nMake sure you edit the {os.path.relpath(filename)} file and then run the scan-multi-account command, as shown below."
)
print(
f"\n\tcloudsplaining scan-multi-account --exclusions-file exclusions.yml -c {os.path.relpath(filename)} -o ./"
)
Loading