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
@@ -1,11 +1,12 @@
1
1
  """AWS compute resource collectors."""
2
2
 
3
3
  import logging
4
- from typing import Dict, List, Any, TYPE_CHECKING
4
+ from typing import Dict, List, Any, Optional, TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  import boto3
8
8
 
9
+ from regscale.integrations.commercial.aws.inventory.resources.systems_manager import SystemsManagerCollector
9
10
  from regscale.integrations.commercial.aws.inventory.base import BaseCollector
10
11
 
11
12
  logger = logging.getLogger("regscale")
@@ -14,14 +15,27 @@ logger = logging.getLogger("regscale")
14
15
  class ComputeCollector(BaseCollector):
15
16
  """Collector for AWS compute resources."""
16
17
 
17
- def __init__(self, session: "boto3.Session", region: str):
18
+ def __init__(
19
+ self,
20
+ session: "boto3.Session",
21
+ region: str,
22
+ account_id: Optional[str] = None,
23
+ tags: Optional[Dict[str, str]] = None,
24
+ enabled_services: Optional[Dict[str, bool]] = None,
25
+ ):
18
26
  """
19
27
  Initialize the compute collector.
20
28
 
21
29
  :param boto3.Session session: AWS session
22
30
  :param str region: AWS region
31
+ :param str account_id: Optional AWS account ID to filter resources
32
+ :param dict tags: Optional tags to filter resources (key-value pairs)
33
+ :param dict enabled_services: Optional dict of service names to boolean flags for enabling/disabling collection
23
34
  """
24
35
  super().__init__(session, region)
36
+ self.account_id = account_id
37
+ self.tags = tags or {}
38
+ self.enabled_services = enabled_services or {}
25
39
  self.ec2_client = self._get_client("ec2")
26
40
  self.logger = logging.getLogger("regscale")
27
41
 
@@ -124,8 +138,12 @@ class ComputeCollector(BaseCollector):
124
138
  # Collect instance information
125
139
  for page in paginator.paginate():
126
140
  for reservation in page.get("Reservations", []):
141
+ # Get account ID from reservation
142
+ owner_id = reservation.get("OwnerId", "")
127
143
  for instance in reservation.get("Instances", []):
128
144
  instance_data = self._build_instance_data(instance, ami_details)
145
+ # Add owner ID for ARN construction
146
+ instance_data["OwnerId"] = owner_id
129
147
  instances.append(instance_data)
130
148
 
131
149
  except Exception as e:
