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.

Files changed (140) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +11 -2
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/dev/cli.py +26 -0
  10. regscale/dev/version.py +72 -0
  11. regscale/integrations/commercial/__init__.py +15 -1
  12. regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
  13. regscale/integrations/commercial/amazon/amazon/common.py +204 -0
  14. regscale/integrations/commercial/amazon/common.py +48 -58
  15. regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
  16. regscale/integrations/commercial/aws/cli.py +3093 -55
  17. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  18. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  19. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  20. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  21. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  22. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  23. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  24. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  25. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  26. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  27. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  28. regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
  29. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  30. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  31. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  32. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  33. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  34. regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
  35. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  36. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  37. regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
  38. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  39. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  40. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  41. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  42. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  43. regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
  44. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  45. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  46. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  47. regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
  48. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  49. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  50. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  51. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  52. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  53. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  54. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  55. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  56. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  57. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  58. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  59. regscale/integrations/commercial/aws/scanner.py +853 -205
  60. regscale/integrations/commercial/aws/security_hub.py +319 -0
  61. regscale/integrations/commercial/aws/session_manager.py +282 -0
  62. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  63. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  64. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  65. regscale/integrations/compliance_integration.py +308 -38
  66. regscale/integrations/control_matcher.py +78 -23
  67. regscale/integrations/due_date_handler.py +3 -0
  68. regscale/integrations/public/csam/csam.py +572 -763
  69. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  70. regscale/integrations/public/csam/csam_common.py +154 -0
  71. regscale/integrations/public/csam/csam_controls.py +432 -0
  72. regscale/integrations/public/csam/csam_poam.py +124 -0
  73. regscale/integrations/public/fedramp/click.py +17 -4
  74. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  75. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  76. regscale/integrations/scanner_integration.py +415 -85
  77. regscale/models/integration_models/cisa_kev_data.json +80 -20
  78. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  79. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
  80. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  81. regscale/models/platform.py +3 -0
  82. regscale/models/regscale_models/__init__.py +5 -0
  83. regscale/models/regscale_models/assessment.py +2 -1
  84. regscale/models/regscale_models/component.py +1 -1
  85. regscale/models/regscale_models/control_implementation.py +55 -24
  86. regscale/models/regscale_models/control_objective.py +74 -5
  87. regscale/models/regscale_models/file.py +2 -0
  88. regscale/models/regscale_models/issue.py +2 -5
  89. regscale/models/regscale_models/organization.py +3 -0
  90. regscale/models/regscale_models/regscale_model.py +17 -5
  91. regscale/models/regscale_models/security_plan.py +1 -0
  92. regscale/regscale.py +11 -1
  93. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
  94. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
  95. tests/regscale/core/test_login.py +171 -4
  96. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  97. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  98. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  99. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  100. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  101. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  102. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  103. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  104. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  105. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  106. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  107. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  108. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  109. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  110. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  111. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  112. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  113. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  114. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  115. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  116. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  117. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  118. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  119. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  120. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  121. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  122. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  123. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  124. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  125. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  126. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  127. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  128. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  129. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  130. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  131. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  132. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  133. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  134. tests/regscale/integrations/commercial/test_aws.py +55 -56
  135. tests/regscale/integrations/test_control_matcher.py +24 -0
  136. tests/regscale/models/test_control_implementation.py +118 -3
  137. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
  138. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
  139. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
  140. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,574 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """AWS IAM Evidence Integration for RegScale CLI."""
