regscale-cli 6.27.2.0__py3-none-any.whl → 6.28.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +11 -2
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +15 -1
- regscale/integrations/commercial/amazon/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/amazon/common.py +204 -0
- regscale/integrations/commercial/amazon/common.py +48 -58
- regscale/integrations/commercial/aws/audit_manager_compliance.py +2671 -0
- regscale/integrations/commercial/aws/cli.py +3093 -55
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +223 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +66 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +106 -31
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +103 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +53 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +853 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +415 -85
- regscale/models/integration_models/cisa_kev_data.json +80 -20
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +44 -3
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/issue.py +2 -5
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +140 -57
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/test_aws.py +55 -56
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -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__(
|
|
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
|
|
267
|
+
Collect compute resources based on enabled_services configuration.
|
|
226
268
|
|
|
227
|
-
:return: Dictionary containing
|
|
269
|
+
:return: Dictionary containing enabled compute resource information
|
|
228
270
|
:rtype: Dict[str, Any]
|
|
229
271
|
"""
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|