regscale-cli 6.27.3.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.

Files changed (112) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/utils/app_utils.py +11 -2
  3. regscale/dev/cli.py +26 -0
  4. regscale/dev/version.py +72 -0
  5. regscale/integrations/commercial/__init__.py +15 -1
  6. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  7. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  8. regscale/integrations/commercial/amazon/common.py +48 -58
  9. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  10. regscale/integrations/commercial/aws/cli.py +3093 -55
  11. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  12. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  13. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  14. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  15. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  16. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  17. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  18. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  19. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  20. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  21. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  22. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  23. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  24. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  25. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  26. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  27. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  28. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  29. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  30. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  31. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  32. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  33. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  34. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  35. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  36. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  37. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  38. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  39. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  40. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  41. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  42. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  43. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  44. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  45. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  46. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  47. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  48. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  49. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  50. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  51. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  52. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  53. regscale/integrations/commercial/aws/scanner.py +851 -206
  54. regscale/integrations/commercial/aws/security_hub.py +319 -0
  55. regscale/integrations/commercial/aws/session_manager.py +282 -0
  56. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  57. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  58. regscale/integrations/compliance_integration.py +308 -38
  59. regscale/integrations/due_date_handler.py +3 -0
  60. regscale/integrations/scanner_integration.py +399 -84
  61. regscale/models/integration_models/cisa_kev_data.json +34 -4
  62. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  63. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
  64. regscale/models/regscale_models/assessment.py +2 -1
  65. regscale/models/regscale_models/control_objective.py +74 -5
  66. regscale/models/regscale_models/file.py +2 -0
  67. regscale/models/regscale_models/issue.py +2 -5
  68. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  69. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
  70. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  71. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  72. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  73. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  74. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  75. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  76. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  77. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  78. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  79. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  80. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  81. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  82. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  83. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  84. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  85. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  86. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  87. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  88. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  89. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  90. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  91. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  92. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  93. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  94. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  95. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  96. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  97. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  98. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  99. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  100. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  101. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  102. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  103. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  104. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  105. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  106. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  107. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  108. tests/regscale/integrations/commercial/test_aws.py +55 -56
  109. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  110. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  111. {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  112. {regscale_cli-6.27.3.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