Skip to content

Commit af95f5e

Browse files
authored
Scan Multiple AWS accounts via AssumeRole (#172)
Add `create-multi-account-config-file` and `scan-multi-account` commands.
1 parent 4505fda commit af95f5e

21 files changed

+600
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ iam-findings-example.json
1313
private/*
1414
current.json
1515
TODO.md
16+
/private-multi-account-config.yml
1617

1718
venv
1819
Pipfile.lock

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,22 +80,22 @@ Cloudsplaining identifies violations of least privilege in AWS IAM policies and
8080

8181
## Installation
8282

83-
* Homebrew
83+
#### Homebrew
8484

8585
```bash
8686
brew tap salesforce/cloudsplaining https://github.com/salesforce/cloudsplaining
8787
brew install cloudsplaining
8888
```
8989

90-
* Pip3
90+
#### Pip3
9191

9292
```bash
9393
pip3 install --user cloudsplaining
9494
```
9595

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

98-
* Shell completion
98+
#### Shell completion
9999

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

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

282+
### Scanning Multiple AWS Accounts
283+
284+
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.
285+
286+
This diagram depicts how the process works:
287+
288+
![Diagram for scanning multiple AWS accounts with Cloudsplaining](docs/_images/scan-multiple-accounts.png)
289+
290+
291+
> 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.
292+
293+
294+
* First, you'll need to create the multi-account config file. Run the following command:
295+
296+
```bash
297+
cloudsplaining create-multi-account-config-file \
298+
-o multi-account-config.yml
299+
```
300+
301+
* This will generate a file called `multi-account-config.yml` with the following contents:
302+
303+
```yaml
304+
accounts:
305+
default_account: 123456789012
306+
prod: 123456789013
307+
test: 123456789014
308+
```
309+
310+
!!! note
311+
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.
312+
313+
314+
For the next step, let's say that:
315+
* We have a role in the target accounts that is called `CommonSecurityRole`.
316+
* The credentials for your IAM user are under the AWS Credentials profile called `scanning-user`.
317+
* That user has `sts:AssumeRole` permissions to assume the `CommonSecurityRole` in all your target accounts specified in the YAML file we created previously.
318+
* You want to save the output to an S3 bucket called `my-results-bucket`
319+
320+
Using the data above, you can run the following command:
321+
322+
```bash
323+
cloudsplaining scan-multi-account \
324+
-c multi-account-config.yml \
325+
--profile scanning-user \
326+
--role-name CommonSecurityRole \
327+
--output-bucket my-results-bucket
328+
```
329+
330+
> 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).
331+
282332

283333
## Cheatsheet
284334

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

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

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

cloudsplaining/__init__.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,50 @@
2424
def change_log_level(log_level):
2525
""""Change log level of module logger"""
2626
logger.setLevel(log_level)
27+
28+
29+
def set_stream_logger(name="cloudsplaining", level=logging.DEBUG, format_string=None): # pylint: disable=redefined-outer-name
30+
"""
31+
Add a stream handler for the given name and level to the logging module.
32+
By default, this logs all cloudsplaining messages to ``stdout``.
33+
>>> import cloudsplaining
34+
>>> cloudsplaining.set_stream_logger('cloudsplaining.scan', logging.INFO)
35+
:type name: string
36+
:param name: Log name
37+
:type level: int
38+
:param level: Logging level, e.g. ``logging.INFO``
39+
:type format_string: str
40+
:param format_string: Log message format
41+
"""
42+
# remove existing handlers. since NullHandler is added by default
43+
handlers = logging.getLogger(name).handlers
44+
for handler in handlers: # pylint: disable=redefined-outer-name
45+
logging.getLogger(name).removeHandler(handler)
46+
if format_string is None:
47+
format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s"
48+
logger = logging.getLogger(name) # pylint: disable=redefined-outer-name
49+
logger.setLevel(level)
50+
handler = logging.StreamHandler() # pylint: disable=redefined-outer-name
51+
handler.setLevel(level)
52+
formatter = logging.Formatter(format_string) # pylint: disable=redefined-outer-name
53+
handler.setFormatter(formatter)
54+
logger.addHandler(handler)
55+
56+
57+
def set_log_level(verbose):
58+
"""
59+
Set Log Level based on click's count argument.
60+
61+
Default log level to critical; otherwise, set to: warning for -v, info for -vv, debug for -vvv
62+
63+
:param verbose: integer for verbosity count.
64+
:return:
65+
"""
66+
if verbose == 1:
67+
set_stream_logger(level=getattr(logging, "WARNING"))
68+
elif verbose == 2:
69+
set_stream_logger(level=getattr(logging, "INFO"))
70+
elif verbose >= 3:
71+
set_stream_logger(level=getattr(logging, "DEBUG"))
72+
else:
73+
set_stream_logger(level=getattr(logging, "CRITICAL"))

cloudsplaining/bin/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ def cloudsplaining():
2121

2222

2323
cloudsplaining.add_command(command.create_exclusions_file.create_exclusions_file)
24+
cloudsplaining.add_command(command.create_multi_account_config_file.create_multi_account_config_file)
2425
cloudsplaining.add_command(command.expand_policy.expand_policy)
2526
cloudsplaining.add_command(command.scan.scan)
27+
cloudsplaining.add_command(command.scan_multi_account.scan_multi_account)
2628
cloudsplaining.add_command(command.scan_policy_file.scan_policy_file)
2729
cloudsplaining.add_command(command.download.download)
2830

cloudsplaining/bin/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# pylint: disable=missing-module-docstring
2-
__version__ = "0.3.2"
2+
__version__ = "0.4.0"

cloudsplaining/command/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# pylint: disable=missing-module-docstring
22
from cloudsplaining.command import create_exclusions_file
3+
from cloudsplaining.command import create_multi_account_config_file
34
from cloudsplaining.command import expand_policy
45
from cloudsplaining.command import download
56
from cloudsplaining.command import scan
7+
from cloudsplaining.command import scan_multi_account
68
from cloudsplaining.command import scan_policy_file

cloudsplaining/command/create_exclusions_file.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import click
1414
from cloudsplaining.shared.constants import EXCLUSIONS_TEMPLATE
1515
from cloudsplaining import change_log_level
16+
from cloudsplaining.shared import utils
1617

1718
logger = logging.getLogger(__name__)
1819

@@ -48,7 +49,7 @@ def create_exclusions_file(output_file, verbose):
4849
with open(filename, "a") as file_obj:
4950
for line in EXCLUSIONS_TEMPLATE:
5051
file_obj.write(line)
51-
print(f"Exclusions template file written to: {filename}")
52+
utils.print_green(f"Success! Exclusions template file written to: {filename}")
5253
print(
5354
"Make sure you download your account authorization details before running the scan. Set your AWS access keys as environment variables then run: "
5455
)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Create YML Template files for the exclusions template command.
3+
This way, users don't have to remember exactly how to phrase the yaml files, since this command creates it for them.
4+
"""
5+
# Copyright (c) 2020, salesforce.com, inc.
6+
# All rights reserved.
7+
# Licensed under the BSD 3-Clause license.
8+
# For full license text, see the LICENSE file in the repo root
9+
# or https://opensource.org/licenses/BSD-3-Clause
10+
import os
11+
from pathlib import Path
12+
import logging
13+
import click
14+
from cloudsplaining.shared.constants import MULTI_ACCOUNT_CONFIG_TEMPLATE
15+
from cloudsplaining import change_log_level
16+
from cloudsplaining.shared import utils
17+
18+
logger = logging.getLogger(__name__)
19+
OK_GREEN = "\033[92m"
20+
END = "\033[0m"
21+
22+
23+
@click.command(
24+
context_settings=dict(max_content_width=160),
25+
short_help="Creates a YML file to be used for multi-account scanning",
26+
)
27+
@click.option(
28+
"--output-file",
29+
"-o",
30+
"output_file",
31+
type=click.Path(exists=False),
32+
default=os.path.join(os.getcwd(), "multi-account-config.yml"),
33+
required=True,
34+
help="Relative path to output file where we want to store the multi account config template.",
35+
)
36+
@click.option(
37+
"--verbose",
38+
"-v",
39+
type=click.Choice(
40+
["critical", "error", "warning", "info", "debug"], case_sensitive=False
41+
),
42+
)
43+
def create_multi_account_config_file(output_file, verbose):
44+
"""
45+
Creates a YML file to be used as a multi-account config template, so users can scan many different accounts.
46+
"""
47+
if verbose:
48+
log_level = getattr(logging, verbose.upper())
49+
change_log_level(log_level)
50+
filename = Path(output_file).resolve()
51+
52+
if os.path.exists(filename):
53+
logger.debug("%s exists. Removing the file and replacing its contents.", filename)
54+
os.remove(filename)
55+
56+
with open(filename, "a") as file_obj:
57+
for line in MULTI_ACCOUNT_CONFIG_TEMPLATE:
58+
file_obj.write(line)
59+
utils.print_green(f"Success! Multi-account config file written to: {os.path.relpath(filename)}")
60+
print(
61+
f"\nMake sure you edit the {os.path.relpath(filename)} file and then run the scan-multi-account command, as shown below."
62+
)
63+
print(
64+
f"\n\tcloudsplaining scan-multi-account --exclusions-file exclusions.yml -c {os.path.relpath(filename)} -o ./"
65+
)

0 commit comments

Comments
 (0)