aws-cis-controls-assessment 1.0.3__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.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. docs/user-guide.md +487 -0
@@ -0,0 +1,313 @@
1
+ """AWS Client Factory for managing AWS service clients with credential handling."""
2
+
3
+ import boto3
4
+ import time
5
+ import random
6
+ from typing import Dict, List, Optional, Any
7
+ from botocore.exceptions import ClientError, NoCredentialsError, PartialCredentialsError
8
+ from botocore.config import Config
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AWSClientFactory:
15
+ """Centralized AWS service client management with credential handling."""
16
+
17
+ def __init__(self, credentials: Optional[Dict[str, str]] = None, regions: Optional[List[str]] = None):
18
+ """Initialize with AWS credentials and target regions.
19
+
20
+ Args:
21
+ credentials: Optional dict with AWS credentials (access_key_id, secret_access_key, session_token)
22
+ regions: List of AWS regions to support. If None, uses current region.
23
+ """
24
+ self.credentials = credentials or {}
25
+ self.regions = regions or ['us-east-1'] # Default to us-east-1 if not specified
26
+ self._clients = {} # Cache for boto3 clients
27
+ self._session = None
28
+ self._account_info = None
29
+
30
+ # Configure boto3 with retry settings
31
+ self._config = Config(
32
+ retries={
33
+ 'max_attempts': 3,
34
+ 'mode': 'adaptive'
35
+ },
36
+ max_pool_connections=50
37
+ )
38
+
39
+ # Initialize session
40
+ self._initialize_session()
41
+
42
+ def _initialize_session(self):
43
+ """Initialize boto3 session with provided credentials."""
44
+ try:
45
+ if self.credentials:
46
+ # Check if profile_name is provided
47
+ if 'profile_name' in self.credentials:
48
+ self._session = boto3.Session(
49
+ profile_name=self.credentials['profile_name'],
50
+ region_name=self.regions[0] if self.regions else None
51
+ )
52
+ else:
53
+ # Use explicit credentials
54
+ self._session = boto3.Session(
55
+ aws_access_key_id=self.credentials.get('aws_access_key_id'),
56
+ aws_secret_access_key=self.credentials.get('aws_secret_access_key'),
57
+ aws_session_token=self.credentials.get('aws_session_token'),
58
+ region_name=self.regions[0] if self.regions else None
59
+ )
60
+ else:
61
+ # Use default credential chain
62
+ self._session = boto3.Session(
63
+ region_name=self.regions[0] if self.regions else None
64
+ )
65
+
66
+ logger.info("AWS session initialized successfully")
67
+
68
+ except Exception as e:
69
+ logger.error(f"Failed to initialize AWS session: {e}")
70
+ raise
71
+
72
+ def get_client(self, service_name: str, region: Optional[str] = None) -> boto3.client:
73
+ """Get AWS service client for specified service and region.
74
+
75
+ Args:
76
+ service_name: AWS service name (e.g., 'ec2', 'iam', 's3')
77
+ region: AWS region. If None, uses first region from regions list.
78
+
79
+ Returns:
80
+ Boto3 client for the specified service
81
+
82
+ Raises:
83
+ ValueError: If region is not in supported regions list
84
+ ClientError: If client creation fails
85
+ """
86
+ if region is None:
87
+ region = self.regions[0]
88
+
89
+ if region not in self.regions:
90
+ raise ValueError(f"Region {region} not in supported regions: {self.regions}")
91
+
92
+ # Create cache key
93
+ cache_key = f"{service_name}_{region}"
94
+
95
+ # Return cached client if available
96
+ if cache_key in self._clients:
97
+ return self._clients[cache_key]
98
+
99
+ try:
100
+ # Create new client
101
+ client = self._session.client(
102
+ service_name,
103
+ region_name=region,
104
+ config=self._config
105
+ )
106
+
107
+ # Cache the client
108
+ self._clients[cache_key] = client
109
+
110
+ logger.debug(f"Created {service_name} client for region {region}")
111
+ return client
112
+
113
+ except Exception as e:
114
+ logger.error(f"Failed to create {service_name} client for region {region}: {e}")
115
+ raise
116
+
117
+ def validate_credentials(self) -> bool:
118
+ """Validate AWS credentials and permissions.
119
+
120
+ Returns:
121
+ True if credentials are valid, False otherwise
122
+ """
123
+ try:
124
+ sts_client = self.get_client('sts')
125
+ response = sts_client.get_caller_identity()
126
+
127
+ # Store account info for later use
128
+ self._account_info = {
129
+ 'account_id': response.get('Account'),
130
+ 'user_id': response.get('UserId'),
131
+ 'arn': response.get('Arn')
132
+ }
133
+
134
+ logger.info(f"Credentials validated for account: {self._account_info['account_id']}")
135
+ return True
136
+
137
+ except (NoCredentialsError, PartialCredentialsError) as e:
138
+ logger.error(f"Invalid credentials: {e}")
139
+ return False
140
+ except ClientError as e:
141
+ logger.error(f"AWS API error during credential validation: {e}")
142
+ return False
143
+ except Exception as e:
144
+ logger.error(f"Unexpected error during credential validation: {e}")
145
+ return False
146
+
147
+ def get_account_info(self) -> Dict[str, str]:
148
+ """Get AWS account ID and caller identity information.
149
+
150
+ Returns:
151
+ Dictionary with account_id, user_id, and arn
152
+
153
+ Raises:
154
+ RuntimeError: If credentials haven't been validated yet
155
+ """
156
+ if self._account_info is None:
157
+ if not self.validate_credentials():
158
+ raise RuntimeError("Failed to validate credentials")
159
+
160
+ return self._account_info.copy()
161
+
162
+ def test_service_access(self, service_name: str, region: Optional[str] = None) -> bool:
163
+ """Test access to a specific AWS service.
164
+
165
+ Args:
166
+ service_name: AWS service name to test
167
+ region: AWS region to test. If None, uses first region.
168
+
169
+ Returns:
170
+ True if service is accessible, False otherwise
171
+ """
172
+ try:
173
+ client = self.get_client(service_name, region)
174
+
175
+ # Test with a simple, low-cost API call for each service
176
+ test_calls = {
177
+ 'ec2': lambda c: c.describe_regions(),
178
+ 'iam': lambda c: c.get_account_summary(),
179
+ 's3': lambda c: c.list_buckets(),
180
+ 'rds': lambda c: c.describe_db_instances(MaxRecords=1),
181
+ 'cloudtrail': lambda c: c.describe_trails(),
182
+ 'config': lambda c: c.describe_configuration_recorders(),
183
+ 'guardduty': lambda c: c.list_detectors(MaxResults=1),
184
+ 'organizations': lambda c: c.describe_organization(),
185
+ 'ssm': lambda c: c.describe_instance_information(MaxResults=1),
186
+ 'elb': lambda c: c.describe_load_balancers(PageSize=1),
187
+ 'elbv2': lambda c: c.describe_load_balancers(PageSize=1),
188
+ 'apigateway': lambda c: c.get_rest_apis(limit=1),
189
+ 'apigatewayv2': lambda c: c.get_apis(MaxResults='1'),
190
+ 'kms': lambda c: c.list_keys(Limit=1),
191
+ 'backup': lambda c: c.list_backup_vaults(MaxResults=1),
192
+ 'dynamodb': lambda c: c.list_tables(Limit=1)
193
+ }
194
+
195
+ if service_name in test_calls:
196
+ test_calls[service_name](client)
197
+ else:
198
+ # Generic test - try to get service's waiter names
199
+ client.waiter_names
200
+
201
+ logger.debug(f"Service {service_name} is accessible in region {region}")
202
+ return True
203
+
204
+ except ClientError as e:
205
+ error_code = e.response.get('Error', {}).get('Code', '')
206
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
207
+ logger.debug(f"Access denied for {service_name} in region {region}")
208
+ else:
209
+ logger.debug(f"Service {service_name} test failed in region {region}: {error_code}")
210
+ return False
211
+ except Exception as e:
212
+ # Don't log parameter validation errors as warnings - they're expected for some services
213
+ if "Parameter validation failed" in str(e):
214
+ logger.debug(f"Parameter validation issue for {service_name} in region {region}: {e}")
215
+ else:
216
+ logger.warning(f"Unexpected error testing {service_name} in region {region}: {e}")
217
+ return False
218
+
219
+ def get_available_regions(self, service_name: str) -> List[str]:
220
+ """Get list of regions where a service is available.
221
+
222
+ Args:
223
+ service_name: AWS service name
224
+
225
+ Returns:
226
+ List of region names where service is available
227
+ """
228
+ available_regions = []
229
+
230
+ for region in self.regions:
231
+ if self.test_service_access(service_name, region):
232
+ available_regions.append(region)
233
+
234
+ return available_regions
235
+
236
+ def aws_api_call_with_retry(self, func, max_retries: int = 3, base_delay: float = 1.0) -> Any:
237
+ """Execute AWS API call with exponential backoff retry logic.
238
+
239
+ Args:
240
+ func: Function to execute (should be a lambda or callable)
241
+ max_retries: Maximum number of retry attempts
242
+ base_delay: Base delay in seconds for exponential backoff
243
+
244
+ Returns:
245
+ Result of the function call
246
+
247
+ Raises:
248
+ ClientError: If all retries are exhausted
249
+ """
250
+ for attempt in range(max_retries + 1):
251
+ try:
252
+ return func()
253
+
254
+ except ClientError as e:
255
+ error_code = e.response.get('Error', {}).get('Code', '')
256
+
257
+ # Retry on throttling errors
258
+ if error_code in ['Throttling', 'RequestLimitExceeded', 'TooManyRequestsException']:
259
+ if attempt < max_retries:
260
+ # Exponential backoff with jitter
261
+ delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
262
+ logger.warning(f"API throttled, retrying in {delay:.2f} seconds (attempt {attempt + 1}/{max_retries + 1})")
263
+ time.sleep(delay)
264
+ continue
265
+
266
+ # Don't retry on other errors
267
+ raise
268
+
269
+ except Exception as e:
270
+ # Don't retry on non-AWS errors
271
+ logger.error(f"Non-retryable error in AWS API call: {e}")
272
+ raise
273
+
274
+ # This should never be reached due to the raise in the loop
275
+ raise ClientError(
276
+ error_response={'Error': {'Code': 'MaxRetriesExceeded', 'Message': 'Maximum retries exceeded'}},
277
+ operation_name='aws_api_call_with_retry'
278
+ )
279
+
280
+ def get_supported_services(self) -> List[str]:
281
+ """Get list of AWS services supported by this client factory.
282
+
283
+ Returns:
284
+ List of supported AWS service names
285
+ """
286
+ return [
287
+ 'ec2', 'iam', 's3', 'rds', 'cloudtrail', 'cloudwatch', 'logs',
288
+ 'config', 'guardduty', 'inspector', 'kms', 'organizations',
289
+ 'ssm', 'securityhub', 'macie2', 'backup', 'dynamodb',
290
+ 'elb', 'elbv2', 'apigateway',
291
+ 'apigatewayv2', 'redshift', 'efs', 'sns', 'sqs', 'lambda',
292
+ 'ecs', 'ecr', 'codebuild', 'elasticsearch', 'opensearch'
293
+ ]
294
+
295
+ def get_enabled_regions(self) -> List[str]:
296
+ """Get list of enabled AWS regions for the current account.
297
+
298
+ Returns:
299
+ List of enabled AWS region names
300
+ """
301
+ try:
302
+ ec2_client = self.get_client('ec2', 'us-east-1')
303
+ response = ec2_client.describe_regions()
304
+ return [region['RegionName'] for region in response['Regions']]
305
+ except Exception as e:
306
+ logger.warning(f"Could not retrieve enabled regions: {e}")
307
+ # Return default region as fallback
308
+ return ['us-east-1']
309
+
310
+ def cleanup(self):
311
+ """Clean up resources and close connections."""
312
+ self._clients.clear()
313
+ logger.info("AWS client factory cleaned up")