cloudsplaining 0.8.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudsplaining-0.8.0/cloudsplaining.egg-info → cloudsplaining-0.8.1}/PKG-INFO +2 -2
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/bin/version.py +1 -1
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/create_exclusions_file.py +2 -2
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/create_multi_account_config_file.py +2 -2
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/scan.py +14 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/scan_multi_account.py +14 -0
- cloudsplaining-0.8.1/cloudsplaining/scan/assume_role_policy_document.py +209 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/authorization_details.py +3 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/inline_policy.py +1 -1
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/managed_policy_detail.py +1 -1
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/role_details.py +70 -1
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/statement_detail.py +1 -2
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/constants.py +12 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/default-exclusions.yml +6 -1
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/exclusions.py +7 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/utils.py +14 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/validation.py +1 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1/cloudsplaining.egg-info}/PKG-INFO +2 -2
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/setup.py +1 -1
- cloudsplaining-0.8.0/cloudsplaining/scan/assume_role_policy_document.py +0 -91
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/LICENSE +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/MANIFEST.in +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/README.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/__init__.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/bin/__init__.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/bin/cli.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/__init__.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/download.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/expand_policy.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/scan_policy_file.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/__init__.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/dist/index.html +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/dist/js/chunk-vendors.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/dist/js/index.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/policy_finding.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/public/index.html +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/report.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/App.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/1-overview.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/2-triage-guidance.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/3-remediation-guidance.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/4-validation.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-assumable-by-compute-service.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-credentials-exposure.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-data-exfiltration.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-infrastructure-modification.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-privilege-escalation.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-resource-exposure.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/definition-service-wildcard.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/glossary.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/how-do-i-validate-results.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/identifying-false-positives.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/logo.png +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/summary.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/what-should-i-do.md +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Appendix.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Button.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Glossary.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Guidance.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/InlinePolicies.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/LinkToFinding.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/ManagedPolicies.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/PolicyTable.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Principals.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/ReportMetadata.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Summary.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/TaskTable.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/charts/SummaryFindings.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/AssumeRoleDetails.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/FindingCard.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/FindingDetails.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/PolicyDocumentDetails.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/PrivilegeEscalationDetails.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/PrivilegeEscalationFormat.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/RiskAlertIndicators.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/finding/StandardRiskDetails.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/principals/PrincipalMetadata.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/principals/RisksPerPrincipal.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/main.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/routes/routes.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/sampleData.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/groups-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/inline-policies-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/managed-policies-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/other-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/principals-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/roles-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/task-table-test.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/glossary.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/groups.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/inline-policies.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/managed-policies.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/other.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/principals.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/roles.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/task-table.js +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/Appendices.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/AwsPolicies.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/CustomerPolicies.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/Guidance.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/IamPrincipals.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/InlinePolicies.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/Summary.vue +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/template.html +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/py.typed +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/__init__.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/group_details.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/policy_document.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/resource_policy_document.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/user_details.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/__init__.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/aws_login.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/exceptions.py +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/multi-account-config.yml +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining.egg-info/SOURCES.txt +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining.egg-info/dependency_links.txt +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining.egg-info/entry_points.txt +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining.egg-info/requires.txt +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining.egg-info/top_level.txt +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining.egg-info/zip-safe +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/pyproject.toml +0 -0
- {cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudsplaining
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: AWS IAM Security Assessment tool that identifies violations of least privilege and generates a risk-prioritized HTML report.
|
|
5
5
|
Home-page: https://github.com/salesforce/cloudsplaining
|
|
6
6
|
Author: Kinnaird McQuade
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: License :: OSI Approved :: MIT License
|
|
23
23
|
Classifier: Operating System :: OS Independent
|
|
24
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# pylint: disable=missing-module-docstring
|
|
2
|
-
__version__ = "0.8.
|
|
2
|
+
__version__ = "0.8.1"
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/command/create_exclusions_file.py
RENAMED
|
@@ -41,8 +41,8 @@ def create_exclusions_file(output_file: str, verbosity: int) -> None:
|
|
|
41
41
|
set_log_level(verbosity)
|
|
42
42
|
|
|
43
43
|
with open(output_file, "a", encoding="utf-8") as file_obj:
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
file_obj.write(EXCLUSIONS_TEMPLATE)
|
|
45
|
+
|
|
46
46
|
utils.print_green(f"Success! Exclusions template file written to: {output_file}")
|
|
47
47
|
print(
|
|
48
48
|
"Make sure you download your account authorization details before running the scan."
|
|
@@ -47,8 +47,8 @@ def create_multi_account_config_file(output_file: str, verbosity: int) -> None:
|
|
|
47
47
|
os.remove(output_file)
|
|
48
48
|
|
|
49
49
|
with open(output_file, "a", encoding="utf-8") as file_obj:
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
file_obj.write(MULTI_ACCOUNT_CONFIG_TEMPLATE)
|
|
51
|
+
|
|
52
52
|
utils.print_green(f"Success! Multi-account config file written to: {os.path.relpath(output_file)}")
|
|
53
53
|
print(
|
|
54
54
|
f"\nMake sure you edit the {os.path.relpath(output_file)} file and then run the scan-multi-account command, as shown below."
|
|
@@ -96,6 +96,14 @@ from cloudsplaining.shared.validation import check_authorization_details_schema
|
|
|
96
96
|
multiple=True,
|
|
97
97
|
type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"], case_sensitive=False),
|
|
98
98
|
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"-t",
|
|
101
|
+
"--flag-trust-policies",
|
|
102
|
+
required=False,
|
|
103
|
+
default=False,
|
|
104
|
+
is_flag=True,
|
|
105
|
+
help="Flag risky trust policies in roles.",
|
|
106
|
+
)
|
|
99
107
|
def scan(
|
|
100
108
|
input_file: str,
|
|
101
109
|
exclusions_file: str,
|
|
@@ -105,6 +113,7 @@ def scan(
|
|
|
105
113
|
flag_all_risky_actions: bool,
|
|
106
114
|
verbosity: int,
|
|
107
115
|
severity: list[str],
|
|
116
|
+
flag_trust_policies: bool,
|
|
108
117
|
) -> None: # pragma: no cover
|
|
109
118
|
"""
|
|
110
119
|
Given the path to account authorization details files and the exclusions config file, scan all inline and
|
|
@@ -142,6 +151,7 @@ def scan(
|
|
|
142
151
|
minimize=minimize,
|
|
143
152
|
flag_conditional_statements=flag_conditional_statements,
|
|
144
153
|
flag_resource_arn_statements=flag_resource_arn_statements,
|
|
154
|
+
flag_trust_policies=flag_trust_policies,
|
|
145
155
|
severity=severity,
|
|
146
156
|
)
|
|
147
157
|
html_output_file = os.path.join(output, f"iam-report-{account_name}.html")
|
|
@@ -207,6 +217,7 @@ def scan_account_authorization_details(
|
|
|
207
217
|
return_json_results: Literal[True],
|
|
208
218
|
flag_conditional_statements: bool = ...,
|
|
209
219
|
flag_resource_arn_statements: bool = ...,
|
|
220
|
+
flag_trust_policies: bool = ...,
|
|
210
221
|
severity: list[str] | None = ...,
|
|
211
222
|
) -> dict[str, Any]: ...
|
|
212
223
|
|
|
@@ -222,6 +233,7 @@ def scan_account_authorization_details(
|
|
|
222
233
|
return_json_results: Literal[False] = ...,
|
|
223
234
|
flag_conditional_statements: bool = ...,
|
|
224
235
|
flag_resource_arn_statements: bool = ...,
|
|
236
|
+
flag_trust_policies: bool = ...,
|
|
225
237
|
severity: list[str] | None = ...,
|
|
226
238
|
) -> str: ...
|
|
227
239
|
|
|
@@ -236,6 +248,7 @@ def scan_account_authorization_details(
|
|
|
236
248
|
return_json_results: bool = False,
|
|
237
249
|
flag_conditional_statements: bool = False,
|
|
238
250
|
flag_resource_arn_statements: bool = False,
|
|
251
|
+
flag_trust_policies: bool = False,
|
|
239
252
|
severity: list[str] | None = None,
|
|
240
253
|
) -> str | dict[str, Any]: # pragma: no cover
|
|
241
254
|
"""
|
|
@@ -250,6 +263,7 @@ def scan_account_authorization_details(
|
|
|
250
263
|
exclusions=exclusions,
|
|
251
264
|
flag_conditional_statements=flag_conditional_statements,
|
|
252
265
|
flag_resource_arn_statements=flag_resource_arn_statements,
|
|
266
|
+
flag_trust_policies=flag_trust_policies,
|
|
253
267
|
severity=severity,
|
|
254
268
|
)
|
|
255
269
|
results = authorization_details.results
|
|
@@ -120,6 +120,14 @@ class MultiAccountConfig:
|
|
|
120
120
|
multiple=True,
|
|
121
121
|
type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"], case_sensitive=False),
|
|
122
122
|
)
|
|
123
|
+
@click.option(
|
|
124
|
+
"-t",
|
|
125
|
+
"--flag-trust-policies",
|
|
126
|
+
required=False,
|
|
127
|
+
default=False,
|
|
128
|
+
is_flag=True,
|
|
129
|
+
help="Flag risky trust policies in roles.",
|
|
130
|
+
)
|
|
123
131
|
def scan_multi_account(
|
|
124
132
|
config_file: str,
|
|
125
133
|
profile: str,
|
|
@@ -131,6 +139,7 @@ def scan_multi_account(
|
|
|
131
139
|
flag_all_risky_actions: bool,
|
|
132
140
|
verbosity: int,
|
|
133
141
|
severity: list[str],
|
|
142
|
+
flag_trust_policies: bool,
|
|
134
143
|
) -> None:
|
|
135
144
|
"""Scan multiple accounts via AssumeRole"""
|
|
136
145
|
set_log_level(verbosity)
|
|
@@ -160,6 +169,7 @@ def scan_multi_account(
|
|
|
160
169
|
severity=severity,
|
|
161
170
|
flag_conditional_statements=flag_conditional_statements,
|
|
162
171
|
flag_resource_arn_statements=flag_resource_arn_statements,
|
|
172
|
+
flag_trust_policies=flag_trust_policies,
|
|
163
173
|
)
|
|
164
174
|
|
|
165
175
|
|
|
@@ -174,6 +184,7 @@ def scan_accounts(
|
|
|
174
184
|
severity: list[str] | None = None,
|
|
175
185
|
flag_conditional_statements: bool = False,
|
|
176
186
|
flag_resource_arn_statements: bool = False,
|
|
187
|
+
flag_trust_policies: bool = False,
|
|
177
188
|
) -> None:
|
|
178
189
|
"""Use this method as a library to scan multiple accounts"""
|
|
179
190
|
# TODO: Speed improvements? Multithreading? This currently runs sequentially.
|
|
@@ -187,6 +198,7 @@ def scan_accounts(
|
|
|
187
198
|
severity=severity,
|
|
188
199
|
flag_conditional_statements=flag_conditional_statements,
|
|
189
200
|
flag_resource_arn_statements=flag_resource_arn_statements,
|
|
201
|
+
flag_trust_policies=flag_trust_policies,
|
|
190
202
|
)
|
|
191
203
|
html_report = HTMLReport(
|
|
192
204
|
account_id=target_account_id,
|
|
@@ -233,6 +245,7 @@ def scan_account(
|
|
|
233
245
|
severity: list[str] | None = None,
|
|
234
246
|
flag_conditional_statements: bool = False,
|
|
235
247
|
flag_resource_arn_statements: bool = False,
|
|
248
|
+
flag_trust_policies: bool = False,
|
|
236
249
|
) -> dict[str, dict[str, Any]]:
|
|
237
250
|
"""Scan a target account in one shot"""
|
|
238
251
|
account_authorization_details = download_account_authorization_details(
|
|
@@ -247,6 +260,7 @@ def scan_account(
|
|
|
247
260
|
severity=severity,
|
|
248
261
|
flag_conditional_statements=flag_conditional_statements,
|
|
249
262
|
flag_resource_arn_statements=flag_resource_arn_statements,
|
|
263
|
+
flag_trust_policies=flag_trust_policies,
|
|
250
264
|
)
|
|
251
265
|
results = authorization_details.results
|
|
252
266
|
return results
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Represents the AssumeRole Trust Policy. This is mainly used for identifying whether roles are assumable via AWS compute services."""
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2020, salesforce.com, inc.
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
# Licensed under the BSD 3-Clause license.
|
|
6
|
+
# For full license text, see the LICENSE file in the repo root
|
|
7
|
+
# or https://opensource.org/licenses/BSD-3-Clause
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cloudsplaining.scan.resource_policy_document import (
|
|
14
|
+
ResourcePolicyDocument,
|
|
15
|
+
ResourceStatement,
|
|
16
|
+
)
|
|
17
|
+
from cloudsplaining.shared.constants import SERVICE_PREFIXES_WITH_COMPUTE_ROLES
|
|
18
|
+
from cloudsplaining.shared.exclusions import (
|
|
19
|
+
DEFAULT_EXCLUSIONS,
|
|
20
|
+
Exclusions,
|
|
21
|
+
)
|
|
22
|
+
from cloudsplaining.shared.utils import get_account_id_from_principal
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AssumeRolePolicyDocument(ResourcePolicyDocument):
|
|
28
|
+
"""Holds the AssumeRole/Trust Policy document
|
|
29
|
+
|
|
30
|
+
It is a specialized version of a Resource-based policy
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
policy: dict[str, Any],
|
|
36
|
+
current_account_id: str | None = None,
|
|
37
|
+
exclusions: Exclusions = DEFAULT_EXCLUSIONS,
|
|
38
|
+
) -> None:
|
|
39
|
+
statement_structure = policy.get("Statement", [])
|
|
40
|
+
self.policy = policy
|
|
41
|
+
self.current_account_id = current_account_id
|
|
42
|
+
# We would actually need to define a proper base class with a generic type for statements
|
|
43
|
+
self.statements: list[AssumeRoleStatement] = [] # type:ignore[assignment]
|
|
44
|
+
self.exclusions = exclusions
|
|
45
|
+
# leaving here but excluding from tests because IAM Policy grammar dictates that it must be a list
|
|
46
|
+
if not isinstance(statement_structure, list): # pragma: no cover
|
|
47
|
+
statement_structure = [statement_structure]
|
|
48
|
+
|
|
49
|
+
for statement in statement_structure:
|
|
50
|
+
self.statements.append(AssumeRoleStatement(statement, current_account_id, exclusions))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def role_assumable_by_compute_services(self) -> list[str]:
|
|
54
|
+
"""Determines whether or not the role is assumed from a compute service, and if so which ones."""
|
|
55
|
+
return [
|
|
56
|
+
principal
|
|
57
|
+
for statement in self.statements
|
|
58
|
+
if statement.role_assumable_by_compute_services
|
|
59
|
+
for principal in statement.role_assumable_by_compute_services
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def role_assumable_by_cross_account_principals(self) -> list[str]:
|
|
64
|
+
"""Determines whether or not the role can be assumed from principals in other accounts, and if so which ones."""
|
|
65
|
+
return [
|
|
66
|
+
principal
|
|
67
|
+
for statement in self.statements
|
|
68
|
+
if statement.role_assumable_by_cross_account_principals
|
|
69
|
+
for principal in statement.role_assumable_by_cross_account_principals
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def role_assumable_by_any_principal(self) -> list[str]:
|
|
74
|
+
"""Determines whether or not the role can be assumed by any principal (*) or any AWS account root."""
|
|
75
|
+
return [
|
|
76
|
+
principal
|
|
77
|
+
for statement in self.statements
|
|
78
|
+
if statement.role_assumable_by_any_principal
|
|
79
|
+
for principal in statement.role_assumable_by_any_principal
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def role_assumable_by_any_principal_with_conditions(self) -> list[str]:
|
|
84
|
+
"""Determines whether or not the role can be assumed by any principal (*) or any AWS account root with conditions."""
|
|
85
|
+
return [
|
|
86
|
+
principal
|
|
87
|
+
for statement in self.statements
|
|
88
|
+
if statement.role_assumable_by_any_principal_with_conditions
|
|
89
|
+
for principal in statement.role_assumable_by_any_principal_with_conditions
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AssumeRoleStatement(ResourceStatement):
|
|
94
|
+
"""
|
|
95
|
+
Statements in an AssumeRole/Trust Policy document
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
statement: dict[str, Any],
|
|
101
|
+
current_account_id: str | None = None,
|
|
102
|
+
exclusions: Exclusions = DEFAULT_EXCLUSIONS,
|
|
103
|
+
) -> None:
|
|
104
|
+
super().__init__(statement=statement)
|
|
105
|
+
self.current_account_id = current_account_id
|
|
106
|
+
self.exclusions = exclusions
|
|
107
|
+
|
|
108
|
+
# self.not_principal = statement.get("NotPrincipal")
|
|
109
|
+
if statement.get("NotPrincipal"):
|
|
110
|
+
logger.critical( # pragma: no cover
|
|
111
|
+
"NotPrincipal is used in the IAM AssumeRole Trust Policy. "
|
|
112
|
+
"This should NOT be used. We suggest reviewing it ASAP."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _assume_role_actions(self) -> list[str]:
|
|
116
|
+
"""Verifies that this is limited to just sts:AssumeRole"""
|
|
117
|
+
actions = self.statement.get("Action", [])
|
|
118
|
+
if not actions:
|
|
119
|
+
logger.debug("The AssumeRole Policy has no actions in it.")
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
if isinstance(actions, list):
|
|
123
|
+
return actions
|
|
124
|
+
|
|
125
|
+
return [actions]
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def role_assumable_by_compute_services(self) -> list[str]:
|
|
129
|
+
"""Determines whether or not the role is assumed from a compute service, and if so which ones."""
|
|
130
|
+
# sts:AssumeRole must be there
|
|
131
|
+
lowercase_actions = [x.lower() for x in self.actions]
|
|
132
|
+
if "sts:AssumeRole".lower() not in lowercase_actions:
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# Effect must be Allow
|
|
136
|
+
if self.effect.lower() != "allow":
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
assumable_by_compute_services = []
|
|
140
|
+
for principal in self.principals:
|
|
141
|
+
if principal.endswith(".amazonaws.com"):
|
|
142
|
+
service_prefix_to_evaluate = principal.split(".")[0]
|
|
143
|
+
if service_prefix_to_evaluate in SERVICE_PREFIXES_WITH_COMPUTE_ROLES:
|
|
144
|
+
assumable_by_compute_services.append(service_prefix_to_evaluate)
|
|
145
|
+
return assumable_by_compute_services
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def role_assumable_by_cross_account_principals(self) -> list[str]:
|
|
149
|
+
"""Determines whether or not the role can be assumed from principals in other accounts, and if so which ones."""
|
|
150
|
+
# sts:AssumeRole must be there
|
|
151
|
+
lowercase_actions = [x.lower() for x in self.actions]
|
|
152
|
+
if "sts:AssumeRole".lower() not in lowercase_actions:
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
# Effect must be Allow
|
|
156
|
+
if self.effect.lower() != "allow":
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
principal
|
|
161
|
+
for principal in self.principals
|
|
162
|
+
if (principal_account_id := get_account_id_from_principal(principal))
|
|
163
|
+
and (self.current_account_id is None or principal_account_id != self.current_account_id)
|
|
164
|
+
and principal_account_id not in self.exclusions.known_accounts
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def role_assumable_by_any_principal(self) -> list[str]:
|
|
169
|
+
"""Determines whether or not the role can be assumed by any principal (*) or any AWS account root."""
|
|
170
|
+
# sts:AssumeRole must be there
|
|
171
|
+
lowercase_actions = [x.lower() for x in self.actions]
|
|
172
|
+
if "sts:AssumeRole".lower() not in lowercase_actions:
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
# Effect must be Allow
|
|
176
|
+
if self.effect.lower() != "allow":
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
# Must have no conditions
|
|
180
|
+
if self.statement.get("Condition"):
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
# Check if any principal is "*" or "arn:aws:iam::*:root"
|
|
184
|
+
any_principals = [
|
|
185
|
+
principal for principal in self.principals if principal == "*" or principal == "arn:aws:iam::*:root"
|
|
186
|
+
]
|
|
187
|
+
return any_principals
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def role_assumable_by_any_principal_with_conditions(self) -> list[str]:
|
|
191
|
+
"""Determines whether or not the role can be assumed by any principal (*) or any AWS account root with conditions."""
|
|
192
|
+
# sts:AssumeRole must be there
|
|
193
|
+
lowercase_actions = [x.lower() for x in self.actions]
|
|
194
|
+
if "sts:AssumeRole".lower() not in lowercase_actions:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
# Effect must be Allow
|
|
198
|
+
if self.effect.lower() != "allow":
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
# Must have conditions (opposite of role_assumable_by_any_principal)
|
|
202
|
+
if not self.statement.get("Condition"):
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
# Check if any principal is "*" or "arn:aws:iam::*:root"
|
|
206
|
+
any_principals = [
|
|
207
|
+
principal for principal in self.principals if principal == "*" or principal == "arn:aws:iam::*:root"
|
|
208
|
+
]
|
|
209
|
+
return any_principals
|
|
@@ -35,6 +35,7 @@ class AuthorizationDetails:
|
|
|
35
35
|
exclusions: Exclusions = DEFAULT_EXCLUSIONS,
|
|
36
36
|
flag_conditional_statements: bool = False,
|
|
37
37
|
flag_resource_arn_statements: bool = False,
|
|
38
|
+
flag_trust_policies: bool = False,
|
|
38
39
|
severity: list[str] | None = None,
|
|
39
40
|
) -> None:
|
|
40
41
|
"""
|
|
@@ -53,6 +54,7 @@ class AuthorizationDetails:
|
|
|
53
54
|
self.exclusions = exclusions
|
|
54
55
|
self.flag_conditional_statements = flag_conditional_statements
|
|
55
56
|
self.flag_resource_arn_statements = flag_resource_arn_statements
|
|
57
|
+
self.flag_trust_policies = flag_trust_policies
|
|
56
58
|
|
|
57
59
|
self.policies = ManagedPolicyDetails(
|
|
58
60
|
auth_json.get("Policies", []),
|
|
@@ -86,6 +88,7 @@ class AuthorizationDetails:
|
|
|
86
88
|
exclusions,
|
|
87
89
|
flag_conditional_statements=flag_conditional_statements,
|
|
88
90
|
flag_resource_arn_statements=flag_resource_arn_statements,
|
|
91
|
+
flag_trust_policies=flag_trust_policies,
|
|
89
92
|
severity=severity,
|
|
90
93
|
)
|
|
91
94
|
|
|
@@ -70,7 +70,7 @@ class InlinePolicy:
|
|
|
70
70
|
links = {}
|
|
71
71
|
for finding in findings:
|
|
72
72
|
links[finding["type"]] = (
|
|
73
|
-
f
|
|
73
|
+
f"https://cloudsplaining.readthedocs.io/en/latest/glossary/privilege-escalation/#{finding['type']}"
|
|
74
74
|
)
|
|
75
75
|
return links
|
|
76
76
|
|
|
@@ -244,7 +244,7 @@ class ManagedPolicy:
|
|
|
244
244
|
links = {}
|
|
245
245
|
for finding in findings:
|
|
246
246
|
links[finding["type"]] = (
|
|
247
|
-
f
|
|
247
|
+
f"https://cloudsplaining.readthedocs.io/en/latest/glossary/privilege-escalation/#{finding['type']}"
|
|
248
248
|
)
|
|
249
249
|
return links
|
|
250
250
|
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
from typing import TYPE_CHECKING, Any
|
|
8
9
|
|
|
10
|
+
from policy_sentry.util.arns import get_account_from_arn
|
|
11
|
+
|
|
9
12
|
from cloudsplaining.scan.assume_role_policy_document import AssumeRolePolicyDocument
|
|
10
13
|
from cloudsplaining.scan.inline_policy import InlinePolicy
|
|
11
14
|
from cloudsplaining.shared import utils
|
|
15
|
+
from cloudsplaining.shared.constants import ISSUE_SEVERITY, RISK_DEFINITION
|
|
12
16
|
from cloudsplaining.shared.exceptions import NotFoundException
|
|
13
17
|
from cloudsplaining.shared.exclusions import (
|
|
14
18
|
DEFAULT_EXCLUSIONS,
|
|
@@ -39,6 +43,7 @@ class RoleDetailList:
|
|
|
39
43
|
exclusions: Exclusions = DEFAULT_EXCLUSIONS,
|
|
40
44
|
flag_conditional_statements: bool = False,
|
|
41
45
|
flag_resource_arn_statements: bool = False,
|
|
46
|
+
flag_trust_policies: bool = False,
|
|
42
47
|
severity: list[str] | None = None,
|
|
43
48
|
) -> None:
|
|
44
49
|
self.severity = [] if severity is None else severity
|
|
@@ -50,6 +55,7 @@ class RoleDetailList:
|
|
|
50
55
|
# Fix Issue #254 - Allow flagging risky actions even when there are resource constraints
|
|
51
56
|
self.flag_conditional_statements = flag_conditional_statements
|
|
52
57
|
self.flag_resource_arn_statements = flag_resource_arn_statements
|
|
58
|
+
self.flag_trust_policies = flag_trust_policies
|
|
53
59
|
self.iam_data: dict[str, dict[Any, Any]] = {
|
|
54
60
|
"groups": {},
|
|
55
61
|
"users": {},
|
|
@@ -73,6 +79,7 @@ class RoleDetailList:
|
|
|
73
79
|
exclusions=exclusions,
|
|
74
80
|
flag_conditional_statements=self.flag_conditional_statements,
|
|
75
81
|
flag_resource_arn_statements=self.flag_resource_arn_statements,
|
|
82
|
+
flag_trust_policies=flag_trust_policies,
|
|
76
83
|
severity=self.severity,
|
|
77
84
|
)
|
|
78
85
|
)
|
|
@@ -138,6 +145,7 @@ class RoleDetail:
|
|
|
138
145
|
exclusions: Exclusions = DEFAULT_EXCLUSIONS,
|
|
139
146
|
flag_conditional_statements: bool = False,
|
|
140
147
|
flag_resource_arn_statements: bool = False,
|
|
148
|
+
flag_trust_policies: bool = False,
|
|
141
149
|
severity: list[str] | None = None,
|
|
142
150
|
) -> None:
|
|
143
151
|
"""
|
|
@@ -147,6 +155,7 @@ class RoleDetail:
|
|
|
147
155
|
:param policy_details: The ManagedPolicyDetails object - i.e., details about all managed policies in the account
|
|
148
156
|
so the role can inherit those attributes
|
|
149
157
|
"""
|
|
158
|
+
self.severity = [] if severity is None else severity
|
|
150
159
|
# Metadata
|
|
151
160
|
self.path = role_detail["Path"]
|
|
152
161
|
self.role_name = role_detail["RoleName"]
|
|
@@ -165,6 +174,7 @@ class RoleDetail:
|
|
|
165
174
|
# Fix Issue #254 - Allow flagging risky actions even when there are resource constraints
|
|
166
175
|
self.flag_conditional_statements = flag_conditional_statements
|
|
167
176
|
self.flag_resource_arn_statements = flag_resource_arn_statements
|
|
177
|
+
self.flag_trust_policies = flag_trust_policies
|
|
168
178
|
|
|
169
179
|
self.iam_data: dict[str, dict[Any, Any]] = {
|
|
170
180
|
"groups": {},
|
|
@@ -176,7 +186,16 @@ class RoleDetail:
|
|
|
176
186
|
self.assume_role_policy_document = None
|
|
177
187
|
assume_role_policy = role_detail.get("AssumeRolePolicyDocument")
|
|
178
188
|
if assume_role_policy:
|
|
179
|
-
|
|
189
|
+
# Extract current account ID from role ARN
|
|
190
|
+
current_account_id = None
|
|
191
|
+
if self.arn:
|
|
192
|
+
with contextlib.suppress(Exception):
|
|
193
|
+
# If we can't parse the account ID, continue without it
|
|
194
|
+
current_account_id = get_account_from_arn(self.arn)
|
|
195
|
+
|
|
196
|
+
self.assume_role_policy_document = AssumeRolePolicyDocument(
|
|
197
|
+
assume_role_policy, current_account_id, exclusions
|
|
198
|
+
)
|
|
180
199
|
|
|
181
200
|
# TODO: Create a class for InstanceProfileList
|
|
182
201
|
self.instance_profile_list = role_detail.get("InstanceProfileList", [])
|
|
@@ -331,4 +350,54 @@ class RoleDetail:
|
|
|
331
350
|
aws_managed_policies=self.attached_aws_managed_policies_pointer_json,
|
|
332
351
|
is_excluded=self.is_excluded,
|
|
333
352
|
)
|
|
353
|
+
|
|
354
|
+
if self.flag_trust_policies:
|
|
355
|
+
severities = {x.lower() for x in self.severity}
|
|
356
|
+
this_role_detail.update(
|
|
357
|
+
{
|
|
358
|
+
"AssumableByComputeServices": {
|
|
359
|
+
"severity": ISSUE_SEVERITY["AssumableByComputeService"],
|
|
360
|
+
"description": RISK_DEFINITION["AssumableByComputeService"],
|
|
361
|
+
"findings": (
|
|
362
|
+
self.assume_role_policy_document.role_assumable_by_compute_services
|
|
363
|
+
if self.assume_role_policy_document
|
|
364
|
+
and (ISSUE_SEVERITY["AssumableByComputeService"] in severities or not self.severity)
|
|
365
|
+
else []
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
"AssumableByCrossAccountPrincipal": {
|
|
369
|
+
"severity": ISSUE_SEVERITY["AssumableByCrossAccountPrincipal"],
|
|
370
|
+
"description": RISK_DEFINITION["AssumableByCrossAccountPrincipal"],
|
|
371
|
+
"findings": (
|
|
372
|
+
self.assume_role_policy_document.role_assumable_by_cross_account_principals
|
|
373
|
+
if self.assume_role_policy_document
|
|
374
|
+
and (ISSUE_SEVERITY["AssumableByCrossAccountPrincipal"] in severities or not self.severity)
|
|
375
|
+
else []
|
|
376
|
+
),
|
|
377
|
+
},
|
|
378
|
+
"AssumableByAnyPrincipal": {
|
|
379
|
+
"severity": ISSUE_SEVERITY["AssumableByAnyPrincipal"],
|
|
380
|
+
"description": RISK_DEFINITION["AssumableByAnyPrincipal"],
|
|
381
|
+
"findings": (
|
|
382
|
+
self.assume_role_policy_document.role_assumable_by_any_principal
|
|
383
|
+
if self.assume_role_policy_document
|
|
384
|
+
and (ISSUE_SEVERITY["AssumableByAnyPrincipal"] in severities or not self.severity)
|
|
385
|
+
else []
|
|
386
|
+
),
|
|
387
|
+
},
|
|
388
|
+
"AssumableByAnyPrincipalWithConditions": {
|
|
389
|
+
"severity": ISSUE_SEVERITY["AssumableByAnyPrincipalWithConditions"],
|
|
390
|
+
"description": RISK_DEFINITION["AssumableByAnyPrincipalWithConditions"],
|
|
391
|
+
"findings": (
|
|
392
|
+
self.assume_role_policy_document.role_assumable_by_any_principal_with_conditions
|
|
393
|
+
if self.assume_role_policy_document
|
|
394
|
+
and (
|
|
395
|
+
ISSUE_SEVERITY["AssumableByAnyPrincipalWithConditions"] in severities
|
|
396
|
+
or not self.severity
|
|
397
|
+
)
|
|
398
|
+
else []
|
|
399
|
+
),
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
)
|
|
334
403
|
return this_role_detail
|
|
@@ -241,8 +241,7 @@ class StatementDetail:
|
|
|
241
241
|
# Fix #390 - if flag_resource_arn_statements is True, then let's treat this as missing resource constraints so we can flag the action anyway.
|
|
242
242
|
elif self.flag_resource_arn_statements:
|
|
243
243
|
actions_missing_resource_constraints = self.restrictable_actions
|
|
244
|
-
|
|
245
|
-
pass
|
|
244
|
+
|
|
246
245
|
result = exclusions.get_allowed_actions(actions_missing_resource_constraints)
|
|
247
246
|
result.sort()
|
|
248
247
|
return result
|
|
@@ -54,6 +54,12 @@ include-actions:
|
|
|
54
54
|
# Write actions to include from the results, such as kms:Decrypt
|
|
55
55
|
exclude-actions:
|
|
56
56
|
- ""
|
|
57
|
+
# Known AWS Account IDs to exclude from assume role analysis.
|
|
58
|
+
# Add your organization's account IDs and any thrid party vendor's
|
|
59
|
+
# account IDs here to avoid false positives.
|
|
60
|
+
# https://github.com/fwdcloudsec/known_aws_accounts/blob/main/accounts.yaml
|
|
61
|
+
known-accounts:
|
|
62
|
+
- ""
|
|
57
63
|
"""
|
|
58
64
|
|
|
59
65
|
MULTI_ACCOUNT_CONFIG_TEMPLATE = """accounts:
|
|
@@ -83,6 +89,9 @@ ISSUE_SEVERITY = {
|
|
|
83
89
|
"CredentialsExposure": "high",
|
|
84
90
|
"InfrastructureModification": "low",
|
|
85
91
|
"AssumableByComputeService": "low",
|
|
92
|
+
"AssumableByCrossAccountPrincipal": "medium",
|
|
93
|
+
"AssumableByAnyPrincipal": "critical",
|
|
94
|
+
"AssumableByAnyPrincipalWithConditions": "medium",
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
RISK_DEFINITION = {
|
|
@@ -93,6 +102,9 @@ RISK_DEFINITION = {
|
|
|
93
102
|
"CredentialsExposure": "<p>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</p>",
|
|
94
103
|
"InfrastructureModification": "",
|
|
95
104
|
"AssumableByComputeService": "<p>IAM Roles can be assumed by AWS Compute Services (such as EC2, ECS, EKS, or Lambda) can present greater risk than user-defined roles, especially if the AWS Compute service is on an instance that is directly or indirectly exposed to the internet. Flagging these roles is particularly useful to penetration testers (or attackers) under certain scenarios.<br>For example, if an attacker obtains privileges to execute <code>ssm:SendCommand</code> and there are privileged EC2 instances with the SSM agent installed, they can effectively have the privileges of those EC2 instances.</p>",
|
|
105
|
+
"AssumableByCrossAccountPrincipal": "<p>IAM Roles that can be assumed from other AWS accounts can present a greater risk than roles that can only be assumed within the same AWS account. This is especially true if the trusting account is not owned by your organization.</p>",
|
|
106
|
+
"AssumableByAnyPrincipal": "<p>IAM Roles that can be assumed by any principal (i.e. Principal: '*') present a very high risk and should be remediated immediately.</p>",
|
|
107
|
+
"AssumableByAnyPrincipalWithConditions": "<p>IAM Roles that can be assumed by any principal (i.e. Principal: '*') but have conditions present can lead to unexpected outcomes. The conditions should be carefully reviewed to ensure they are not overly permissive.</p>",
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
PRIVILEGE_ESCALATION_METHODS = {
|
|
@@ -31,4 +31,9 @@ include-actions:
|
|
|
31
31
|
# Write actions to include from the results, such as kms:Decrypt
|
|
32
32
|
exclude-actions:
|
|
33
33
|
- ""
|
|
34
|
-
|
|
34
|
+
# Known AWS Account IDs to exclude from assume role analysis.
|
|
35
|
+
# Add your organization's account IDs and any thrid party vendor's
|
|
36
|
+
# account IDs here to avoid false positives.
|
|
37
|
+
# https://github.com/fwdcloudsec/known_aws_accounts/blob/main/accounts.yaml
|
|
38
|
+
known-accounts:
|
|
39
|
+
- ""
|
|
@@ -28,6 +28,7 @@ class Exclusions:
|
|
|
28
28
|
self.users = self._users()
|
|
29
29
|
self.groups = self._groups()
|
|
30
30
|
self.policies = self._policies()
|
|
31
|
+
self.known_accounts = self._known_accounts()
|
|
31
32
|
|
|
32
33
|
def _roles(self) -> list[str]:
|
|
33
34
|
provided_roles = self.config.get("roles", [])
|
|
@@ -65,6 +66,12 @@ class Exclusions:
|
|
|
65
66
|
always_exclude_actions = [x.lower() for x in exclude_actions]
|
|
66
67
|
return always_exclude_actions
|
|
67
68
|
|
|
69
|
+
def _known_accounts(self) -> set[str]:
|
|
70
|
+
provided_known_accounts = self.config.get("known-accounts", [])
|
|
71
|
+
# Normalize for comparisons - remove empty strings
|
|
72
|
+
known_accounts = {account for account in provided_known_accounts if account.strip()}
|
|
73
|
+
return known_accounts
|
|
74
|
+
|
|
68
75
|
def is_action_always_included(self, action_in_question: str) -> bool | str:
|
|
69
76
|
"""
|
|
70
77
|
Supply an IAM action, and get a decision about whether or not it is excluded.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# or https://opensource.org/licenses/BSD-3-Clause
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import contextlib
|
|
10
11
|
import json
|
|
11
12
|
import logging
|
|
12
13
|
import os
|
|
@@ -20,6 +21,7 @@ from policy_sentry.querying.actions import (
|
|
|
20
21
|
remove_actions_not_matching_access_level,
|
|
21
22
|
)
|
|
22
23
|
from policy_sentry.querying.all import get_all_service_prefixes
|
|
24
|
+
from policy_sentry.util.arns import get_account_from_arn
|
|
23
25
|
|
|
24
26
|
all_service_prefixes = get_all_service_prefixes()
|
|
25
27
|
logger = logging.getLogger(__name__)
|
|
@@ -169,3 +171,15 @@ def write_json_to_file(file: str, content: str) -> None:
|
|
|
169
171
|
os.remove(file)
|
|
170
172
|
|
|
171
173
|
Path(file).write_text(json.dumps(content, indent=4, default=str), encoding="utf-8")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_account_id_from_principal(principal: str) -> str | None:
|
|
177
|
+
"""Return the AWS account ID for a principal ARN or bare 12-digit account identifier."""
|
|
178
|
+
principal_stripped = principal.strip()
|
|
179
|
+
if principal_stripped.startswith("arn:aws:iam::"):
|
|
180
|
+
with contextlib.suppress(Exception):
|
|
181
|
+
return get_account_from_arn(principal_stripped)
|
|
182
|
+
return None
|
|
183
|
+
if principal_stripped.isdigit() and len(principal_stripped) == 12:
|
|
184
|
+
return principal_stripped
|
|
185
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloudsplaining
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: AWS IAM Security Assessment tool that identifies violations of least privilege and generates a risk-prioritized HTML report.
|
|
5
5
|
Home-page: https://github.com/salesforce/cloudsplaining
|
|
6
6
|
Author: Kinnaird McQuade
|
|
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: License :: OSI Approved :: MIT License
|
|
23
23
|
Classifier: Operating System :: OS Independent
|
|
24
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"""Represents the AssumeRole Trust Policy. This is mainly used for identifying whether roles are assumable via AWS compute services."""
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2020, salesforce.com, inc.
|
|
4
|
-
# All rights reserved.
|
|
5
|
-
# Licensed under the BSD 3-Clause license.
|
|
6
|
-
# For full license text, see the LICENSE file in the repo root
|
|
7
|
-
# or https://opensource.org/licenses/BSD-3-Clause
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import logging
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from cloudsplaining.scan.resource_policy_document import (
|
|
14
|
-
ResourcePolicyDocument,
|
|
15
|
-
ResourceStatement,
|
|
16
|
-
)
|
|
17
|
-
from cloudsplaining.shared.constants import SERVICE_PREFIXES_WITH_COMPUTE_ROLES
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AssumeRolePolicyDocument(ResourcePolicyDocument):
|
|
23
|
-
"""Holds the AssumeRole/Trust Policy document
|
|
24
|
-
|
|
25
|
-
It is a specialized version of a Resource-based policy
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def __init__(self, policy: dict[str, Any]) -> None:
|
|
29
|
-
statement_structure = policy.get("Statement", [])
|
|
30
|
-
self.policy = policy
|
|
31
|
-
# We would actually need to define a proper base class with a generic type for statements
|
|
32
|
-
self.statements: list[AssumeRoleStatement] = [] # type:ignore[assignment]
|
|
33
|
-
# leaving here but excluding from tests because IAM Policy grammar dictates that it must be a list
|
|
34
|
-
if not isinstance(statement_structure, list): # pragma: no cover
|
|
35
|
-
statement_structure = [statement_structure]
|
|
36
|
-
|
|
37
|
-
for statement in statement_structure:
|
|
38
|
-
self.statements.append(AssumeRoleStatement(statement))
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def role_assumable_by_compute_services(self) -> list[str]:
|
|
42
|
-
"""Determines whether or not the role is assumed from a compute service, and if so which ones."""
|
|
43
|
-
assumable_by_compute_services = []
|
|
44
|
-
for statement in self.statements:
|
|
45
|
-
if statement.role_assumable_by_compute_services:
|
|
46
|
-
assumable_by_compute_services.extend(statement.role_assumable_by_compute_services)
|
|
47
|
-
return assumable_by_compute_services
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class AssumeRoleStatement(ResourceStatement):
|
|
51
|
-
"""
|
|
52
|
-
Statements in an AssumeRole/Trust Policy document
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
def __init__(self, statement: dict[str, Any]) -> None:
|
|
56
|
-
super().__init__(statement=statement)
|
|
57
|
-
|
|
58
|
-
# self.not_principal = statement.get("NotPrincipal")
|
|
59
|
-
if statement.get("NotPrincipal"):
|
|
60
|
-
logger.critical( # pragma: no cover
|
|
61
|
-
"NotPrincipal is used in the IAM AssumeRole Trust Policy. "
|
|
62
|
-
"This should NOT be used. We suggest reviewing it ASAP."
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def _assume_role_actions(self) -> list[str]:
|
|
66
|
-
"""Verifies that this is limited to just sts:AssumeRole"""
|
|
67
|
-
actions = self.statement.get("Action", [])
|
|
68
|
-
if not actions:
|
|
69
|
-
logger.debug("The AssumeRole Policy has no actions in it.")
|
|
70
|
-
return []
|
|
71
|
-
|
|
72
|
-
if isinstance(actions, list):
|
|
73
|
-
return actions
|
|
74
|
-
|
|
75
|
-
return [actions]
|
|
76
|
-
|
|
77
|
-
@property
|
|
78
|
-
def role_assumable_by_compute_services(self) -> list[str]:
|
|
79
|
-
"""Determines whether or not the role is assumed from a compute service, and if so which ones."""
|
|
80
|
-
# sts:AssumeRole must be there
|
|
81
|
-
lowercase_actions = [x.lower() for x in self.actions]
|
|
82
|
-
if "sts:AssumeRole".lower() not in lowercase_actions:
|
|
83
|
-
return []
|
|
84
|
-
|
|
85
|
-
assumable_by_compute_services = []
|
|
86
|
-
for principal in self.principals:
|
|
87
|
-
if principal.endswith(".amazonaws.com"):
|
|
88
|
-
service_prefix_to_evaluate = principal.split(".")[0]
|
|
89
|
-
if service_prefix_to_evaluate in SERVICE_PREFIXES_WITH_COMPUTE_ROLES:
|
|
90
|
-
assumable_by_compute_services.append(service_prefix_to_evaluate)
|
|
91
|
-
return assumable_by_compute_services
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/dist/js/chunk-vendors.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/1-overview.md
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/2-triage-guidance.md
RENAMED
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/4-validation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/assets/what-should-i-do.md
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Appendix.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Button.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Glossary.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Guidance.vue
RENAMED
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/LinkToFinding.vue
RENAMED
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/PolicyTable.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Principals.vue
RENAMED
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/Summary.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/components/TaskTable.vue
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/inline-policies-test.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/principals-test.js
RENAMED
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/test/task-table-test.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/inline-policies.js
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/util/managed-policies.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/Appendices.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/AwsPolicies.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/CustomerPolicies.vue
RENAMED
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/IamPrincipals.vue
RENAMED
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/output/src/views/InlinePolicies.vue
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/scan/resource_policy_document.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudsplaining-0.8.0 → cloudsplaining-0.8.1}/cloudsplaining/shared/multi-account-config.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|