Skip to content

Commit ed9c1ee

Browse files
authored
Merge pull request #14 from kmcquade/fix/GH-10-add-recursive-scan-option
Added recursive scanning option - and fixed exclude-actions
2 parents dfe3c22 + fa4cb75 commit ed9c1ee

24 files changed

+155
-113
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 0.0.6 (2020-05-01)
4+
* Fix `exclude-actions` in the exclusions file - it was not being respected before.
5+
* Add a recursive scanning option.
6+
37
## 0.0.4 (2020-05-01)
48
* Provide option to skip opening HTML report (`--skip-open-report`)
59
* Provide report indicator on whether it is assumable by compute services

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ This will generate a file in your current directory titled `exclusions.yml`.
141141
Now when you run the `scan` command, you can use the exclusions file like this:
142142

143143
```bash
144-
cloudsplaining scan --exclusions-file exclusions.yml --file examples/files/example.json --output examples/files/
144+
cloudsplaining scan --exclusions-file exclusions.yml --input examples/files/example.json --output examples/files/
145145
```
146146

147147
For more information on the structure of the exclusions file, see [Filtering False Positives](#filtering-false-positives)
@@ -153,7 +153,7 @@ Now that we've downloaded the account authorization file, we can scan *all* of t
153153
Run the following command:
154154

155155
```bash
156-
cloudsplaining scan --exclusions-file exclusions.yml --file examples/files/example.json --output examples/files/
156+
cloudsplaining scan --exclusions-file exclusions.yml --input examples/files/example.json --output examples/files/
157157
```
158158

159159
It will create an HTML report like [this](https://opensource.salesforce.com/cloudsplaining/):
@@ -243,7 +243,7 @@ exclude-actions:
243243
Now when you run the `scan` command, you can use the exclusions file like this:
244244

245245
```bash
246-
cloudsplaining scan --exclusions-file exclusions.yml --file examples/files/example.json --output examples/files/
246+
cloudsplaining scan --exclusions-file exclusions.yml --input examples/files/example.json --output examples/files/
247247
```
248248

249249

@@ -252,7 +252,7 @@ cloudsplaining scan --exclusions-file exclusions.yml --file examples/files/examp
252252
You can also scan a single policy file to identify risks instead of an entire account.
253253

254254
```bash
255-
cloudsplaining scan-policy-file --file examples/policies/explicit-actions.json
255+
cloudsplaining scan-policy-file --input examples/policies/explicit-actions.json
256256
```
257257

258258
The output will include a finding description and a list of the IAM actions that do not leverage resource constraints.
@@ -283,13 +283,13 @@ cloudsplaining download --profile someprofile
283283
cloudsplaining download --profile all
284284
285285
# Scan Authorization details
286-
cloudsplaining scan --file default.json
286+
cloudsplaining scan --input default.json
287287
# Scan Authorization details with custom exclusions
288-
cloudsplaining scan --file default.json --exclusions-file exclusions.yml
288+
cloudsplaining scan --input default.json --exclusions-file exclusions.yml
289289
290290
# Scan Policy Files
291-
cloudsplaining scan-policy-file --file examples/policies/wildcards.json
292-
cloudsplaining scan-policy-file --file examples/policies/wildcards.json --exclusions-file examples/example-exclusions.yml
291+
cloudsplaining scan-policy-file --input examples/policies/wildcards.json
292+
cloudsplaining scan-policy-file --input examples/policies/wildcards.json --exclusions-file examples/example-exclusions.yml
293293
```
294294

295295
## FAQ

cloudsplaining/bin/cloudsplaining

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88
Cloudsplaining is an AWS IAM Assessment tool that identifies violations of least privilege and generates a risk-prioritized HTML report with a triage worksheet.
99
"""
10-
__version__ = "0.0.5"
10+
__version__ = "0.0.6"
1111
import click
1212
from cloudsplaining import command
1313

cloudsplaining/command/create_exclusions_file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ def create_exclusions_file(output_file):
4646
)
4747
print("\tcloudsplaining download")
4848
print("You can use this with the scan command as shown below: ")
49-
print("\tcloudsplaining scan --exclusions-file exclusions.yml --file default.json")
49+
print("\tcloudsplaining scan --exclusions-file exclusions.yml --input default.json")

cloudsplaining/command/expand_policy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@
2020
short_help="Expand the * Actions in IAM policy files to improve readability"
2121
)
2222
@click.option(
23-
"--file",
23+
"--input",
2424
type=click.Path(exists=True),
2525
required=True,
2626
help="Path to the JSON policy file.",
2727
)
2828
@click_log.simple_verbosity_option(logger)
29-
def expand_policy(file):
29+
def expand_policy(input): # pylint: disable=redefined-builtin
3030
"""
3131
Expand the * Actions in IAM policy files to improve readability
3232
"""
33+
file = input
3334
with open(file) as json_file:
3435
logger.debug(f"Opening {file}")
3536
data = json.load(json_file)

cloudsplaining/command/scan.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"IAM security posture. "
3232
)
3333
@click.option(
34-
"--file",
34+
"--input",
3535
type=click.Path(exists=True),
3636
required=True,
3737
help="Path of IAM account authorization details file",
@@ -66,7 +66,7 @@
6666
"This helps when running the report in automation.",
6767
)
6868
@click_log.simple_verbosity_option()
69-
def scan(file, exclusions_file, output, all_access_levels, skip_open_report):
69+
def scan(input, exclusions_file, output, all_access_levels, skip_open_report): # pylint: disable=redefined-builtin
7070
"""
7171
Given the path to account authorization details files and the exclusions config file, scan all inline and
7272
managed policies in the account to identify actions that do not leverage resource constraints.
@@ -78,8 +78,22 @@ def scan(file, exclusions_file, output, all_access_levels, skip_open_report):
7878
except yaml.YAMLError as exc:
7979
logger.critical(exc)
8080
check_exclusions_schema(exclusions_cfg)
81+
if os.path.isfile(input):
82+
scan_account_authorization_file(input, exclusions_cfg, output, all_access_levels, skip_open_report)
83+
if os.path.isdir(input):
84+
logger.info("The path given is a directory. Scanning for account authorization files and generating report.")
85+
input_files = get_authorization_files_in_directory(input)
86+
for file in input_files:
87+
logger.info(f"Scanning file: {file}")
88+
scan_account_authorization_file(file, exclusions_cfg, output, all_access_levels, skip_open_report)
8189

82-
with open(file) as f:
90+
91+
def scan_account_authorization_file(input_file, exclusions_cfg, output, all_access_levels, skip_open_report):
92+
"""
93+
Given the path to account authorization details files and the exclusions config file, scan all inline and
94+
managed policies in the account to identify actions that do not leverage resource constraints.
95+
"""
96+
with open(input_file) as f:
8397
contents = f.read()
8498
account_authorization_details_cfg = json.loads(contents)
8599
check_authorization_details_schema(account_authorization_details_cfg)
@@ -104,7 +118,7 @@ def scan(file, exclusions_file, output, all_access_levels, skip_open_report):
104118
exclusions_cfg, modify_only=True
105119
)
106120

107-
account_name = Path(file).stem
121+
account_name = Path(input_file).stem
108122

109123
# Lazy method to get an account ID
110124
account_id = None
@@ -139,3 +153,21 @@ def scan(file, exclusions_file, output, all_access_levels, skip_open_report):
139153
exclusions_cfg,
140154
skip_open_report=skip_open_report,
141155
)
156+
157+
158+
def get_authorization_files_in_directory(directory):
159+
"""Get a list of download-account-authorization-files in a directory"""
160+
file_list = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
161+
file_list_with_full_path = []
162+
for file in file_list:
163+
if file.endswith(".json"):
164+
file_list_with_full_path.append(os.path.abspath(os.path.join(directory, file)))
165+
new_file_list = []
166+
for file in file_list_with_full_path:
167+
with open(file) as f:
168+
contents = f.read()
169+
account_authorization_details_cfg = json.loads(contents)
170+
valid_schema = check_authorization_details_schema(account_authorization_details_cfg)
171+
if valid_schema:
172+
new_file_list.append(file)
173+
return new_file_list

cloudsplaining/command/scan_policy_file.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
short_help="Scan a single policy file to identify identify missing resource constraints."
3030
)
3131
@click.option(
32-
"--file",
32+
"--input",
3333
type=click.Path(exists=True),
3434
required=True,
3535
help="Path of to the IAM policy file.",
@@ -50,9 +50,9 @@
5050
" (Resource Exposure, Privilege Escalation, Data Exfiltration). This can help with prioritization.",
5151
)
5252
@click_log.simple_verbosity_option(logger)
53-
def scan_policy_file(file, exclusions_file, high_priority_only):
53+
def scan_policy_file(input, exclusions_file, high_priority_only): # pylint: disable=redefined-builtin
5454
"""Scan a single policy file to identify missing resource constraints."""
55-
55+
file = input
5656
# Get the exclusions configuration
5757
with open(exclusions_file, "r") as yaml_file:
5858
try:
@@ -129,7 +129,7 @@ def scan_policy(policy_json, policy_name, exclusions_cfg=DEFAULT_EXCLUSIONS_CONF
129129
results_placeholder = []
130130
for action in actions_missing_resource_constraints:
131131
if excluded_actions:
132-
if not is_name_excluded(action.lower, excluded_actions):
132+
if not is_name_excluded(action.lower(), excluded_actions):
133133
results_placeholder.append(action)
134134
else:
135135
results_placeholder.append(action)

cloudsplaining/output/findings.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
# or https://opensource.org/licenses/BSD-3-Clause
77
import logging
88
from policy_sentry.util.arns import get_account_from_arn, get_resource_string
9-
from cloudsplaining.shared.utils import capitalize_first_character
109
from cloudsplaining.shared.constants import READ_ONLY_DATA_LEAK_ACTIONS
10+
from cloudsplaining.shared.exclusions import is_name_excluded
11+
from cloudsplaining.shared.utils import capitalize_first_character
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -23,9 +24,13 @@ def __init__(self):
2324
def add(self, finding):
2425
"""Add a finding to the list."""
2526
if isinstance(finding, list):
26-
self.findings.extend(finding)
27+
for a_finding in finding:
28+
# Only add it if there are any actions after processing exclusions
29+
if a_finding.actions:
30+
self.findings.append(finding)
2731
elif isinstance(finding, Finding):
28-
self.findings.append(finding)
32+
if finding.actions:
33+
self.findings.append(finding)
2934

3035
@property
3136
def json(self):
@@ -49,13 +54,29 @@ def __init__(
4954
actions,
5055
policy_document,
5156
assume_role_policy_document=None,
57+
always_exclude_actions=None,
5258
):
5359
self.policy_name = policy_name
5460
self.arn = arn
55-
self.actions = actions
61+
# self.actions = actions
62+
self.always_exclude_actions = always_exclude_actions
63+
self.actions = self._actions(actions)
5664
self.policy_document = policy_document
5765
self.assume_role_policy_document = assume_role_policy_document
5866

67+
def _actions(self, actions):
68+
results = []
69+
if self.always_exclude_actions:
70+
for action in actions:
71+
if is_name_excluded(action.lower(), self.always_exclude_actions):
72+
# logger.info("Excluded action: %s" % action)
73+
pass
74+
else:
75+
results.append(action)
76+
return results
77+
else:
78+
return actions
79+
5980
@property
6081
def managed_by(self):
6182
"""Determine whether the policy is AWS-Managed or Customer-managed based on a Policy ARN pattern."""
@@ -110,8 +131,17 @@ def role_assumable_by_compute_services(self):
110131
@property
111132
def permissions_management_actions_without_constraints(self):
112133
"""Return a list of actions that could cause resource exposure via actions at the 'Permissions management'
113-
access level, if applicable. """
114-
return self.policy_document.permissions_management_without_constraints
134+
access level, if applicable."""
135+
results = []
136+
if self.always_exclude_actions:
137+
for action in self.policy_document.permissions_management_without_constraints:
138+
if is_name_excluded(action.lower(), self.always_exclude_actions):
139+
pass
140+
else:
141+
results.append(action)
142+
return results
143+
else:
144+
return self.policy_document.permissions_management_without_constraints
115145

116146
@property
117147
def privilege_escalation(self):
@@ -148,7 +178,7 @@ def json(self):
148178
"DataExfiltrationActions": self.data_leak_actions,
149179
"PermissionsManagementActions": self.permissions_management_actions_without_constraints,
150180
# Separate the "Write" and "Tagging" actions in the machine-readable output only
151-
"WriteActions": self.policy_document.write_actions_without_constraints,
152-
"TaggingActions": self.policy_document.tagging_actions_without_constraints,
181+
# "WriteActions": self.policy_document.write_actions_without_constraints,
182+
# "TaggingActions": self.policy_document.tagging_actions_without_constraints,
153183
}
154184
return result

cloudsplaining/output/templates/guidance/2-triage-guidance.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ To recap: you've followed these steps to generate this report:
3535
<ul><li><code>cloudsplaining create-exclusions-file --output-file exclusions.yml</code></li></ul>
3636
<li>Generated the report</li>
3737
<ul>
38-
<li><code>cloudsplaining scan --file default-account-details.json --exclusions-file exclusions.yml</code></li>
38+
<li><code>cloudsplaining scan --input default-account-details.json --exclusions-file exclusions.yml</code></li>
3939
<li>This generates three files: (1) The single-file HTML report, (2) The triage CSV worksheet, and (3) The raw JSON data file</li>
4040
</ul>
4141
</ul>
@@ -149,7 +149,7 @@ Add whatever values you want to the above depending on your organization's conte
149149
* Now, run the scan to generate a *new* Cloudsplaining report that considers your exclusions criteria. This way, you are working with a report version that consists of True Positives only.
150150

151151
```bash
152-
cloudsplaining scan --file default.json --exclusions-file exclusions.yml
152+
cloudsplaining scan --input default.json --exclusions-file exclusions.yml
153153
```
154154

155155
You can now proceed to the Remediation stage.

cloudsplaining/output/templates/guidance/4-validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ After you've rewritten your IAM policy, we suggest two options for validating th
1313

1414
You can validate that your remediated policy passes Cloudsplaining by running the following command:
1515

16-
```cloudsplaining scan-policy-file --file policy.json --exclusions-file exclusions.yml```
16+
```cloudsplaining scan-policy-file --input policy.json --exclusions-file exclusions.yml```
1717

1818
When there are no more results, it passes!
1919

0 commit comments

Comments
 (0)