Skip to content

Commit d74fdec

Browse files
authored
Merge pull request #37 from kmcquade/refactor/granular-exclusions-v4
Granular Exclusions, UI Uplift, track group membership
2 parents 30992c3 + c4cc1e2 commit d74fdec

36 files changed

+6986
-18080
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Unreleased
44
* Docker
55

6+
## 0.1.0 (2020-05-11)
7+
* Granular exclusions: Fixed issue where exclusions file was including dangling policies in the results (Fixes #33)
8+
* Changed IAM Principals table so that the principals can be sorted according to their risks. This will really help with pentesting
9+
610
## 0.0.14 (2020-05-07)
711
* Fix issue where Data Exposure tallies were not showing up in the AWS Managed table correctly.
812

Pipfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ bandit = "==1.6.2"
1717
mkdocs = "==1.1"
1818

1919
[packages]
20-
policy_sentry = "==0.8.0.5"
20+
policy_sentry = "==0.8.0.6"
2121
click = "==7.0"
2222
schema = "==0.7.1"
2323
boto3 = "*"

cloudsplaining/bin/cli.py

+1-1
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.14"
10+
__version__ = "0.1.0"
1111
import click
1212
from cloudsplaining import command
1313

cloudsplaining/command/scan.py

+56-18
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414
import click
1515
import click_log
1616
from cloudsplaining.shared.constants import EXCLUSIONS_FILE
17-
from cloudsplaining.shared.validation import (
18-
check_exclusions_schema,
19-
check_authorization_details_schema,
20-
)
17+
from cloudsplaining.shared.validation import check_authorization_details_schema
18+
from cloudsplaining.shared.exclusions import Exclusions, DEFAULT_EXCLUSIONS
2119
from cloudsplaining.scan.authorization_details import AuthorizationDetails
2220
from cloudsplaining.output.html_report import generate_html_report
2321
from cloudsplaining.output.data_file import write_results_data_file
@@ -67,29 +65,43 @@
6765
)
6866
@click_log.simple_verbosity_option()
6967
# pylint: disable=redefined-builtin
70-
def scan(input, exclusions_file, output, all_access_levels, skip_open_report): # pragma: no cover
68+
def scan(
69+
input, exclusions_file, output, all_access_levels, skip_open_report
70+
): # pragma: no cover
7171
"""
7272
Given the path to account authorization details files and the exclusions config file, scan all inline and
7373
managed policies in the account to identify actions that do not leverage resource constraints.
7474
"""
7575
if exclusions_file:
76+
# Get the exclusions configuration
7677
with open(exclusions_file, "r") as yaml_file:
7778
try:
7879
exclusions_cfg = yaml.safe_load(yaml_file)
7980
except yaml.YAMLError as exc:
8081
logger.critical(exc)
81-
check_exclusions_schema(exclusions_cfg)
82+
exclusions = Exclusions(exclusions_cfg)
83+
else:
84+
exclusions = DEFAULT_EXCLUSIONS
85+
8286
if os.path.isfile(input):
83-
scan_account_authorization_file(input, exclusions_cfg, output, all_access_levels, skip_open_report)
87+
scan_account_authorization_file(
88+
input, exclusions, output, all_access_levels, skip_open_report
89+
)
8490
if os.path.isdir(input):
85-
logger.info("The path given is a directory. Scanning for account authorization files and generating report.")
91+
logger.info(
92+
"The path given is a directory. Scanning for account authorization files and generating report."
93+
)
8694
input_files = get_authorization_files_in_directory(input)
8795
for file in input_files:
8896
logger.info(f"Scanning file: {file}")
89-
scan_account_authorization_file(file, exclusions_cfg, output, all_access_levels, skip_open_report)
97+
scan_account_authorization_file(
98+
file, exclusions, output, all_access_levels, skip_open_report
99+
)
90100

91101

