regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +853 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +415 -85
- regscale/models/integration_models/cisa_kev_data.json +80 -20
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""AWS IAM resource collection."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from regscale.integrations.commercial.aws.inventory.base import BaseCollector
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("regscale")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IAMCollector(BaseCollector):
|
|
14
|
+
"""Collector for AWS IAM resources."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, session: Any, region: str, account_id: Optional[str] = None, tags: Optional[Dict[str, str]] = None
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Initialize IAM collector.
|
|
21
|
+
|
|
22
|
+
:param session: AWS session to use for API calls
|
|
23
|
+
:param str region: AWS region to collect from
|
|
24
|
+
:param str account_id: Optional AWS account ID to filter resources
|
|
25
|
+
:param dict tags: Optional tags to filter resources (key-value pairs)
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(session, region)
|
|
28
|
+
self.account_id = account_id
|
|
29
|
+
self.tags = tags or {}
|
|
30
|
+
|
|
31
|
+
def collect(self) -> Dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Collect AWS IAM resources.
|
|
34
|
+
|
|
35
|
+
:return: Dictionary containing IAM resources
|
|
36
|
+
:rtype: Dict[str, Any]
|
|
37
|
+
"""
|
|
38
|
+
result = {
|
|
39
|
+
"Users": [],
|
|
40
|
+
"Roles": [],
|
|
41
|
+
"Groups": [],
|
|
42
|
+
"Policies": [],
|
|
43
|
+
"AccessKeys": [],
|
|
44
|
+
"MFADevices": [],
|
|
45
|
+
"AccountSummary": {},
|
|
46
|
+
"PasswordPolicy": {},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
client = self._get_client("iam")
|
|
51
|
+
|
|
52
|
+
# Get account summary
|
|
53
|
+
result["AccountSummary"] = self._get_account_summary(client)
|
|
54
|
+
|
|
55
|
+
# Get password policy
|
|
56
|
+
result["PasswordPolicy"] = self._get_password_policy(client)
|
|
57
|
+
|
|
58
|
+
# Get users
|
|
59
|
+
users = self._list_users(client)
|
|
60
|
+
result["Users"] = users
|
|
61
|
+
|
|
62
|
+
# Get roles
|
|
63
|
+
roles = self._list_roles(client)
|
|
64
|
+
result["Roles"] = roles
|
|
65
|
+
|
|
66
|
+
# Get groups
|
|
67
|
+
groups = self._list_groups(client)
|
|
68
|
+
result["Groups"] = groups
|
|
69
|
+
|
|
70
|
+
# Get policies
|
|
71
|
+
policies = self._list_policies(client)
|
|
72
|
+
result["Policies"] = policies
|
|
73
|
+
|
|
74
|
+
# Get access keys for users
|
|
75
|
+
access_keys = []
|
|
76
|
+
for user in users:
|
|
77
|
+
user_name = user.get("UserName")
|
|
78
|
+
if user_name:
|
|
79
|
+
keys = self._list_access_keys(client, user_name)
|
|
80
|
+
access_keys.extend(keys)
|
|
81
|
+
result["AccessKeys"] = access_keys
|
|
82
|
+
|
|
83
|
+
# Get MFA devices for users
|
|
84
|
+
mfa_devices = []
|
|
85
|
+
for user in users:
|
|
86
|
+
user_name = user.get("UserName")
|
|
87
|
+
if user_name:
|
|
88
|
+
devices = self._list_mfa_devices(client, user_name)
|
|
89
|
+
mfa_devices.extend(devices)
|
|
90
|
+
result["MFADevices"] = mfa_devices
|
|
91
|
+
|
|
92
|
+
logger.info(
|
|
93
|
+
f"Collected {len(users)} IAM user(s), {len(roles)} role(s), "
|
|
94
|
+
f"{len(groups)} group(s), {len(policies)} polic(ies) from {self.region}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except ClientError as e:
|
|
98
|
+
self._handle_error(e, "IAM resources")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Unexpected error collecting IAM resources: {e}", exc_info=True)
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def _get_account_summary(self, client: Any) -> Dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
Get IAM account summary.
|
|
107
|
+
|
|
108
|
+
:param client: IAM client
|
|
109
|
+
:return: Account summary information
|
|
110
|
+
:rtype: Dict[str, Any]
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
response = client.get_account_summary()
|
|
114
|
+
summary = response.get("SummaryMap", {})
|
|
115
|
+
summary["Region"] = self.region
|
|
116
|
+
return summary
|
|
117
|
+
except ClientError as e:
|
|
118
|
+
if e.response["Error"]["Code"] == "AccessDenied":
|
|
119
|
+
logger.warning(f"Access denied to get IAM account summary in {self.region}")
|
|
120
|
+
else:
|
|
121
|
+
logger.error(f"Error getting IAM account summary: {e}")
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
def _get_password_policy(self, client: Any) -> Dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
Get IAM password policy.
|
|
127
|
+
|
|
128
|
+
:param client: IAM client
|
|
129
|
+
:return: Password policy information
|
|
130
|
+
:rtype: Dict[str, Any]
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
response = client.get_account_password_policy()
|
|
134
|
+
policy = response.get("PasswordPolicy", {})
|
|
135
|
+
policy["Region"] = self.region
|
|
136
|
+
return policy
|
|
137
|
+
except ClientError as e:
|
|
138
|
+
if e.response["Error"]["Code"] in ["NoSuchEntity", "AccessDenied"]:
|
|
139
|
+
logger.debug(f"No password policy found or access denied in {self.region}")
|
|
140
|
+
else:
|
|
141
|
+
logger.error(f"Error getting IAM password policy: {e}")
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
def _list_users(self, client: Any) -> List[Dict[str, Any]]:
|
|
145
|
+
"""
|
|
146
|
+
List IAM users with pagination.
|
|
147
|
+
|
|
148
|
+
:param client: IAM client
|
|
149
|
+
:return: List of users
|
|
150
|
+
:rtype: List[Dict[str, Any]]
|
|
151
|
+
"""
|
|
152
|
+
users = []
|
|
153
|
+
try:
|
|
154
|
+
paginator = client.get_paginator("list_users")
|
|
155
|
+
|
|
156
|
+
for page in paginator.paginate():
|
|
157
|
+
for user in page.get("Users", []):
|
|
158
|
+
processed_user = self._process_user(user)
|
|
159
|
+
if processed_user:
|
|
160
|
+
users.append(processed_user)
|
|
161
|
+
|
|
162
|
+
except ClientError as e:
|
|
163
|
+
self._handle_list_users_error(e)
|
|
164
|
+
|
|
165
|
+
return users
|
|
166
|
+
|
|
167
|
+
def _process_user(self, user: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
168
|
+
"""
|
|
169
|
+
Process and filter a single IAM user.
|
|
170
|
+
|
|
171
|
+
:param dict user: Raw user data from AWS API
|
|
172
|
+
:return: Processed user dictionary or None if filtered out
|
|
173
|
+
:rtype: Optional[Dict[str, Any]]
|
|
174
|
+
"""
|
|
175
|
+
if not self._should_include_user(user):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
return self._build_user_dict(user)
|
|
179
|
+
|
|
180
|
+
def _should_include_user(self, user: Dict[str, Any]) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Check if user should be included based on filters.
|
|
183
|
+
|
|
184
|
+
:param dict user: Raw user data from AWS API
|
|
185
|
+
:return: True if user passes all filters
|
|
186
|
+
:rtype: bool
|
|
187
|
+
"""
|
|
188
|
+
if self.account_id and not self._matches_account_id(user.get("Arn", "")):
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
if self.tags:
|
|
192
|
+
user_tags = self._convert_tags_to_dict(user.get("Tags", []))
|
|
193
|
+
if not self._matches_tags(user_tags):
|
|
194
|
+
logger.debug(f"Skipping user {user.get('UserName')} - does not match tag filters")
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
def _build_user_dict(self, user: Dict[str, Any]) -> Dict[str, Any]:
|
|
200
|
+
"""
|
|
201
|
+
Build user dictionary with standardized fields.
|
|
202
|
+
|
|
203
|
+
:param dict user: Raw user data from AWS API
|
|
204
|
+
:return: Processed user dictionary
|
|
205
|
+
:rtype: Dict[str, Any]
|
|
206
|
+
"""
|
|
207
|
+
password_last_used = user.get("PasswordLastUsed")
|
|
208
|
+
return {
|
|
209
|
+
"Region": self.region,
|
|
210
|
+
"UserName": user.get("UserName"),
|
|
211
|
+
"UserId": user.get("UserId"),
|
|
212
|
+
"Arn": user.get("Arn"),
|
|
213
|
+
"CreateDate": str(user.get("CreateDate")),
|
|
214
|
+
"PasswordLastUsed": str(password_last_used) if password_last_used else None,
|
|
215
|
+
"Path": user.get("Path"),
|
|
216
|
+
"PermissionsBoundary": user.get("PermissionsBoundary"),
|
|
217
|
+
"Tags": user.get("Tags", []),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def _handle_list_users_error(self, e: ClientError) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Handle errors from listing IAM users.
|
|
223
|
+
|
|
224
|
+
:param ClientError e: The client error to handle
|
|
225
|
+
"""
|
|
226
|
+
if e.response["Error"]["Code"] == "AccessDenied":
|
|
227
|
+
logger.warning(f"Access denied to list IAM users in {self.region}")
|
|
228
|
+
else:
|
|
229
|
+
logger.error(f"Error listing IAM users: {e}")
|
|
230
|
+
|
|
231
|
+
def _list_roles(self, client: Any) -> List[Dict[str, Any]]:
|
|
232
|
+
"""
|
|
233
|
+
List IAM roles with pagination.
|
|
234
|
+
|
|
235
|
+
:param client: IAM client
|
|
236
|
+
:return: List of roles
|
|
237
|
+
:rtype: List[Dict[str, Any]]
|
|
238
|
+
"""
|
|
239
|
+
roles = []
|
|
240
|
+
try:
|
|
241
|
+
paginator = client.get_paginator("list_roles")
|
|
242
|
+
|
|
243
|
+
for page in paginator.paginate():
|
|
244
|
+
for role in page.get("Roles", []):
|
|
245
|
+
# Filter by account ID if specified
|
|
246
|
+
if self.account_id and not self._matches_account_id(role.get("Arn", "")):
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Filter by tags if specified
|
|
250
|
+
role_tags = self._convert_tags_to_dict(role.get("Tags", []))
|
|
251
|
+
if self.tags and not self._matches_tags(role_tags):
|
|
252
|
+
logger.debug(f"Skipping role {role.get('RoleName')} - does not match tag filters")
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
role_dict = {
|
|
256
|
+
"Region": self.region,
|
|
257
|
+
"RoleName": role.get("RoleName"),
|
|
258
|
+
"RoleId": role.get("RoleId"),
|
|
259
|
+
"Arn": role.get("Arn"),
|
|
260
|
+
"CreateDate": str(role.get("CreateDate")),
|
|
261
|
+
"AssumeRolePolicyDocument": role.get("AssumeRolePolicyDocument"),
|
|
262
|
+
"Description": role.get("Description"),
|
|
263
|
+
"MaxSessionDuration": role.get("MaxSessionDuration"),
|
|
264
|
+
"Path": role.get("Path"),
|
|
265
|
+
"PermissionsBoundary": role.get("PermissionsBoundary"),
|
|
266
|
+
"Tags": role.get("Tags", []),
|
|
267
|
+
}
|
|
268
|
+
roles.append(role_dict)
|
|
269
|
+
|
|
270
|
+
except ClientError as e:
|
|
271
|
+
if e.response["Error"]["Code"] == "AccessDenied":
|
|
272
|
+
logger.warning(f"Access denied to list IAM roles in {self.region}")
|
|
273
|
+
else:
|
|
274
|
+
logger.error(f"Error listing IAM roles: {e}")
|
|
275
|
+
|
|
276
|
+
return roles
|
|
277
|
+
|
|
278
|
+
def _list_groups(self, client: Any) -> List[Dict[str, Any]]:
|
|
279
|
+
"""
|
|
280
|
+
List IAM groups with pagination.
|
|
281
|
+
|
|
282
|
+
:param client: IAM client
|
|
283
|
+
:return: List of groups
|
|
284
|
+
:rtype: List[Dict[str, Any]]
|
|
285
|
+
"""
|
|
286
|
+
groups = []
|
|
287
|
+
try:
|
|
288
|
+
paginator = client.get_paginator("list_groups")
|
|
289
|
+
|
|
290
|
+
for page in paginator.paginate():
|
|
291
|
+
for group in page.get("Groups", []):
|
|
292
|
+
# Filter by account ID if specified
|
|
293
|
+
if self.account_id and not self._matches_account_id(group.get("Arn", "")):
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
group_dict = {
|
|
297
|
+
"Region": self.region,
|
|
298
|
+
"GroupName": group.get("GroupName"),
|
|
299
|
+
"GroupId": group.get("GroupId"),
|
|
300
|
+
"Arn": group.get("Arn"),
|
|
301
|
+
"CreateDate": str(group.get("CreateDate")),
|
|
302
|
+
"Path": group.get("Path"),
|
|
303
|
+
}
|
|
304
|
+
groups.append(group_dict)
|
|
305
|
+
|
|
306
|
+
except ClientError as e:
|
|
307
|
+
if e.response["Error"]["Code"] == "AccessDenied":
|
|
308
|
+
logger.warning(f"Access denied to list IAM groups in {self.region}")
|
|
309
|
+
else:
|
|
310
|
+
logger.error(f"Error listing IAM groups: {e}")
|
|
311
|
+
|
|
312
|
+
return groups
|
|
313
|
+
|
|
314
|
+
def _list_policies(self, client: Any, scope: str = "Local") -> List[Dict[str, Any]]:
|
|
315
|
+
"""
|
|
316
|
+
List IAM policies with pagination.
|
|
317
|
+
|
|
318
|
+
:param client: IAM client
|
|
319
|
+
:param str scope: Policy scope (Local or AWS)
|
|
320
|
+
:return: List of policies
|
|
321
|
+
:rtype: List[Dict[str, Any]]
|
|
322
|
+
"""
|
|
323
|
+
policies = []
|
|
324
|
+
try:
|
|
325
|
+
paginator = client.get_paginator("list_policies")
|
|
326
|
+
|
|
327
|
+
for page in paginator.paginate(Scope=scope):
|
|
328
|
+
for policy in page.get("Policies", []):
|
|
329
|
+
# Filter by account ID if specified
|
|
330
|
+
if self.account_id and not self._matches_account_id(policy.get("Arn", "")):
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
# Filter by tags if specified
|
|
334
|
+
policy_tags = self._convert_tags_to_dict(policy.get("Tags", []))
|
|
335
|
+
if self.tags and not self._matches_tags(policy_tags):
|
|
336
|
+
logger.debug(f"Skipping policy {policy.get('PolicyName')} - does not match tag filters")
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
policy_dict = {
|
|
340
|
+
"Region": self.region,
|
|
341
|
+
"PolicyName": policy.get("PolicyName"),
|
|
342
|
+
"PolicyId": policy.get("PolicyId"),
|
|
343
|
+
"Arn": policy.get("Arn"),
|
|
344
|
+
"CreateDate": str(policy.get("CreateDate")),
|
|
345
|
+
"UpdateDate": str(policy.get("UpdateDate")),
|
|
346
|
+
"AttachmentCount": policy.get("AttachmentCount"),
|
|
347
|
+
"PermissionsBoundaryUsageCount": policy.get("PermissionsBoundaryUsageCount"),
|
|
348
|
+
"IsAttachable": policy.get("IsAttachable"),
|
|
349
|
+
"Description": policy.get("Description"),
|
|
350
|
+
"DefaultVersionId": policy.get("DefaultVersionId"),
|
|
351
|
+
"Path": policy.get("Path"),
|
|
352
|
+
"Tags": policy.get("Tags", []),
|
|
353
|
+
}
|
|
354
|
+
policies.append(policy_dict)
|
|
355
|
+
|
|
356
|
+
except ClientError as e:
|
|
357
|
+
if e.response["Error"]["Code"] == "AccessDenied":
|
|
358
|
+
logger.warning(f"Access denied to list IAM policies in {self.region}")
|
|
359
|
+
else:
|
|
360
|
+
logger.error(f"Error listing IAM policies: {e}")
|
|
361
|
+
|
|
362
|
+
return policies
|
|
363
|
+
|
|
364
|
+
def _list_access_keys(self, client: Any, user_name: str) -> List[Dict[str, Any]]:
|
|
365
|
+
"""
|
|
366
|
+
List access keys for a user.
|
|
367
|
+
|
|
368
|
+
:param client: IAM client
|
|
369
|
+
:param str user_name: User name
|
|
370
|
+
:return: List of access keys
|
|
371
|
+
:rtype: List[Dict[str, Any]]
|
|
372
|
+
"""
|
|
373
|
+
access_keys = []
|
|
374
|
+
try:
|
|
375
|
+
response = client.list_access_keys(UserName=user_name)
|
|
376
|
+
for key in response.get("AccessKeyMetadata", []):
|
|
377
|
+
key_dict = {
|
|
378
|
+
"Region": self.region,
|
|
379
|
+
"UserName": user_name,
|
|
380
|
+
"AccessKeyId": key.get("AccessKeyId"),
|
|
381
|
+
"Status": key.get("Status"),
|
|
382
|
+
"CreateDate": str(key.get("CreateDate")),
|
|
383
|
+
}
|
|
384
|
+
access_keys.append(key_dict)
|
|
385
|
+
|
|
386
|
+
except ClientError as e:
|
|
387
|
+
if e.response["Error"]["Code"] != "AccessDenied":
|
|
388
|
+
logger.debug(f"Error listing access keys for user {user_name}: {e}")
|
|
389
|
+
|
|
390
|
+
return access_keys
|
|
391
|
+
|
|
392
|
+
def _list_mfa_devices(self, client: Any, user_name: str) -> List[Dict[str, Any]]:
|
|
393
|
+
"""
|
|
394
|
+
List MFA devices for a user.
|
|
395
|
+
|
|
396
|
+
:param client: IAM client
|
|
397
|
+
:param str user_name: User name
|
|
398
|
+
:return: List of MFA devices
|
|
399
|
+
:rtype: List[Dict[str, Any]]
|
|
400
|
+
"""
|
|
401
|
+
mfa_devices = []
|
|
402
|
+
try:
|
|
403
|
+
response = client.list_mfa_devices(UserName=user_name)
|
|
404
|
+
for device in response.get("MFADevices", []):
|
|
405
|
+
device_dict = {
|
|
406
|
+
"Region": self.region,
|
|
407
|
+
"UserName": user_name,
|
|
408
|
+
"SerialNumber": device.get("SerialNumber"),
|
|
409
|
+
"EnableDate": str(device.get("EnableDate")),
|
|
410
|
+
}
|
|
411
|
+
mfa_devices.append(device_dict)
|
|
412
|
+
|
|
413
|
+
except ClientError as e:
|
|
414
|
+
if e.response["Error"]["Code"] != "AccessDenied":
|
|
415
|
+
logger.debug(f"Error listing MFA devices for user {user_name}: {e}")
|
|
416
|
+
|
|
417
|
+
return mfa_devices
|
|
418
|
+
|
|
419
|
+
def _matches_account_id(self, arn: str) -> bool:
|
|
420
|
+
"""
|
|
421
|
+
Check if ARN matches the specified account ID.
|
|
422
|
+
|
|
423
|
+
:param str arn: ARN to check
|
|
424
|
+
:return: True if matches or no account_id filter specified
|
|
425
|
+
:rtype: bool
|
|
426
|
+
"""
|
|
427
|
+
if not self.account_id:
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
# ARN format: arn:aws:iam::account-id:resource-type/resource-name
|
|
431
|
+
try:
|
|
432
|
+
arn_parts = arn.split(":")
|
|
433
|
+
if len(arn_parts) >= 5:
|
|
434
|
+
resource_account_id = arn_parts[4]
|
|
435
|
+
return resource_account_id == self.account_id
|
|
436
|
+
except (IndexError, AttributeError):
|
|
437
|
+
logger.warning(f"Could not parse account ID from ARN: {arn}")
|
|
438
|
+
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
def _convert_tags_to_dict(self, tags_list: List[Dict[str, str]]) -> Dict[str, str]:
|
|
442
|
+
"""
|
|
443
|
+
Convert IAM tags list format to dictionary format.
|
|
444
|
+
|
|
445
|
+
IAM returns tags as list of dicts: [{"Key": "k1", "Value": "v1"}]
|
|
446
|
+
Convert to dict format: {"k1": "v1"}
|
|
447
|
+
|
|
448
|
+
:param list tags_list: List of tag dictionaries
|
|
449
|
+
:return: Dictionary of tags (Key -> Value)
|
|
450
|
+
:rtype: Dict[str, str]
|
|
451
|
+
"""
|
|
452
|
+
return {tag.get("Key", ""): tag.get("Value", "") for tag in tags_list}
|
|
453
|
+
|
|
454
|
+
def _matches_tags(self, resource_tags: Dict[str, str]) -> bool:
|
|
455
|
+
"""
|
|
456
|
+
Check if resource tags match the specified filter tags.
|
|
457
|
+
|
|
458
|
+
:param dict resource_tags: Tags on the resource
|
|
459
|
+
:return: True if all filter tags match
|
|
460
|
+
:rtype: bool
|
|
461
|
+
"""
|
|
462
|
+
if not self.tags:
|
|
463
|
+
return True
|
|
464
|
+
|
|
465
|
+
# All filter tags must match
|
|
466
|
+
for key, value in self.tags.items():
|
|
467
|
+
if resource_tags.get(key) != value:
|
|
468
|
+
return False
|
|
469
|
+
|
|
470
|
+
return True
|