4
+
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import os
9
+ import time
10
+ from datetime import datetime, timedelta
11
+ from io import BytesIO
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import boto3
15
+ from botocore.exceptions import ClientError
16
+
17
+ from regscale.core.app.api import Api
18
+ from regscale.core.app.utils.app_utils import get_current_datetime
19
+ from regscale.integrations.commercial.aws.iam_control_mappings import IAMControlMapper
20
+ from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
21
+ from regscale.models.regscale_models.evidence import Evidence
22
+ from regscale.models.regscale_models.evidence_mapping import EvidenceMapping
23
+ from regscale.models.regscale_models.file import File
24
+
25
+ logger = logging.getLogger("regscale")
26
+
27
+ IAM_CACHE_FILE = os.path.join("artifacts", "aws", "iam_data.json")
28
+ CACHE_TTL_SECONDS = 4 * 60 * 60
29
+
30
+ HTML_STRONG_OPEN = "<strong>"
31
+ HTML_STRONG_CLOSE = "</strong>"
32
+ HTML_P_OPEN = "<p>"
33
+ HTML_P_CLOSE = "</p>"
34
+ HTML_UL_OPEN = "<ul>"
35
+ HTML_UL_CLOSE = "</ul>"
36
+ HTML_LI_OPEN = "<li>"
37
+ HTML_LI_CLOSE = "</li>"
38
+ HTML_H2_OPEN = "<h2>"
39
+ HTML_H2_CLOSE = "</h2>"
40
+ HTML_H3_OPEN = "<h3>"
41
+ HTML_H3_CLOSE = "</h3>"
42
+ HTML_BR = "<br>"
43
+
44
+
45
+ class IAMComplianceItem(ComplianceItem):
46
+ """Compliance item representing AWS IAM assessment."""
47
+
48
+ def __init__(self, iam_data: Dict[str, Any], control_mapper: IAMControlMapper):
49
+ self.iam_data = iam_data
50
+ self.control_mapper = control_mapper
51
+ self._users = iam_data.get("users", [])
52
+ self._groups = iam_data.get("groups", [])
53
+ self._roles = iam_data.get("roles", [])
54
+ self._policies = iam_data.get("policies", [])
55
+ self._compliance_results = control_mapper.assess_iam_compliance(iam_data)
56
+
57
+ @property
58
+ def resource_id(self) -> str:
59
+ return "iam-account"
60
+
61
+ @property
62
+ def resource_name(self) -> str:
63
+ return f"AWS IAM Account ({len(self._users)} users, {len(self._roles)} roles)"
64
+
65
+ @property
66
+ def control_id(self) -> str:
67
+ for control_id, result in self._compliance_results.items():
68
+ if result == "FAIL":
69
+ return control_id
70
+ return list(self._compliance_results.keys())[0] if self._compliance_results else "AC-2"
71
+
72
+ @property
73
+ def compliance_result(self) -> str:
74
+ if not self._compliance_results:
75
+ return "PASS"
76
+ return "FAIL" if "FAIL" in self._compliance_results.values() else "PASS"
77
+
78
+ @property
79
+ def severity(self) -> Optional[str]:
80
+ if self.compliance_result == "PASS":
81
+ return None
82
+ if self._compliance_results.get("AC-2") == "FAIL" or self._compliance_results.get("IA-2") == "FAIL":
83
+ return "HIGH"
84
+ return "MEDIUM"
85
+
86
+ @property
87
+ def description(self) -> str:
88
+ desc_parts = [
89
+ f"{HTML_H3_OPEN}AWS IAM Access Control Assessment{HTML_H3_CLOSE}",
90
+ HTML_P_OPEN,
91
+ f"{HTML_STRONG_OPEN}Users:{HTML_STRONG_CLOSE} {len(self._users)}{HTML_BR}",
92
+ f"{HTML_STRONG_OPEN}Groups:{HTML_STRONG_CLOSE} {len(self._groups)}{HTML_BR}",
93
+ f"{HTML_STRONG_OPEN}Roles:{HTML_STRONG_CLOSE} {len(self._roles)}{HTML_BR}",
94
+ f"{HTML_STRONG_OPEN}Managed Policies:{HTML_STRONG_CLOSE} {len(self._policies)}",
95
+ HTML_P_CLOSE,
96
+ f"{HTML_H3_OPEN}Control Compliance Results{HTML_H3_CLOSE}",
97
+ HTML_UL_OPEN,
98
+ ]
99
+
100
+ for control_id, result in self._compliance_results.items():
101
+ result_color = "#d32f2f" if result == "FAIL" else "#2e7d32"
102
+ control_desc = self.control_mapper.get_control_description(control_id)
103
+ desc_parts.append(
104
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
105
+ f"<span style='color: {result_color};'>{result}</span> - {control_desc}{HTML_LI_CLOSE}"
106
+ )
107
+ desc_parts.append(HTML_UL_CLOSE)
108
+
109
+ if self.compliance_result == "FAIL":
110
+ desc_parts.append(f"{HTML_H3_OPEN}Remediation Guidance{HTML_H3_CLOSE}")
111
+ desc_parts.append(HTML_UL_OPEN)
112
+ if self._compliance_results.get("AC-2") == "FAIL":
113
+ desc_parts.append(f"{HTML_LI_OPEN}Enable MFA for all IAM users{HTML_LI_CLOSE}")
114
+ desc_parts.append(f"{HTML_LI_OPEN}Secure root account with MFA and remove access keys{HTML_LI_CLOSE}")
115
+ if self._compliance_results.get("AC-6") == "FAIL":
116
+ desc_parts.append(
117
+ f"{HTML_LI_OPEN}Remove AdministratorAccess from users, use groups/roles{HTML_LI_CLOSE}"
118
+ )
119
+ if self._compliance_results.get("IA-2") == "FAIL":
120
+ desc_parts.append(f"{HTML_LI_OPEN}Strengthen password policy requirements{HTML_LI_CLOSE}")
121
+ if self._compliance_results.get("IA-5") == "FAIL":
122
+ desc_parts.append(f"{HTML_LI_OPEN}Rotate access keys older than 90 days{HTML_LI_CLOSE}")
123
+ if self._compliance_results.get("AC-3") == "FAIL":
124
+ desc_parts.append(f"{HTML_LI_OPEN}Review and restrict role trust policies{HTML_LI_CLOSE}")
125
+ desc_parts.append(HTML_UL_CLOSE)
126
+
127
+ return "\n".join(desc_parts)
128
+
129
+ @property
130
+ def framework(self) -> str:
131
+ return self.control_mapper.framework
132
+
133
+
134
+ class AWSIAMEvidenceIntegration(ComplianceIntegration):
135
+ """Process AWS IAM data and create evidence/compliance records in RegScale."""
136
+
137
+ def __init__(
138
+ self,
139
+ plan_id: int,
140
+ region: str = "us-east-1",
141
+ framework: str = "NIST800-53R5",
142
+ create_issues: bool = True,
143
+ update_control_status: bool = True,
144
+ create_poams: bool = False,
145
+ parent_module: str = "securityplans",
146
+ collect_evidence: bool = False,
147
+ evidence_as_attachments: bool = True,
148
+ evidence_control_ids: Optional[List[str]] = None,
149
+ evidence_frequency: int = 30,
150
+ force_refresh: bool = False,
151
+ **kwargs,
152
+ ):
153
+ super().__init__(
154
+ plan_id=plan_id,
155
+ framework=framework,
156
+ create_issues=create_issues,
157
+ update_control_status=update_control_status,
158
+ create_poams=create_poams,
159
+ parent_module=parent_module,
160
+ **kwargs,
161
+ )
162
+
163
+ self.region = region
164
+ self.title = "AWS IAM"
165
+ self.collect_evidence = collect_evidence
166
+ self.evidence_as_attachments = evidence_as_attachments
167
+ self.evidence_control_ids = evidence_control_ids
168
+ self.evidence_frequency = evidence_frequency
169
+ self.force_refresh = force_refresh
170
+ self.control_mapper = IAMControlMapper(framework=framework)
171
+ self.api = Api()
172
+
173
+ profile = kwargs.get("profile")
174
+ aws_access_key_id = kwargs.get("aws_access_key_id")
175
+ aws_secret_access_key = kwargs.get("aws_secret_access_key")
176
+ aws_session_token = kwargs.get("aws_session_token")
177
+
178
+ if aws_access_key_id and aws_secret_access_key:
179
+ logger.info("Initializing AWS IAM client with explicit credentials")
180
+ self.session = boto3.Session(
181
+ region_name=region,
182
+ aws_access_key_id=aws_access_key_id,
183
+ aws_secret_access_key=aws_secret_access_key,
184
+ aws_session_token=aws_session_token,
185
+ )
186
+ else:
187
+ logger.info(f"Initializing AWS IAM client with profile: {profile if profile else 'default'}")
188
+ self.session = boto3.Session(profile_name=profile, region_name=region)
189
+
190
+ try:
191
+ self.client = self.session.client("iam")
192
+ logger.info("Successfully created AWS IAM client")
193
+ except Exception as e:
194
+ logger.error(f"Failed to create AWS IAM client: {e}")
195
+ raise
196
+
197
+ self.raw_iam_data: Dict[str, Any] = {}
198
+
199
+ def _is_cache_valid(self) -> bool:
200
+ if not os.path.exists(IAM_CACHE_FILE):
201
+ return False
202
+ file_age = time.time() - os.path.getmtime(IAM_CACHE_FILE)
203
+ is_valid = file_age < CACHE_TTL_SECONDS
204
+ if is_valid:
205
+ logger.info(f"Using cached IAM data (age: {file_age / 3600:.1f} hours)")
206
+ return is_valid
207
+
208
+ def _load_cached_data(self) -> Dict[str, Any]:
209
+ try:
210
+ with open(IAM_CACHE_FILE, encoding="utf-8") as file:
211
+ data = json.load(file)
212
+
213
+ # Validate cache format - must be a dict
214
+ if not isinstance(data, dict):
215
+ logger.warning("Invalid cache format detected (not a dict). Invalidating cache.")
216
+ return {}
217
+
218
+ return data
219
+ except (json.JSONDecodeError, IOError) as e:
220
+ logger.warning(f"Error reading cache: {e}")
221
+ return {}
222
+
223
+ def _save_to_cache(self, iam_data: Dict[str, Any]) -> None:
224
+ try:
225
+ os.makedirs(os.path.dirname(IAM_CACHE_FILE), exist_ok=True)
226
+ with open(IAM_CACHE_FILE, "w", encoding="utf-8") as file:
227
+ json.dump(iam_data, file, indent=2, default=str)
228
+ logger.info(f"Cached IAM data to {IAM_CACHE_FILE}")
229
+ except IOError as e:
230
+ logger.warning(f"Error writing cache: {e}")
231
+
232
+ def _fetch_fresh_iam_data(self) -> Dict[str, Any]:
233
+ logger.info("Fetching IAM data from AWS...")
234
+ iam_data = {}
235
+
236
+ try:
237
+ iam_data["account_summary"] = self.client.get_account_summary().get("SummaryMap", {})
238
+ iam_data["password_policy"] = self._get_password_policy()
239
+ iam_data["users"] = self._list_users()
240
+ logger.info(f"Found {len(iam_data['users'])} IAM users")
241
+ iam_data["groups"] = self._list_groups()
242
+ logger.info(f"Found {len(iam_data['groups'])} IAM groups")
243
+ iam_data["roles"] = self._list_roles()
244
+ logger.info(f"Found {len(iam_data['roles'])} IAM roles")
245
+ iam_data["policies"] = self._list_policies()
246
+ logger.info(f"Found {len(iam_data['policies'])} customer managed policies")
247
+ except ClientError as e:
248
+ logger.error(f"Error fetching IAM data: {e}")
249
+ return {}
250
+
251
+ return iam_data
252
+
253
+ def _get_password_policy(self) -> Dict[str, Any]:
254
+ try:
255
+ return self.client.get_account_password_policy().get("PasswordPolicy", {})
256
+ except ClientError as e:
257
+ if e.response["Error"]["Code"] == "NoSuchEntity":
258
+ logger.warning("No account password policy configured")
259
+ return {}
260
+ raise
261
+
262
+ def _list_users(self) -> List[Dict[str, Any]]:
263
+ users = []
264
+ try:
265
+ paginator = self.client.get_paginator("list_users")
266
+ for page in paginator.paginate():
267
+ for user in page.get("Users", []):
268
+ user_name = user["UserName"]
269
+ user["MfaEnabled"] = self._user_has_mfa(user_name)
270
+ user["AccessKeys"] = self._list_user_access_keys(user_name)
271
+ user["AttachedPolicies"] = self._list_user_attached_policies(user_name)
272
+ user["InlinePolicies"] = self._list_user_inline_policies(user_name)
273
+ user["PasswordLastUsed"] = self._get_password_last_used(user)
274
+ users.append(user)
275
+ except ClientError as e:
276
+ logger.error(f"Error listing users: {e}")
277
+ return users
278
+
279
+ def _user_has_mfa(self, user_name: str) -> bool:
280
+ try:
281
+ mfa_devices = self.client.list_mfa_devices(UserName=user_name).get("MFADevices", [])
282
+ return len(mfa_devices) > 0
283
+ except ClientError:
284
+ return False
285
+
286
+ def _list_user_access_keys(self, user_name: str) -> List[Dict[str, Any]]:
287
+ try:
288
+ keys = self.client.list_access_keys(UserName=user_name).get("AccessKeyMetadata", [])
289
+ for key in keys:
290
+ created_date = key.get("CreateDate")
291
+ if created_date:
292
+ age = (datetime.now(created_date.tzinfo) - created_date).days
293
+ key["AgeDays"] = age
294
+ return keys
295
+ except ClientError:
296
+ return []
297
+
298
+ def _list_user_attached_policies(self, user_name: str) -> List[Dict[str, Any]]:
299
+ try:
300
+ return self.client.list_attached_user_policies(UserName=user_name).get("AttachedPolicies", [])
301
+ except ClientError:
302
+ return []
303
+
304
+ def _list_user_inline_policies(self, user_name: str) -> List[Dict[str, Any]]:
305
+ try:
306
+ policy_names = self.client.list_user_policies(UserName=user_name).get("PolicyNames", [])
307
+ policies = []
308
+ for policy_name in policy_names:
309
+ policy_doc = self.client.get_user_policy(UserName=user_name, PolicyName=policy_name)
310
+ policies.append({"PolicyName": policy_name, "PolicyDocument": policy_doc.get("PolicyDocument", {})})
311
+ return policies
312
+ except ClientError:
313
+ return []
314
+
315
+ def _get_password_last_used(self, user: Dict[str, Any]) -> Optional[Dict[str, Any]]:
316
+ password_last_used = user.get("PasswordLastUsed")
317
+ if password_last_used:
318
+ days_since = (datetime.now(password_last_used.tzinfo) - password_last_used).days
319
+ return {"LastUsedDate": password_last_used, "DaysSinceUsed": days_since}
320
+ return None
321
+
322
+ def _list_groups(self) -> List[Dict[str, Any]]:
323
+ groups = []
324
+ try:
325
+ paginator = self.client.get_paginator("list_groups")
326
+ for page in paginator.paginate():
327
+ groups.extend(page.get("Groups", []))
328
+ except ClientError as e:
329
+ logger.error(f"Error listing groups: {e}")
330
+ return groups
331
+
332
+ def _list_roles(self) -> List[Dict[str, Any]]:
333
+ roles = []
334
+ try:
335
+ paginator = self.client.get_paginator("list_roles")
336
+ for page in paginator.paginate():
337
+ for role in page.get("Roles", []):
338
+ role_name = role["RoleName"]
339
+ role["AttachedPolicies"] = self._list_role_attached_policies(role_name)
340
+ roles.append(role)
341
+ except ClientError as e:
342
+ logger.error(f"Error listing roles: {e}")
343
+ return roles
344
+
345
+ def _list_role_attached_policies(self, role_name: str) -> List[Dict[str, Any]]:
346
+ try:
347
+ return self.client.list_attached_role_policies(RoleName=role_name).get("AttachedPolicies", [])
348
+ except ClientError:
349
+ return []
350
+
351
+ def _list_policies(self) -> List[Dict[str, Any]]:
352
+ policies = []
353
+ try:
354
+ paginator = self.client.get_paginator("list_policies")
355
+ for page in paginator.paginate(Scope="Local"):
356
+ policies.extend(page.get("Policies", []))
357
+ except ClientError as e:
358
+ logger.error(f"Error listing policies: {e}")
359
+ return policies
360
+
361
+ def fetch_compliance_data(self) -> List[Dict[str, Any]]:
362
+ if not self.force_refresh and self._is_cache_valid():
363
+ cached_data = self._load_cached_data()
364
+ if cached_data:
365
+ self.raw_iam_data = cached_data
366
+ return [cached_data]
367
+
368
+ if self.force_refresh:
369
+ logger.info("Force refresh requested, fetching fresh IAM data...")
370
+
371
+ try:
372
+ iam_data = self._fetch_fresh_iam_data()
373
+ self.raw_iam_data = iam_data
374
+ self._save_to_cache(iam_data)
375
+ return [iam_data] if iam_data else []
376
+ except ClientError as e:
377
+ logger.error(f"Error fetching IAM data: {e}")
378
+ return []
379
+
380
+ def create_compliance_item(self, raw_data: Dict[str, Any]) -> ComplianceItem:
381
+ return IAMComplianceItem(raw_data, self.control_mapper)
382
+
383
+ def sync_compliance(self) -> None:
384
+ super().sync_compliance()
385
+ if self.collect_evidence:
386
+ logger.info("Evidence collection enabled, starting evidence collection...")
387
+ self._collect_iam_evidence()
388
+
389
+ def _collect_iam_evidence(self) -> None:
390
+ if not self.raw_iam_data:
391
+ logger.warning("No IAM data available for evidence collection")
392
+ return
393
+
394
+ scan_date = get_current_datetime(dt_format="%Y-%m-%d")
395
+
396
+ if self.evidence_as_attachments:
397
+ logger.info("Creating SSP file attachment with IAM evidence...")
398
+ self._create_ssp_attachment(scan_date)
399
+ else:
400
+ logger.info("Creating Evidence record with IAM evidence...")
401
+ self._create_evidence_record(scan_date)
402
+
403
+ def _create_ssp_attachment(self, scan_date: str) -> None:
404
+ try:
405
+ # Check for existing evidence to avoid duplicates
406
+ date_str = datetime.now().strftime("%Y%m%d")
407
+ file_name_pattern = f"iam_evidence_{date_str}"
408
+
409
+ if self.check_for_existing_evidence(file_name_pattern):
410
+ logger.info("Evidence file for IAM already exists for today. Skipping upload to avoid duplicates.")
411
+ return
412
+
413
+ # Add timestamp to make filename unique if run multiple times per day
414
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
415
+ file_name = f"iam_evidence_{timestamp}.jsonl.gz"
416
+
417
+ compliance_item = self.create_compliance_item(self.raw_iam_data)
418
+ evidence_entry = {
419
+ **self.raw_iam_data,
420
+ "compliance_assessment": {
421
+ "overall_result": compliance_item.compliance_result,
422
+ "control_results": compliance_item._compliance_results,
423
+ "assessed_controls": list(compliance_item._compliance_results.keys()),
424
+ "assessment_date": scan_date,
425
+ },
426
+ }
427
+ jsonl_content = json.dumps(evidence_entry, default=str)
428
+
429
+ compressed_buffer = BytesIO()
430
+ with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
431
+ gz_file.write(jsonl_content)
432
+
433
+ compressed_data = compressed_buffer.getvalue()
434
+ api = Api()
435
+ success = File.upload_file_to_regscale(
436
+ file_name=file_name,
437
+ parent_id=self.plan_id,
438
+ parent_module=self.parent_module,
439
+ api=api,
440
+ file_data=compressed_data,
441
+ tags="aws,iam,access-control,automated",
442
+ )
443
+
444
+ if success:
445
+ logger.info(f"Successfully uploaded IAM evidence file: {file_name}")
446
+ else:
447
+ logger.error("Failed to upload IAM evidence file")
448
+
449
+ except Exception as e:
450
+ logger.error(f"Error creating SSP attachment: {e}", exc_info=True)
451
+
452
+ def _create_evidence_record(self, scan_date: str) -> None:
453
+ try:
454
+ title = f"AWS IAM Evidence - {scan_date}"
455
+ description = self._build_evidence_description(scan_date)
456
+ due_date = (datetime.now() + timedelta(days=self.evidence_frequency)).isoformat()
457
+
458
+ evidence = Evidence(
459
+ title=title,
460
+ description=description,
461
+ status="Collected",
462
+ updateFrequency=self.evidence_frequency,
463
+ dueDate=due_date,
464
+ )
465
+
466
+ created_evidence = evidence.create()
467
+ if not created_evidence or not created_evidence.id:
468
+ logger.error("Failed to create evidence record")
469
+ return
470
+
471
+ logger.info(f"Created evidence record {created_evidence.id}: {title}")
472
+ self._upload_evidence_file(created_evidence.id, scan_date)
473
+ self._link_evidence_to_ssp(created_evidence.id)
474
+
475
+ # Link to controls if specified
476
+ if self.evidence_control_ids:
477
+ self._link_evidence_to_controls(created_evidence.id, is_attachment=False)
478
+
479
+ except Exception as e:
480
+ logger.error(f"Error creating evidence record: {e}", exc_info=True)
481
+
482
+ def _build_evidence_description(self, scan_date: str) -> str:
483
+ users = self.raw_iam_data.get("users", [])
484
+ groups = self.raw_iam_data.get("groups", [])
485
+ roles = self.raw_iam_data.get("roles", [])
486
+ compliance_item = self.create_compliance_item(self.raw_iam_data)
487
+
488
+ desc_parts = [
489
+ "<h1>AWS IAM Access Control Evidence</h1>",
490
+ f"{HTML_P_OPEN}{HTML_STRONG_OPEN}Assessment Date:{HTML_STRONG_CLOSE} {scan_date}{HTML_P_CLOSE}",
491
+ f"{HTML_H2_OPEN}IAM Summary{HTML_H2_CLOSE}",
492
+ HTML_UL_OPEN,
493
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Users:{HTML_STRONG_CLOSE} {len(users)}{HTML_LI_CLOSE}",
494
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Groups:{HTML_STRONG_CLOSE} {len(groups)}{HTML_LI_CLOSE}",
495
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}Roles:{HTML_STRONG_CLOSE} {len(roles)}{HTML_LI_CLOSE}",
496
+ HTML_UL_CLOSE,
497
+ f"{HTML_H2_OPEN}Control Compliance Results{HTML_H2_CLOSE}",
498
+ HTML_UL_OPEN,
499
+ ]
500
+
501
+ for control_id, result in compliance_item._compliance_results.items():
502
+ control_desc = self.control_mapper.get_control_description(control_id)
503
+ result_color = "#d32f2f" if result == "FAIL" else "#2e7d32"
504
+ desc_parts.append(
505
+ f"{HTML_LI_OPEN}{HTML_STRONG_OPEN}{control_id}:{HTML_STRONG_CLOSE} "
506
+ f"<span style='color: {result_color};'>{result}</span> - {control_desc}{HTML_LI_CLOSE}"
507
+ )
508
+
509
+ desc_parts.append(HTML_UL_CLOSE)
510
+ return "\n".join(desc_parts)
511
+
512
+ def _upload_evidence_file(self, evidence_id: int, scan_date: str) -> None:
513
+ try:
514
+ compliance_item = self.create_compliance_item(self.raw_iam_data)
515
+ evidence_entry = {
516
+ **self.raw_iam_data,
517
+ "compliance_assessment": {
518
+ "overall_result": compliance_item.compliance_result,
519
+ "control_results": compliance_item._compliance_results,
520
+ "assessed_controls": list(compliance_item._compliance_results.keys()),
521
+ "assessment_date": scan_date,
522
+ },
523
+ }
524
+ jsonl_content = json.dumps(evidence_entry, default=str)
525
+
526
+ compressed_buffer = BytesIO()
527
+ with gzip.open(compressed_buffer, "wt", encoding="utf-8", compresslevel=9) as gz_file:
528
+ gz_file.write(jsonl_content)
529
+
530
+ compressed_data = compressed_buffer.getvalue()
531
+ file_name = f"iam_evidence_{scan_date}.jsonl.gz"
532
+
533
+ api = Api()
534
+ success = File.upload_file_to_regscale(
535
+ file_name=file_name,
536
+ parent_id=evidence_id,
537
+ parent_module="evidence",
538
+ api=api,
539
+ file_data=compressed_data,
540
+ tags="aws,iam,access-control",
541
+ )
542
+
543
+ if success:
544
+ logger.info(f"Uploaded IAM evidence file to Evidence {evidence_id}")
545
+ else:
546
+ logger.warning(f"Failed to upload evidence file to Evidence {evidence_id}")
547
+
548
+ except Exception as e:
549
+ logger.error(f"Error uploading evidence file: {e}", exc_info=True)
550
+
551
+ def _link_evidence_to_ssp(self, evidence_id: int) -> None:
552
+ try:
553
+ mapping = EvidenceMapping(evidenceID=evidence_id, mappedID=self.plan_id, mappingType=self.parent_module)
554
+ mapping.create()
555
+ logger.info(f"Linked evidence {evidence_id} to SSP {self.plan_id}")
556
+ except Exception as ex:
557
+ logger.warning(f"Failed to link evidence to SSP: {ex}")
558
+
559
+ def _link_evidence_to_controls(self, evidence_id: int, is_attachment: bool = False) -> None:
560
+ """
561
+ Link evidence to specified control IDs.
562
+
563
+ :param int evidence_id: Evidence or attachment ID
564
+ :param bool is_attachment: True if linking attachment, False for evidence record
565
+ """
566
+ try:
567
+ for control_id in self.evidence_control_ids:
568
+ if is_attachment:
569
+ self.api.link_ssp_attachment_to_control(self.plan_id, evidence_id, control_id)
570
+ else:
571
+ self.api.link_evidence_to_control(evidence_id, control_id)
572
+ logger.info(f"Linked evidence {evidence_id} to control {control_id}")
573
+ except Exception as e:
574
+ logger.error(f"Failed to link evidence to controls: {e}", exc_info=True)