@@ -152,6 +170,7 @@ class ComputeCollector(BaseCollector):
152
170
  {
153
171
  "Region": self.region,
154
172
  "FunctionName": function.get("FunctionName"),
173
+ "FunctionArn": function.get("FunctionArn"),
155
174
  "Runtime": function.get("Runtime"),
156
175
  "Handler": function.get("Handler"),
157
176
  "CodeSize": function.get("CodeSize"),
@@ -220,15 +239,53 @@ class ComputeCollector(BaseCollector):
220
239
  self._handle_error(e, "ECS clusters")
221
240
  return clusters
222
241
 
242
+ def get_systems_manager_info(self) -> Dict[str, Any]:
243
+ """
244
+ Get information about Systems Manager resources.
245
+
246
+ :return: Dictionary containing Systems Manager information
247
+ :rtype: Dict[str, Any]
248
+ """
249
+ try:
250
+ ssm_collector = SystemsManagerCollector(self.session, self.region, self.account_id, self.tags)
251
+ return ssm_collector.collect()
252
+ except Exception as e:
253
+ self._handle_error(e, "Systems Manager resources")
254
+ return {
255
+ "ManagedInstances": [],
256
+ "Parameters": [],
257
+ "Documents": [],
258
+ "PatchBaselines": [],
259
+ "MaintenanceWindows": [],
260
+ "Associations": [],
261
+ "InventoryEntries": [],
262
+ "ComplianceSummary": {},
263
+ }
264
+
223
265
  def collect(self) -> Dict[str, Any]:
224
266
  """
225
- Collect all compute resources.
267
+ Collect compute resources based on enabled_services configuration.
226
268
 
227
- :return: Dictionary containing all compute resource information
269
+ :return: Dictionary containing enabled compute resource information
228
270
  :rtype: Dict[str, Any]
229
271
  """
230
- return {
231
- "EC2Instances": self.get_ec2_instances(),
232
- "LambdaFunctions": self.get_lambda_functions(),
233
- "ECSClusters": self.get_ecs_clusters(),
234
- }
272
+ result = {}
273
+
274
+ # EC2 Instances
275
+ if self.enabled_services.get("ec2", True):
276
+ result["EC2Instances"] = self.get_ec2_instances()
277
+
278
+ # Lambda Functions
279
+ if self.enabled_services.get("lambda", True):
280
+ result["LambdaFunctions"] = self.get_lambda_functions()
281
+
282
+ # ECS Clusters
283
+ if self.enabled_services.get("ecs", True):
284
+ result["ECSClusters"] = self.get_ecs_clusters()
285
+
286
+ # Systems Manager
287
+ if self.enabled_services.get("systems_manager", True):
288
+ ssm_info = self.get_systems_manager_info()
289
+ result.update(ssm_info)
290
+
291
+ return result
@@ -0,0 +1,464 @@
1
+ """AWS Config 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 ConfigCollector(BaseCollector):
14
+ """Collector for AWS Config 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 Config collector with filtering support.
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, account_id, tags)
28
+
29
+ def collect(self) -> Dict[str, Any]:
30
+ """
31
+ Collect AWS Config resources.
32
+
33
+ :return: Dictionary containing Config recorders, rules, and compliance information
34
+ :rtype: Dict[str, Any]
35
+ """
36
+ result = {
37
+ "ConfigurationRecorders": [],
38
+ "RecorderStatuses": [],
39
+ "DeliveryChannels": [],
40
+ "ConfigRules": [],
41
+ "ComplianceSummary": [],
42
+ }
43
+
44
+ try:
45
+ client = self._get_client("config")
46
+
47
+ # Collect basic Config resources
48
+ result["ConfigurationRecorders"] = self._describe_configuration_recorders(client)
49
+ result["RecorderStatuses"] = self._describe_configuration_recorder_status(client)
50
+ result["DeliveryChannels"] = self._describe_delivery_channels(client)
51
+
52
+ # Get and filter config rules
53
+ config_rules = self._describe_config_rules(client)
54
+ filtered_rules = self._filter_config_rules(client, config_rules)
55
+ result["ConfigRules"] = filtered_rules
56
+
57
+ # Get compliance information
58
+ result["ComplianceSummary"] = self._collect_compliance_summary(client, filtered_rules)
59
+
60
+ logger.info(
61
+ f"Collected {len(result['ConfigurationRecorders'])} Config recorder(s), "
62
+ f"{len(filtered_rules)} rule(s) from {self.region}"
63
+ )
64
+
65
+ except ClientError as e:
66
+ self._handle_error(e, "AWS Config resources")
67
+ except Exception as e:
68
+ logger.error(f"Unexpected error collecting AWS Config resources: {e}", exc_info=True)
69
+
70
+ return result
71
+
72
+ def _filter_config_rules(self, client: Any, config_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
73
+ """
74
+ Filter config rules by account ID and tags if specified.
75
+
76
+ :param client: Config client
77
+ :param List[Dict[str, Any]] config_rules: List of config rules to filter
78
+ :return: Filtered list of config rules
79
+ :rtype: List[Dict[str, Any]]
80
+ """
81
+ filtered_rules = []
82
+ for rule in config_rules:
83
+ if self._should_include_rule(client, rule):
84
+ filtered_rules.append(rule)
85
+ return filtered_rules
86
+
87
+ def _should_include_rule(self, client: Any, rule: Dict[str, Any]) -> bool:
88
+ """
89
+ Determine if a config rule should be included based on filters.
90
+
91
+ :param client: Config client
92
+ :param Dict[str, Any] rule: Config rule to check
93
+ :return: True if rule should be included, False otherwise
94
+ :rtype: bool
95
+ """
96
+ rule_arn = rule.get("ConfigRuleArn", "")
97
+
98
+ # Filter by account ID if specified using BaseCollector method
99
+ if not self._matches_account(rule_arn):
100
+ logger.debug(f"Skipping rule {rule_arn} - does not match account ID {self.account_id}")
101
+ return False
102
+
103
+ # Get tags for filtering using BaseCollector method
104
+ if self.tags:
105
+ rule_tags = self._get_rule_tags(client, rule_arn)
106
+ if not self._matches_tags(rule_tags):
107
+ logger.debug(f"Skipping rule {rule_arn} - does not match tag filters")
108
+ return False
109
+ rule["Tags"] = rule_tags
110
+
111
+ return True
112
+
113
+ def _collect_compliance_summary(self, client: Any, filtered_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
114
+ """
115
+ Collect compliance information for filtered rules.
116
+
117
+ :param client: Config client
118
+ :param List[Dict[str, Any]] filtered_rules: List of filtered config rules
119
+ :return: List of compliance summaries
120
+ :rtype: List[Dict[str, Any]]
121
+ """
122
+ compliance_summary = []
123
+ for rule in filtered_rules:
124
+ rule_name = rule.get("ConfigRuleName")
125
+ if rule_name:
126
+ compliance = self._describe_compliance_by_config_rule(client, rule_name)
127
+ if compliance:
128
+ compliance_summary.append(compliance)
129
+ return compliance_summary
130
+
131
+ def _describe_configuration_recorders(self, client: Any) -> List[Dict[str, Any]]:
132
+ """
133
+ Describe configuration recorders.
134
+
135
+ :param client: Config client
136
+ :return: List of configuration recorders
137
+ :rtype: List[Dict[str, Any]]
138
+ """
139
+ try:
140
+ response = client.describe_configuration_recorders()
141
+ recorders = response.get("ConfigurationRecorders", [])
142
+
143
+ # Add region information
144
+ for recorder in recorders:
145
+ recorder["Region"] = self.region
146
+
147
+ return recorders
148
+ except ClientError as e:
149
+ if e.response["Error"]["Code"] == "AccessDeniedException":
150
+ logger.warning(f"Access denied to describe configuration recorders in {self.region}")
151
+ return []
152
+ raise
153
+
154
+ def _describe_configuration_recorder_status(self, client: Any) -> List[Dict[str, Any]]:
155
+ """
156
+ Describe configuration recorder status.
157
+
158
+ :param client: Config client
159
+ :return: List of recorder statuses
160
+ :rtype: List[Dict[str, Any]]
161
+ """
162
+ try:
163
+ response = client.describe_configuration_recorder_status()
164
+ statuses = response.get("ConfigurationRecordersStatus", [])
165
+
166
+ # Add region information
167
+ for status in statuses:
168
+ status["Region"] = self.region
169
+
170
+ return statuses
171
+ except ClientError as e:
172
+ if e.response["Error"]["Code"] == "AccessDeniedException":
173
+ logger.warning(f"Access denied to describe configuration recorder status in {self.region}")
174
+ return []
175
+ logger.error(f"Error describing configuration recorder status: {e}")
176
+ return []
177
+
178
+ def _describe_delivery_channels(self, client: Any) -> List[Dict[str, Any]]:
179
+ """
180
+ Describe delivery channels.
181
+
182
+ :param client: Config client
183
+ :return: List of delivery channels
184
+ :rtype: List[Dict[str, Any]]
185
+ """
186
+ try:
187
+ response = client.describe_delivery_channels()
188
+ channels = response.get("DeliveryChannels", [])
189
+
190
+ # Add region information
191
+ for channel in channels:
192
+ channel["Region"] = self.region
193
+
194
+ return channels
195
+ except ClientError as e:
196
+ if e.response["Error"]["Code"] == "AccessDeniedException":
197
+ logger.warning(f"Access denied to describe delivery channels in {self.region}")
198
+ return []
199
+ logger.error(f"Error describing delivery channels: {e}")
200
+ return []
201
+
202
+ def _describe_config_rules(self, client: Any) -> List[Dict[str, Any]]:
203
+ """
204
+ Describe AWS Config rules with pagination support.
205
+
206
+ :param client: Config client
207
+ :return: List of config rules
208
+ :rtype: List[Dict[str, Any]]
209
+ """
210
+ rules = []
211
+ next_token = None
212
+
213
+ try:
214
+ while True:
215
+ params = {}
216
+ if next_token:
217
+ params["NextToken"] = next_token
218
+
219
+ response = client.describe_config_rules(**params)
220
+ rules.extend(response.get("ConfigRules", []))
221
+
222
+ next_token = response.get("NextToken")
223
+ if not next_token:
224
+ break
225
+
226
+ # Add region information
227
+ for rule in rules:
228
+ rule["Region"] = self.region
229
+
230
+ except ClientError as e:
231
+ if e.response["Error"]["Code"] == "AccessDeniedException":
232
+ logger.warning(f"Access denied to describe config rules in {self.region}")
233
+ else:
234
+ logger.error(f"Error describing config rules: {e}")
235
+
236
+ return rules
237
+
238
+ def _describe_compliance_by_config_rule(self, client: Any, rule_name: str) -> Optional[Dict[str, Any]]:
239
+ """
240
+ Get compliance information for a specific config rule.
241
+
242
+ :param client: Config client
243
+ :param str rule_name: Name of the config rule
244
+ :return: Compliance information or None
245
+ :rtype: Optional[Dict[str, Any]]
246
+ """
247
+ try:
248
+ response = client.describe_compliance_by_config_rule(ConfigRuleNames=[rule_name])
249
+ compliance_by_rules = response.get("ComplianceByConfigRules", [])
250
+
251
+ if compliance_by_rules:
252
+ compliance = compliance_by_rules[0]
253
+ compliance["Region"] = self.region
254
+ return compliance
255
+
256
+ return None
257
+ except ClientError as e:
258
+ if e.response["Error"]["Code"] != "AccessDeniedException":
259
+ logger.error(f"Error getting compliance for rule {rule_name}: {e}")
260
+ return None
261
+
262
+ def get_compliance_details(
263
+ self, rule_name: str, compliance_types: Optional[List[str]] = None
264
+ ) -> List[Dict[str, Any]]:
265
+ """
266
+ Get detailed compliance information for a config rule.
267
+
268
+ :param str rule_name: Name of the config rule
269
+ :param List[str] compliance_types: Optional list of compliance types to filter
270
+ :return: List of compliance details
271
+ :rtype: List[Dict[str, Any]]
272
+ """
273
+ details = []
274
+ next_token = None
275
+
276
+ try:
277
+ client = self._get_client("config")
278
+
279
+ while True:
280
+ params = {"ConfigRuleName": rule_name}
281
+
282
+ if compliance_types:
283
+ params["ComplianceTypes"] = compliance_types
284
+
285
+ if next_token:
286
+ params["NextToken"] = next_token
287
+
288
+ response = client.get_compliance_details_by_config_rule(**params)
289
+ evaluation_results = response.get("EvaluationResults", [])
290
+
291
+ for result in evaluation_results:
292
+ result["Region"] = self.region
293
+
294
+ details.extend(evaluation_results)
295
+
296
+ next_token = response.get("NextToken")
297
+ if not next_token:
298
+ break
299
+
300
+ except ClientError as e:
301
+ self._handle_error(e, f"compliance details for rule {rule_name}")
302
+
303
+ return details
304
+
305
+ def _get_rule_tags(self, client: Any, rule_arn: str) -> Dict[str, str]:
306
+ """
307
+ Get tags for an AWS Config rule.
308
+
309
+ :param client: Config client
310
+ :param str rule_arn: Config rule ARN
311
+ :return: Dictionary of tags (TagKey -> TagValue)
312
+ :rtype: Dict[str, str]
313
+ """
314
+ try:
315
+ response = client.list_tags_for_resource(ResourceArn=rule_arn)
316
+ tags_list = response.get("Tags", [])
317
+ return {tag["Key"]: tag["Value"] for tag in tags_list}
318
+ except ClientError as e:
319
+ logger.debug(f"Error getting tags for config rule {rule_arn}: {e}")
320
+ return {}
321
+
322
+ def get_conformance_packs(self) -> List[Dict[str, Any]]:
323
+ """
324
+ Get deployed conformance packs.
325
+
326
+ :return: List of conformance packs
327
+ :rtype: List[Dict[str, Any]]
328
+ """
329
+ packs = []
330
+ next_token = None
331
+
332
+ try:
333
+ client = self._get_client("config")
334
+
335
+ while True:
336
+ params = {}
337
+ if next_token:
338
+ params["NextToken"] = next_token
339
+
340
+ response = client.describe_conformance_packs(**params)
341
+ pack_details = response.get("ConformancePackDetails", [])
342
+
343
+ for pack in pack_details:
344
+ pack["Region"] = self.region
345
+
346
+ packs.extend(pack_details)
347
+
348
+ next_token = response.get("NextToken")
349
+ if not next_token:
350
+ break
351
+
352
+ except ClientError as e:
353
+ if e.response["Error"]["Code"] == "AccessDeniedException":
354
+ logger.warning(f"Access denied to describe conformance packs in {self.region}")
355
+ else:
356
+ logger.error(f"Error describing conformance packs: {e}")
357
+
358
+ return packs
359
+
360
+ def get_conformance_pack_compliance(self, pack_name: str) -> Dict[str, Any]:
361
+ """
362
+ Get compliance status for a conformance pack.
363
+
364
+ :param str pack_name: Name of the conformance pack
365
+ :return: Conformance pack compliance status
366
+ :rtype: Dict[str, Any]
367
+ """
368
+ try:
369
+ client = self._get_client("config")
370
+ response = client.describe_conformance_pack_status(ConformancePackNames=[pack_name])
371
+ statuses = response.get("ConformancePackStatusDetails", [])
372
+
373
+ if statuses:
374
+ status = statuses[0]
375
+ status["Region"] = self.region
376
+ return status
377
+
378
+ return {}
379
+ except ClientError as e:
380
+ if e.response["Error"]["Code"] != "AccessDeniedException":
381
+ logger.error(f"Error getting conformance pack compliance for {pack_name}: {e}")
382
+ return {}
383
+
384
+ def get_conformance_pack_compliance_details(self, pack_name: str) -> List[Dict[str, Any]]:
385
+ """
386
+ Get detailed compliance information for all rules in a conformance pack.
387
+
388
+ :param str pack_name: Name of the conformance pack
389
+ :return: List of rule compliance details
390
+ :rtype: List[Dict[str, Any]]
391
+ """
392
+ details = []
393
+ next_token = None
394
+
395
+ try:
396
+ client = self._get_client("config")
397
+
398
+ while True:
399
+ params = {"ConformancePackName": pack_name}
400
+
401
+ if next_token:
402
+ params["NextToken"] = next_token
403
+
404
+ response = client.get_conformance_pack_compliance_details(**params)
405
+ rule_details = response.get("ConformancePackRuleCompliances", [])
406
+
407
+ for rule in rule_details:
408
+ rule["Region"] = self.region
409
+ rule["ConformancePackName"] = pack_name
410
+
411
+ details.extend(rule_details)
412
+
413
+ next_token = response.get("NextToken")
414
+ if not next_token:
415
+ break
416
+
417
+ except ClientError as e:
418
+ self._handle_error(e, f"conformance pack compliance details for {pack_name}")
419
+
420
+ return details
421
+
422
+ def get_aggregate_compliance_by_control(
423
+ self, control_mappings: Dict[str, List[str]]
424
+ ) -> Dict[str, List[Dict[str, Any]]]:
425
+ """
426
+ Aggregate Config rule compliance by control ID.
427
+
428
+ :param Dict[str, List[str]] control_mappings: Map of control_id -> list of rule names
429
+ :return: Dictionary mapping control_id to list of rule evaluation results
430
+ :rtype: Dict[str, List[Dict[str, Any]]]
431
+ """
432
+ control_compliance = {}
433
+
434
+ try:
435
+ client = self._get_client("config")
436
+
437
+ for control_id, rule_names in control_mappings.items():
438
+ control_compliance[control_id] = []
439
+
440
+ for rule_name in rule_names:
441
+ # Get compliance summary for this rule
442
+ compliance = self._describe_compliance_by_config_rule(client, rule_name)
443
+
444
+ if compliance:
445
+ # Get detailed evaluation results
446
+ details = self.get_compliance_details(rule_name)
447
+
448
+ evaluation_result = {
449
+ "control_id": control_id,
450
+ "rule_name": rule_name,
451
+ "compliance_type": compliance.get("Compliance", {}).get("ComplianceType", ""),
452
+ "compliance_summary": compliance.get("Compliance", {}),
453
+ "evaluation_details": details,
454
+ "non_compliant_resource_count": sum(
455
+ 1 for d in details if d.get("ComplianceType") == "NON_COMPLIANT"
456
+ ),
457
+ }
458
+
459
+ control_compliance[control_id].append(evaluation_result)
460
+
461
+ except Exception as e:
462
+ logger.error(f"Error aggregating compliance by control: {e}", exc_info=True)
463
+
464
+ return control_compliance