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,513 @@
1
+ """AWS Audit Manager 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 AuditManagerCollector(BaseCollector):
14
+ """Collector for AWS Audit Manager 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 Audit Manager 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 Audit Manager resources.
34
+
35
+ :return: Dictionary containing Audit Manager information
36
+ :rtype: Dict[str, Any]
37
+ """
38
+ result = {
39
+ "Assessments": [],
40
+ "AssessmentFrameworks": [],
41
+ "Controls": [],
42
+ "AssessmentReports": [],
43
+ "Evidence": [],
44
+ "Settings": {},
45
+ }
46
+
47
+ try:
48
+ client = self._get_client("auditmanager")
49
+
50
+ # Get assessments
51
+ assessments = self._list_assessments(client)
52
+ result["Assessments"] = assessments
53
+
54
+ # Get assessment frameworks
55
+ frameworks = self._list_assessment_frameworks(client)
56
+ result["AssessmentFrameworks"] = frameworks
57
+
58
+ # Get controls
59
+ controls = self._list_controls(client)
60
+ result["Controls"] = controls
61
+
62
+ # Get account settings
63
+ settings = self._get_settings(client)
64
+ result["Settings"] = settings
65
+
66
+ logger.info(
67
+ f"Collected {len(assessments)} assessment(s), {len(frameworks)} framework(s) from {self.region}"
68
+ )
69
+
70
+ except ClientError as e:
71
+ self._handle_error(e, "Audit Manager resources")
72
+ except Exception as e:
73
+ logger.error(f"Unexpected error collecting Audit Manager resources: {e}", exc_info=True)
74
+
75
+ return result
76
+
77
+ def _list_assessments(self, client: Any) -> List[Dict[str, Any]]:
78
+ """
79
+ List audit assessments.
80
+
81
+ :param client: Audit Manager client
82
+ :return: List of assessment information
83
+ :rtype: List[Dict[str, Any]]
84
+ """
85
+ assessments = []
86
+ try:
87
+ # Note: list_assessments does not support pagination
88
+ response = client.list_assessments()
89
+
90
+ for assessment_metadata in response.get("assessmentMetadata", []):
91
+ assessment_id = assessment_metadata.get("id")
92
+ assessment_dict = self._fetch_and_process_assessment(client, assessment_id)
93
+ if assessment_dict:
94
+ assessments.append(assessment_dict)
95
+
96
+ except ClientError as e:
97
+ if e.response["Error"]["Code"] == "AccessDeniedException":
98
+ logger.warning(f"Access denied to list assessments in {self.region}")
99
+ else:
100
+ logger.error(f"Error listing assessments: {e}")
101
+
102
+ return assessments
103
+
104
+ def _fetch_and_process_assessment(self, client: Any, assessment_id: str) -> Optional[Dict[str, Any]]:
105
+ """
106
+ Fetch and process a single assessment if it passes filters.
107
+
108
+ :param client: Audit Manager client
109
+ :param str assessment_id: Assessment ID
110
+ :return: Assessment dictionary or None if filtered out or error
111
+ :rtype: Optional[Dict[str, Any]]
112
+ """
113
+ try:
114
+ # Get full assessment details
115
+ assessment_response = client.get_assessment(assessmentId=assessment_id)
116
+ assessment = assessment_response.get("assessment", {})
117
+
118
+ # Filter by account ID if specified
119
+ if self.account_id:
120
+ aws_account = assessment.get("awsAccount", {})
121
+ if not self._matches_account_id(aws_account.get("id", "")):
122
+ return None
123
+
124
+ # Filter by tags if specified
125
+ if self.tags and not self._matches_tags(assessment.get("tags", {})):
126
+ return None
127
+
128
+ return self._build_assessment_dict(assessment)
129
+
130
+ except ClientError as e:
131
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "AccessDeniedException"]:
132
+ logger.error(f"Error getting assessment {assessment_id}: {e}")
133
+ return None
134
+
135
+ def _build_assessment_dict(self, assessment: Dict[str, Any]) -> Dict[str, Any]:
136
+ """
137
+ Build assessment dictionary from assessment data.
138
+
139
+ :param dict assessment: Assessment data
140
+ :return: Formatted assessment dictionary
141
+ :rtype: Dict[str, Any]
142
+ """
143
+ metadata = assessment.get("metadata", {})
144
+ framework = assessment.get("framework", {})
145
+
146
+ return {
147
+ "Region": self.region,
148
+ "AssessmentId": assessment.get("arn"),
149
+ "Name": metadata.get("name"),
150
+ "Description": metadata.get("description"),
151
+ "ComplianceType": metadata.get("complianceType"),
152
+ "Status": metadata.get("status"),
153
+ "AwsAccount": assessment.get("awsAccount", {}),
154
+ "Framework": {
155
+ "Id": framework.get("id"),
156
+ "Type": framework.get("type"),
157
+ "Arn": framework.get("arn"),
158
+ "Metadata": framework.get("metadata", {}),
159
+ },
160
+ "Scope": metadata.get("scope", {}),
161
+ "Roles": metadata.get("roles", []),
162
+ "CreationTime": str(metadata.get("creationTime")) if metadata.get("creationTime") else None,
163
+ "LastUpdated": str(metadata.get("lastUpdated")) if metadata.get("lastUpdated") else None,
164
+ "Tags": assessment.get("tags", {}),
165
+ }
166
+
167
+ def _list_assessment_frameworks(self, client: Any) -> List[Dict[str, Any]]:
168
+ """
169
+ List assessment frameworks.
170
+
171
+ :param client: Audit Manager client
172
+ :return: List of framework information
173
+ :rtype: List[Dict[str, Any]]
174
+ """
175
+ frameworks = []
176
+ try:
177
+ # Get standard and custom frameworks
178
+ frameworks.extend(self._list_frameworks_by_type(client, "Standard"))
179
+ frameworks.extend(self._list_frameworks_by_type(client, "Custom"))
180
+
181
+ except ClientError as e:
182
+ if e.response["Error"]["Code"] == "AccessDeniedException":
183
+ logger.warning(f"Access denied to list assessment frameworks in {self.region}")
184
+ else:
185
+ logger.error(f"Error listing assessment frameworks: {e}")
186
+
187
+ return frameworks
188
+
189
+ def _list_frameworks_by_type(self, client: Any, framework_type: str) -> List[Dict[str, Any]]:
190
+ """
191
+ List assessment frameworks of a specific type with pagination.
192
+
193
+ :param client: Audit Manager client
194
+ :param str framework_type: Framework type (Standard or Custom)
195
+ :return: List of framework information
196
+ :rtype: List[Dict[str, Any]]
197
+ """
198
+ frameworks = []
199
+ next_token = None
200
+
201
+ while True:
202
+ params = {"frameworkType": framework_type}
203
+ if next_token:
204
+ params["nextToken"] = next_token
205
+
206
+ response = client.list_assessment_frameworks(**params)
207
+
208
+ for framework in response.get("frameworkMetadataList", []):
209
+ framework_dict = self._process_framework(client, framework, framework_type)
210
+ if framework_dict:
211
+ frameworks.append(framework_dict)
212
+
213
+ next_token = response.get("nextToken")
214
+ if not next_token:
215
+ break
216
+
217
+ return frameworks
218
+
219
+ def _process_framework(
220
+ self, client: Any, framework: Dict[str, Any], framework_type: str
221
+ ) -> Optional[Dict[str, Any]]:
222
+ """
223
+ Process a single framework and return its dictionary if it passes filters.
224
+
225
+ :param client: Audit Manager client
226
+ :param dict framework: Framework metadata
227
+ :param str framework_type: Framework type (Standard or Custom)
228
+ :return: Framework dictionary or None if filtered out
229
+ :rtype: Optional[Dict[str, Any]]
230
+ """
231
+ framework_arn = framework.get("arn")
232
+
233
+ # Get tags if filtering is enabled
234
+ framework_tags = {}
235
+ if self.tags and framework_arn:
236
+ framework_tags = self._get_resource_tags(client, framework_arn)
237
+
238
+ # Filter by tags if specified
239
+ if self.tags and not self._matches_tags(framework_tags):
240
+ return None
241
+
242
+ return {
243
+ "Region": self.region,
244
+ "Id": framework.get("id"),
245
+ "Arn": framework_arn,
246
+ "Name": framework.get("name"),
247
+ "Type": framework_type,
248
+ "Description": framework.get("description"),
249
+ "ComplianceType": framework.get("complianceType"),
250
+ "ControlsCount": framework.get("controlsCount"),
251
+ "ControlSetsCount": framework.get("controlSetsCount"),
252
+ "CreatedAt": str(framework.get("createdAt")) if framework.get("createdAt") else None,
253
+ "LastUpdatedAt": str(framework.get("lastUpdatedAt")) if framework.get("lastUpdatedAt") else None,
254
+ "Tags": framework_tags,
255
+ }
256
+
257
+ def _list_controls(self, client: Any) -> List[Dict[str, Any]]:
258
+ """
259
+ List controls.
260
+
261
+ :param client: Audit Manager client
262
+ :return: List of control information
263
+ :rtype: List[Dict[str, Any]]
264
+ """
265
+ controls = []
266
+ try:
267
+ # Get standard and custom controls
268
+ controls.extend(self._list_controls_by_type(client, "Standard"))
269
+ controls.extend(self._list_controls_by_type(client, "Custom"))
270
+
271
+ except ClientError as e:
272
+ if e.response["Error"]["Code"] == "AccessDeniedException":
273
+ logger.warning(f"Access denied to list controls in {self.region}")
274
+ else:
275
+ logger.error(f"Error listing controls: {e}")
276
+
277
+ return controls
278
+
279
+ def _list_controls_by_type(self, client: Any, control_type: str) -> List[Dict[str, Any]]:
280
+ """
281
+ List controls of a specific type with pagination.
282
+
283
+ :param client: Audit Manager client
284
+ :param str control_type: Control type (Standard or Custom)
285
+ :return: List of control information
286
+ :rtype: List[Dict[str, Any]]
287
+ """
288
+ controls = []
289
+ next_token = None
290
+
291
+ while True:
292
+ params = {"controlType": control_type}
293
+ if next_token:
294
+ params["nextToken"] = next_token
295
+
296
+ response = client.list_controls(**params)
297
+
298
+ for control in response.get("controlMetadataList", []):
299
+ control_dict = self._process_control(client, control, control_type)
300
+ if control_dict:
301
+ controls.append(control_dict)
302
+
303
+ next_token = response.get("nextToken")
304
+ if not next_token:
305
+ break
306
+
307
+ return controls
308
+
309
+ def _process_control(self, client: Any, control: Dict[str, Any], control_type: str) -> Optional[Dict[str, Any]]:
310
+ """
311
+ Process a single control and return its dictionary if it passes filters.
312
+
313
+ :param client: Audit Manager client
314
+ :param dict control: Control metadata
315
+ :param str control_type: Control type (Standard or Custom)
316
+ :return: Control dictionary or None if filtered out
317
+ :rtype: Optional[Dict[str, Any]]
318
+ """
319
+ control_arn = control.get("arn")
320
+
321
+ # Get tags if filtering is enabled
322
+ control_tags = {}
323
+ if self.tags and control_arn:
324
+ control_tags = self._get_resource_tags(client, control_arn)
325
+
326
+ # Filter by tags if specified
327
+ if self.tags and not self._matches_tags(control_tags):
328
+ return None
329
+
330
+ return {
331
+ "Region": self.region,
332
+ "Id": control.get("id"),
333
+ "Arn": control_arn,
334
+ "Name": control.get("name"),
335
+ "Type": control_type,
336
+ "ControlSources": control.get("controlSources"),
337
+ "CreatedAt": str(control.get("createdAt")) if control.get("createdAt") else None,
338
+ "LastUpdatedAt": str(control.get("lastUpdatedAt")) if control.get("lastUpdatedAt") else None,
339
+ "Tags": control_tags,
340
+ }
341
+
342
+ def _get_settings(self, client: Any) -> Dict[str, Any]:
343
+ """
344
+ Get account settings.
345
+
346
+ :param client: Audit Manager client
347
+ :return: Settings information
348
+ :rtype: Dict[str, Any]
349
+ """
350
+ try:
351
+ response = client.get_settings(attribute="ALL")
352
+ settings = response.get("settings", {})
353
+
354
+ return {
355
+ "IsAwsOrgEnabled": settings.get("isAwsOrgEnabled", False),
356
+ "SnsTopic": settings.get("snsTopic"),
357
+ "DefaultAssessmentReportsDestination": settings.get("defaultAssessmentReportsDestination", {}),
358
+ "DefaultProcessOwners": settings.get("defaultProcessOwners", []),
359
+ "KmsKey": settings.get("kmsKey"),
360
+ "EvidenceFinderEnabled": settings.get("evidenceFinderEnablement", {}).get("enablementStatus")
361
+ == "ENABLED",
362
+ }
363
+ except ClientError as e:
364
+ if e.response["Error"]["Code"] == "AccessDeniedException":
365
+ logger.warning(f"Access denied to get settings in {self.region}")
366
+ else:
367
+ logger.debug(f"Error getting settings: {e}")
368
+ return {}
369
+
370
+ def _get_resource_tags(self, client: Any, resource_arn: str) -> Dict[str, str]:
371
+ """
372
+ Retrieve tags for a resource.
373
+
374
+ :param client: Audit Manager client
375
+ :param str resource_arn: ARN of the resource
376
+ :return: Dictionary of tags
377
+ :rtype: Dict[str, str]
378
+ """
379
+ try:
380
+ response = client.list_tags_for_resource(resourceArn=resource_arn)
381
+ return response.get("tags", {})
382
+ except ClientError as e:
383
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "AccessDeniedException"]:
384
+ logger.debug(f"Error getting tags for resource {resource_arn}: {e}")
385
+ return {}
386
+
387
+ def _matches_account_id(self, account_id: str) -> bool:
388
+ """
389
+ Check if account ID matches the specified filter.
390
+
391
+ :param str account_id: Account ID to check
392
+ :return: True if matches or no account_id filter specified
393
+ :rtype: bool
394
+ """
395
+ if not self.account_id:
396
+ return True
397
+ return account_id == self.account_id
398
+
399
+ def _matches_tags(self, resource_tags: Dict[str, str]) -> bool:
400
+ """
401
+ Check if resource tags match the specified filter tags.
402
+
403
+ :param dict resource_tags: Tags on the resource
404
+ :return: True if all filter tags match
405
+ :rtype: bool
406
+ """
407
+ if not self.tags:
408
+ return True
409
+
410
+ # All filter tags must match
411
+ for key, value in self.tags.items():
412
+ if resource_tags.get(key) != value:
413
+ return False
414
+
415
+ return True
416
+
417
+ def get_assessment_evidence(self, assessment_id: str, control_set_id: str, control_id: str) -> List[Dict[str, Any]]:
418
+ """
419
+ Get evidence for a specific control in an assessment.
420
+
421
+ :param str assessment_id: Assessment ID
422
+ :param str control_set_id: Control set ID
423
+ :param str control_id: Control ID
424
+ :return: List of evidence items
425
+ :rtype: List[Dict[str, Any]]
426
+ """
427
+ evidence_items = []
428
+ try:
429
+ client = self._get_client("auditmanager")
430
+
431
+ paginator = client.get_paginator("get_evidence_by_evidence_folder")
432
+ pages = paginator.paginate(
433
+ assessmentId=assessment_id, controlSetId=control_set_id, evidenceFolderId=control_id
434
+ )
435
+
436
+ for page in pages:
437
+ evidence_items.extend(page.get("evidence", []))
438
+
439
+ logger.debug(f"Retrieved {len(evidence_items)} evidence items for control {control_id}")
440
+
441
+ except ClientError as e:
442
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "AccessDeniedException"]:
443
+ logger.error(f"Error getting evidence for control {control_id}: {e}")
444
+
445
+ return evidence_items
446
+
447
+ def get_control_assessment_results(
448
+ self, assessment_id: str, control_set_id: str, control_id: str
449
+ ) -> Optional[Dict[str, Any]]:
450
+ """
451
+ Get assessment results for a specific control.
452
+
453
+ :param str assessment_id: Assessment ID
454
+ :param str control_set_id: Control set ID
455
+ :param str control_id: Control ID
456
+ :return: Control assessment result or None
457
+ :rtype: Optional[Dict[str, Any]]
458
+ """
459
+ try:
460
+ client = self._get_client("auditmanager")
461
+
462
+ response = client.get_change_logs(
463
+ assessmentId=assessment_id, controlSetId=control_set_id, controlId=control_id
464
+ )
465
+
466
+ change_logs = response.get("changeLogs", [])
467
+ if not change_logs:
468
+ return None
469
+
470
+ latest_change = max(change_logs, key=lambda x: x.get("createdAt", ""))
471
+
472
+ return {
473
+ "controlId": control_id,
474
+ "status": latest_change.get("objectType", "UNDER_REVIEW"),
475
+ "createdAt": latest_change.get("createdAt"),
476
+ "createdBy": latest_change.get("createdBy"),
477
+ "action": latest_change.get("action"),
478
+ }
479
+
480
+ except ClientError as e:
481
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "AccessDeniedException"]:
482
+ logger.error(f"Error getting control assessment results for {control_id}: {e}")
483
+ return None
484
+
485
+ def get_assessment_framework_details(self, framework_id: str) -> Optional[Dict[str, Any]]:
486
+ """
487
+ Get detailed information about a framework.
488
+
489
+ :param str framework_id: Framework ID
490
+ :return: Framework details or None
491
+ :rtype: Optional[Dict[str, Any]]
492
+ """
493
+ try:
494
+ client = self._get_client("auditmanager")
495
+
496
+ response = client.get_assessment_framework(frameworkId=framework_id)
497
+ framework = response.get("framework", {})
498
+
499
+ return {
500
+ "id": framework.get("id"),
501
+ "arn": framework.get("arn"),
502
+ "name": framework.get("name"),
503
+ "type": framework.get("type"),
504
+ "complianceType": framework.get("complianceType"),
505
+ "description": framework.get("description"),
506
+ "controlSets": framework.get("controlSets", []),
507
+ "createdAt": str(framework.get("createdAt")) if framework.get("createdAt") else None,
508
+ }
509
+
510
+ except ClientError as e:
511
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "AccessDeniedException"]:
512
+ logger.error(f"Error getting framework details for {framework_id}: {e}")
513
+ return None