92-
def scan_account_authorization_file(input_file, exclusions_cfg, output, all_access_levels, skip_open_report): # pragma: no cover
102+
def scan_account_authorization_file(
103+
input_file, exclusions, output, all_access_levels, skip_open_report
104+
): # pragma: no cover
93105
"""
94106
Given the path to account authorization details files and the exclusions config file, scan all inline and
95107
managed policies in the account to identify actions that do not leverage resource constraints.
@@ -107,7 +119,7 @@ def scan_account_authorization_file(input_file, exclusions_cfg, output, all_acce
107119
)
108120
authorization_details = AuthorizationDetails(account_authorization_details_cfg)
109121
results = authorization_details.missing_resource_constraints(
110-
exclusions_cfg, modify_only=False
122+
exclusions, modify_only=False
111123
)
112124
else:
113125
logger.debug(
@@ -116,10 +128,26 @@ def scan_account_authorization_file(input_file, exclusions_cfg, output, all_acce
116128
)
117129
authorization_details = AuthorizationDetails(account_authorization_details_cfg)
118130
results = authorization_details.missing_resource_constraints(
119-
exclusions_cfg, modify_only=True
131+
exclusions, modify_only=True
120132
)
121133

122134
principal_policy_mapping = authorization_details.principal_policy_mapping
135+
for principal_policy_entry in principal_policy_mapping:
136+
for finding_result in results:
137+
if (
138+
principal_policy_entry.get("PolicyName").lower()
139+
== finding_result.get("PolicyName").lower()
140+
):
141+
principal_policy_entry["Actions"] = len(finding_result["Actions"])
142+
principal_policy_entry["PrivilegeEscalation"] = len(
143+
finding_result["PrivilegeEscalation"]
144+
)
145+
principal_policy_entry["DataExfiltrationActions"] = len(
146+
finding_result["DataExfiltrationActions"]
147+
)
148+
principal_policy_entry["PermissionsManagementActions"] = len(
149+
finding_result["PermissionsManagementActions"]
150+
)
123151

124152
account_name = Path(input_file).stem
125153

@@ -149,8 +177,12 @@ def scan_account_authorization_file(input_file, exclusions_cfg, output, all_acce
149177
print(f"Raw data file saved: {str(raw_data_filepath)}")
150178

151179
# Principal policy mapping
152-
principal_policy_mapping_file = os.path.join(output, f"iam-principals-{account_name}.json")
153-
principal_policy_mapping_filepath = write_results_data_file(principal_policy_mapping, principal_policy_mapping_file)
180+
principal_policy_mapping_file = os.path.join(
181+
output, f"iam-principals-{account_name}.json"
182+
)
183+
principal_policy_mapping_filepath = write_results_data_file(
184+
principal_policy_mapping, principal_policy_mapping_file
185+
)
154186
print(f"Principals data file saved: {str(principal_policy_mapping_filepath)}")
155187

156188
print("Creating the HTML Report")
@@ -159,24 +191,30 @@ def scan_account_authorization_file(input_file, exclusions_cfg, output, all_acce
159191
results,
160192
principal_policy_mapping,
161193
output_directory,
162-
exclusions_cfg,
194+
exclusions.config,
163195
skip_open_report=skip_open_report,
164196
)
165197

166198

167199
def get_authorization_files_in_directory(directory): # pragma: no cover
168200
"""Get a list of download-account-authorization-files in a directory"""
169-
file_list = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
201+
file_list = [
202+
f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))
203+
]
170204
file_list_with_full_path = []
171205
for file in file_list:
172206
if file.endswith(".json"):
173-
file_list_with_full_path.append(os.path.abspath(os.path.join(directory, file)))
207+
file_list_with_full_path.append(
208+
os.path.abspath(os.path.join(directory, file))
209+
)
174210
new_file_list = []
175211
for file in file_list_with_full_path:
176212
with open(file) as f:
177213
contents = f.read()
178214
account_authorization_details_cfg = json.loads(contents)
179-
valid_schema = check_authorization_details_schema(account_authorization_details_cfg)
215+
valid_schema = check_authorization_details_schema(
216+
account_authorization_details_cfg
217+
)
180218
if valid_schema:
181219
new_file_list.append(file)
182220
return new_file_list

cloudsplaining/command/scan_policy_file.py

+32-37
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
import yaml
1313
import click
1414
import click_log
15-
from cloudsplaining.output.findings import Findings, Finding
16-
from cloudsplaining.shared.constants import EXCLUSIONS_FILE, DEFAULT_EXCLUSIONS_CONFIG
15+
from cloudsplaining.output.findings import Findings, PolicyFinding
16+
from cloudsplaining.shared.constants import EXCLUSIONS_FILE
1717
from cloudsplaining.scan.policy_document import PolicyDocument
18-
from cloudsplaining.shared.validation import check_exclusions_schema
19-
from cloudsplaining.shared.exclusions import is_name_excluded
18+
from cloudsplaining.shared.exclusions import (
19+
Exclusions,
20+
DEFAULT_EXCLUSIONS,
21+
)
2022

2123
logger = logging.getLogger(__name__)
2224
click_log.basic_config(logger)
@@ -60,7 +62,7 @@ def scan_policy_file(input, exclusions_file, high_priority_only): # pragma: no
6062
exclusions_cfg = yaml.safe_load(yaml_file)
6163
except yaml.YAMLError as exc:
6264
logger.critical(exc)
63-
check_exclusions_schema(exclusions_cfg)
65+
exclusions = Exclusions(exclusions_cfg)
6466

6567
# Get the Policy
6668
with open(file) as json_file:
@@ -70,79 +72,72 @@ def scan_policy_file(input, exclusions_file, high_priority_only): # pragma: no
7072
policy_name = Path(file).stem
7173

7274
# Run the scan and get the raw data.
73-
results = scan_policy(policy, policy_name, exclusions_cfg)
75+
results = scan_policy(policy, policy_name, exclusions)
7476

7577
# There will only be one finding in the results but it is in a list.
78+
results_exist = 0
7679
for finding in results:
7780
if finding["PrivilegeEscalation"]:
7881
print(f"{RED}Issue found: Privilege Escalation{END}")
82+
results_exist += 1
7983
for item in finding["PrivilegeEscalation"]:
8084
print(f"- Method: {item['type']}")
8185
print(f" Actions: {', '.join(item['PrivilegeEscalation'])}\n")
8286
if finding["DataExfiltrationActions"]:
87+
results_exist += 1
8388
print(f"{RED}Issue found: Data Exfiltration{END}")
8489
print(
8590
f"{BOLD}Actions{END}: {', '.join(finding['DataExfiltrationActions'])}\n"
8691
)
8792
if finding["PermissionsManagementActions"]:
93+
results_exist += 1
8894
print(f"{RED}Issue found: Resource Exposure{END}")
8995
print(
9096
f"{BOLD}Actions{END}: {', '.join(finding['PermissionsManagementActions'])}\n"
9197
)
9298
if not high_priority_only:
99+
results_exist += 1
93100
print(f"{RED}Issue found: Unrestricted Infrastructure Modification{END}")
94101
print(f"{BOLD}Actions{END}: {', '.join(finding['Actions'])}")
102+
if results_exist == 0:
103+
print("There were no results found.")
95104

96105

97-
def scan_policy(policy_json, policy_name, exclusions_cfg=DEFAULT_EXCLUSIONS_CONFIG):
106+
def scan_policy(policy_json, policy_name, exclusions=DEFAULT_EXCLUSIONS):
98107
"""
99108
Scan a policy document for missing resource constraints.
100109
110+
:param exclusions: Exclusions object
101111
:param policy_json: The AWS IAM policy document.
102-
:param exclusions_cfg: Defaults to the embedded exclusions file, which has no effect here.
103112
:param policy_name: The name of the IAM policy. Defaults to the filename when used from command line.
104113
:return:
105114
"""
106-
policy_document = PolicyDocument(policy_json)
107115
actions_missing_resource_constraints = []
108116

109-
# EXCLUDED ACTIONS - actions to exclude if they are false positives
110-
excluded_actions = exclusions_cfg.get("exclude-actions", None)
111-
if excluded_actions == [""]:
112-
excluded_actions = None
113-
114-
# convert to lowercase for comparison purposes
115-
# some weird if/else logic to reduce loops and improve performance slightly
116-
if excluded_actions:
117-
excluded_actions = [x.lower() for x in excluded_actions]
117+
policy_document = PolicyDocument(policy_json)
118118

119-
always_include_actions = exclusions_cfg.get("include-actions")
120-
findings = Findings()
119+
findings = Findings(exclusions)
121120

122121
for statement in policy_document.statements:
122+
logger.debug("Evaluating statement: %s", statement.json)
123123
if statement.effect == "Allow":
124124
actions_missing_resource_constraints.extend(
125-
statement.missing_resource_constraints_for_modify_actions(
126-
always_include_actions
127-
)
125+
statement.missing_resource_constraints_for_modify_actions(exclusions)
128126
)
129127
if actions_missing_resource_constraints:
130-
results_placeholder = []
131-
for action in actions_missing_resource_constraints:
132-
if excluded_actions:
133-
if not is_name_excluded(action.lower(), excluded_actions):
134-
results_placeholder.append(action) # pragma: no cover
135-
else:
136-
results_placeholder.append(action)
137-
actions_missing_resource_constraints = list(
138-
dict.fromkeys(results_placeholder)
128+
these_results = list(
129+
dict.fromkeys(actions_missing_resource_constraints)
139130
) # remove duplicates
140-
actions_missing_resource_constraints.sort()
141-
finding = Finding(
131+
these_results.sort()
132+
finding = PolicyFinding(
142133
policy_name=policy_name,
143134
arn=policy_name,
144-
actions=actions_missing_resource_constraints,
135+
actions=these_results,
145136
policy_document=policy_document,
137+
exclusions=exclusions,
146138
)
147-
findings.add(finding)
148-
return findings.json
139+
findings.add_policy_finding(finding)
140+
findings.single_use = True
141+
return finding.json
142+
else:
143+
return []

0 commit comments

Comments
 (0)