aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.2__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 (45) hide show
  1. aws_cis_assessment/__init__.py +4 -4
  2. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
  3. aws_cis_assessment/controls/base_control.py +106 -24
  4. aws_cis_assessment/controls/ig1/__init__.py +144 -15
  5. aws_cis_assessment/controls/ig1/control_4_1.py +4 -4
  6. aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
  7. aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
  8. aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
  9. aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
  10. aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
  11. aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
  12. aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
  13. aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
  14. aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
  15. aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
  16. aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
  17. aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
  18. aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
  19. aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
  20. aws_cis_assessment/controls/ig1/control_macie.py +165 -0
  21. aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
  22. aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
  23. aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
  24. aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
  25. aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
  26. aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
  27. aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
  28. aws_cis_assessment/controls/ig1/control_version_mgmt.py +337 -0
  29. aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
  30. aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
  31. aws_cis_assessment/core/assessment_engine.py +160 -11
  32. aws_cis_assessment/core/aws_client_factory.py +17 -5
  33. aws_cis_assessment/core/models.py +20 -1
  34. aws_cis_assessment/core/scoring_engine.py +102 -1
  35. aws_cis_assessment/reporters/base_reporter.py +58 -13
  36. aws_cis_assessment/reporters/html_reporter.py +186 -9
  37. aws_cis_controls_assessment-1.2.2.dist-info/METADATA +320 -0
  38. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/RECORD +44 -20
  39. docs/developer-guide.md +204 -5
  40. docs/user-guide.md +137 -4
  41. aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
  42. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/WHEEL +0 -0
  43. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/entry_points.txt +0 -0
  44. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/licenses/LICENSE +0 -0
  45. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,626 @@
1
+ """
2
+ CIS Control 2.1-2.3 - Patch Management Controls
3
+ Ensures systems are properly patched and patch compliance is maintained.
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict, Any
8
+ from botocore.exceptions import ClientError
9
+
10
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
11
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
12
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SSMPatchManagerEnabledAssessment(BaseConfigRuleAssessment):
18
+ """
19
+ CIS Control 2.1 - Establish and Maintain a Software Inventory
20
+ AWS Config Rule: ssm-patch-manager-enabled
21
+
22
+ Ensures AWS Systems Manager Patch Manager is configured with patch baselines
23
+ for automated patch management across EC2 instances.
24
+ """
25
+
26
+ def __init__(self):
27
+ super().__init__(
28
+ rule_name="ssm-patch-manager-enabled",
29
+ control_id="2.1",
30
+ resource_types=["AWS::::Account"]
31
+ )
32
+
33
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
34
+ """Get SSM Patch Manager configuration for the account."""
35
+ if resource_type != "AWS::::Account":
36
+ return []
37
+
38
+ try:
39
+ ssm_client = aws_factory.get_client('ssm', region)
40
+
41
+ # Get all patch baselines
42
+ baselines = []
43
+ paginator = ssm_client.get_paginator('describe_patch_baselines')
44
+
45
+ for page in paginator.paginate():
46
+ baselines.extend(page.get('BaselineIdentities', []))
47
+
48
+ # Get default patch baseline for each OS
49
+ default_baselines = {}
50
+ for os_type in ['WINDOWS', 'AMAZON_LINUX', 'AMAZON_LINUX_2', 'UBUNTU', 'REDHAT_ENTERPRISE_LINUX',
51
+ 'SUSE', 'CENTOS', 'ORACLE_LINUX', 'DEBIAN', 'MACOS']:
52
+ try:
53
+ response = ssm_client.get_default_patch_baseline(OperatingSystem=os_type)
54
+ default_baselines[os_type] = response.get('BaselineId', '')
55
+ except ClientError:
56
+ # No default baseline for this OS type
57
+ pass
58
+
59
+ # Check if any patch baselines are configured
60
+ has_baselines = len(baselines) > 0
61
+ has_default_baselines = len(default_baselines) > 0
62
+
63
+ return [{
64
+ 'AccountId': aws_factory.get_account_info().get('account_id', 'unknown'),
65
+ 'Region': region,
66
+ 'TotalBaselines': len(baselines),
67
+ 'Baselines': baselines[:10], # Limit to first 10 for display
68
+ 'DefaultBaselines': default_baselines,
69
+ 'HasBaselines': has_baselines,
70
+ 'HasDefaultBaselines': has_default_baselines
71
+ }]
72
+
73
+ except ClientError as e:
74
+ error_code = e.response.get('Error', {}).get('Code', '')
75
+ if error_code == 'AccessDeniedException':
76
+ logger.warning(f"Access denied to check SSM Patch Manager in {region}")
77
+ else:
78
+ logger.error(f"Error checking SSM Patch Manager in {region}: {e}")
79
+ return []
80
+
81
+ def _evaluate_resource_compliance(
82
+ self,
83
+ resource: Dict[str, Any],
84
+ aws_factory: AWSClientFactory,
85
+ region: str
86
+ ) -> ComplianceResult:
87
+ """Evaluate if SSM Patch Manager is properly configured."""
88
+ account_id = resource.get('AccountId', 'unknown')
89
+ has_baselines = resource.get('HasBaselines', False)
90
+ has_default_baselines = resource.get('HasDefaultBaselines', False)
91
+ total_baselines = resource.get('TotalBaselines', 0)
92
+ default_baselines = resource.get('DefaultBaselines', {})
93
+
94
+ # Check if patch management is configured
95
+ is_compliant = has_baselines and has_default_baselines
96
+
97
+ if is_compliant:
98
+ os_list = ', '.join(default_baselines.keys())
99
+ evaluation_reason = (
100
+ f"SSM Patch Manager is configured in {region} with {total_baselines} patch baseline(s). "
101
+ f"Default baselines configured for: {os_list}"
102
+ )
103
+ compliance_status = ComplianceStatus.COMPLIANT
104
+ else:
105
+ if not has_baselines:
106
+ evaluation_reason = f"No patch baselines configured in SSM Patch Manager in {region}."
107
+ elif not has_default_baselines:
108
+ evaluation_reason = (
109
+ f"SSM Patch Manager has {total_baselines} baseline(s) but no default baselines "
110
+ f"are configured for any operating systems in {region}."
111
+ )
112
+ else:
113
+ evaluation_reason = f"SSM Patch Manager is not properly configured in {region}."
114
+ compliance_status = ComplianceStatus.NON_COMPLIANT
115
+
116
+ return ComplianceResult(
117
+ resource_id=f"{account_id}-{region}",
118
+ resource_type="AWS::::Account",
119
+ compliance_status=compliance_status,
120
+ evaluation_reason=evaluation_reason,
121
+ config_rule_name=self.rule_name,
122
+ region=region
123
+ )
124
+
125
+ def _get_rule_remediation_steps(self) -> List[str]:
126
+ """Get remediation steps for configuring SSM Patch Manager."""
127
+ return [
128
+ "1. Configure SSM Patch Manager with default patch baselines:",
129
+ " # Set default patch baseline for Amazon Linux 2",
130
+ " aws ssm register-default-patch-baseline \\",
131
+ " --baseline-id <baseline-id> \\",
132
+ " --region <region>",
133
+ "",
134
+ "2. Create a custom patch baseline (example for Amazon Linux 2):",
135
+ " aws ssm create-patch-baseline \\",
136
+ " --name 'AmazonLinux2-CustomBaseline' \\",
137
+ " --operating-system AMAZON_LINUX_2 \\",
138
+ " --approval-rules 'PatchRules=[{PatchFilterGroup={PatchFilters=[{Key=CLASSIFICATION,Values=[Security,Bugfix]},{Key=SEVERITY,Values=[Critical,Important]}]},ApprovalAfterDays=7}]' \\",
139
+ " --description 'Custom patch baseline for Amazon Linux 2' \\",
140
+ " --region <region>",
141
+ "",
142
+ "3. Register the custom baseline as default:",
143
+ " aws ssm register-default-patch-baseline \\",
144
+ " --baseline-id <baseline-id-from-step-2> \\",
145
+ " --region <region>",
146
+ "",
147
+ "4. Create patch baselines for all OS types in use:",
148
+ " # Windows",
149
+ " aws ssm create-patch-baseline \\",
150
+ " --name 'Windows-CustomBaseline' \\",
151
+ " --operating-system WINDOWS \\",
152
+ " --approval-rules 'PatchRules=[{PatchFilterGroup={PatchFilters=[{Key=CLASSIFICATION,Values=[SecurityUpdates,CriticalUpdates]},{Key=MSRC_SEVERITY,Values=[Critical,Important]}]},ApprovalAfterDays=7}]' \\",
153
+ " --region <region>",
154
+ "",
155
+ " # Ubuntu",
156
+ " aws ssm create-patch-baseline \\",
157
+ " --name 'Ubuntu-CustomBaseline' \\",
158
+ " --operating-system UBUNTU \\",
159
+ " --approval-rules 'PatchRules=[{PatchFilterGroup={PatchFilters=[{Key=PRIORITY,Values=[Required,Important]}]},ApprovalAfterDays=7}]' \\",
160
+ " --region <region>",
161
+ "",
162
+ "5. Console method:",
163
+ " - Navigate to AWS Systems Manager",
164
+ " - Click 'Patch Manager' in the left menu",
165
+ " - Click 'Configure patching'",
166
+ " - Select 'Patch baselines'",
167
+ " - Click 'Create patch baseline'",
168
+ " - Configure baseline settings:",
169
+ " * Name and description",
170
+ " * Operating system",
171
+ " * Approval rules (auto-approve after X days)",
172
+ " * Patch exceptions (if needed)",
173
+ " - Click 'Create patch baseline'",
174
+ " - Set as default: Actions > Set default patch baseline",
175
+ "",
176
+ "6. Create a maintenance window for automated patching:",
177
+ " aws ssm create-maintenance-window \\",
178
+ " --name 'PatchingWindow' \\",
179
+ " --schedule 'cron(0 2 ? * SUN *)' \\",
180
+ " --duration 4 \\",
181
+ " --cutoff 1 \\",
182
+ " --allow-unassociated-targets \\",
183
+ " --region <region>",
184
+ "",
185
+ "7. Register targets for the maintenance window:",
186
+ " aws ssm register-target-with-maintenance-window \\",
187
+ " --window-id <window-id> \\",
188
+ " --target-type INSTANCE \\",
189
+ " --targets 'Key=tag:Patch Group,Values=Production' \\",
190
+ " --region <region>",
191
+ "",
192
+ "8. Register a patch task:",
193
+ " aws ssm register-task-with-maintenance-window \\",
194
+ " --window-id <window-id> \\",
195
+ " --target-id <target-id> \\",
196
+ " --task-type RUN_COMMAND \\",
197
+ " --task-arn AWS-RunPatchBaseline \\",
198
+ " --service-role-arn <role-arn> \\",
199
+ " --task-invocation-parameters 'RunCommand={Parameters={Operation=[Install]}}' \\",
200
+ " --priority 1 \\",
201
+ " --max-concurrency 50% \\",
202
+ " --max-errors 10% \\",
203
+ " --region <region>",
204
+ "",
205
+ "9. Best practices:",
206
+ " - Create separate patch baselines for different environments (dev/staging/prod)",
207
+ " - Use patch groups to organize instances",
208
+ " - Set appropriate approval delays (7 days for production)",
209
+ " - Configure maintenance windows during off-peak hours",
210
+ " - Enable SNS notifications for patch operations",
211
+ " - Monitor patch compliance with AWS Config",
212
+ " - Test patches in non-production first",
213
+ "",
214
+ "10. Verify configuration:",
215
+ " # List all patch baselines",
216
+ " aws ssm describe-patch-baselines --region <region>",
217
+ "",
218
+ " # Get default baseline for an OS",
219
+ " aws ssm get-default-patch-baseline \\",
220
+ " --operating-system AMAZON_LINUX_2 \\",
221
+ " --region <region>",
222
+ "",
223
+ "Priority: HIGH - Patch management is critical for security",
224
+ "Effort: Medium - Requires baseline configuration and maintenance windows",
225
+ "",
226
+ "AWS Documentation:",
227
+ "https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-patch.html"
228
+ ]
229
+
230
+
231
+
232
+ class SSMPatchBaselineConfiguredAssessment(BaseConfigRuleAssessment):
233
+ """
234
+ CIS Control 2.2 - Ensure Authorized Software is Currently Supported
235
+ AWS Config Rule: ssm-patch-baseline-configured
236
+
237
+ Ensures SSM patch baselines are properly configured with appropriate
238
+ approval rules and patch filters for security updates.
239
+ """
240
+
241
+ def __init__(self):
242
+ super().__init__(
243
+ rule_name="ssm-patch-baseline-configured",
244
+ control_id="2.2",
245
+ resource_types=["AWS::SSM::PatchBaseline"]
246
+ )
247
+
248
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
249
+ """Get all SSM patch baselines in the region."""
250
+ if resource_type != "AWS::SSM::PatchBaseline":
251
+ return []
252
+
253
+ try:
254
+ ssm_client = aws_factory.get_client('ssm', region)
255
+
256
+ baselines = []
257
+ paginator = ssm_client.get_paginator('describe_patch_baselines')
258
+
259
+ for page in paginator.paginate(Filters=[{'Key': 'OWNER', 'Values': ['Self']}]):
260
+ for baseline_identity in page.get('BaselineIdentities', []):
261
+ baseline_id = baseline_identity.get('BaselineId', '')
262
+
263
+ try:
264
+ # Get detailed baseline information
265
+ response = ssm_client.get_patch_baseline(BaselineId=baseline_id)
266
+
267
+ baseline_name = response.get('Name', '')
268
+ operating_system = response.get('OperatingSystem', '')
269
+ approval_rules = response.get('ApprovalRules', {})
270
+ patch_groups = response.get('PatchGroups', [])
271
+
272
+ # Check if baseline has approval rules
273
+ has_approval_rules = bool(approval_rules.get('PatchRules', []))
274
+
275
+ baselines.append({
276
+ 'BaselineId': baseline_id,
277
+ 'BaselineName': baseline_name,
278
+ 'OperatingSystem': operating_system,
279
+ 'ApprovalRules': approval_rules,
280
+ 'HasApprovalRules': has_approval_rules,
281
+ 'PatchGroups': patch_groups,
282
+ 'Description': response.get('Description', '')
283
+ })
284
+
285
+ except ClientError as e:
286
+ logger.warning(f"Error getting baseline {baseline_id}: {e}")
287
+ continue
288
+
289
+ logger.debug(f"Found {len(baselines)} custom patch baselines in {region}")
290
+ return baselines
291
+
292
+ except ClientError as e:
293
+ error_code = e.response.get('Error', {}).get('Code', '')
294
+ if error_code == 'AccessDeniedException':
295
+ logger.warning(f"Access denied to list patch baselines in {region}")
296
+ else:
297
+ logger.error(f"Error retrieving patch baselines from {region}: {e}")
298
+ return []
299
+
300
+ def _evaluate_resource_compliance(
301
+ self,
302
+ resource: Dict[str, Any],
303
+ aws_factory: AWSClientFactory,
304
+ region: str
305
+ ) -> ComplianceResult:
306
+ """Evaluate if patch baseline is properly configured."""
307
+ baseline_id = resource.get('BaselineId', 'unknown')
308
+ baseline_name = resource.get('BaselineName', '')
309
+ operating_system = resource.get('OperatingSystem', '')
310
+ has_approval_rules = resource.get('HasApprovalRules', False)
311
+ approval_rules = resource.get('ApprovalRules', {})
312
+
313
+ # Check if baseline has proper approval rules
314
+ is_compliant = has_approval_rules
315
+
316
+ if is_compliant:
317
+ patch_rules = approval_rules.get('PatchRules', [])
318
+ rule_count = len(patch_rules)
319
+ evaluation_reason = (
320
+ f"Patch baseline '{baseline_name}' ({operating_system}) is properly configured "
321
+ f"with {rule_count} approval rule(s)."
322
+ )
323
+ compliance_status = ComplianceStatus.COMPLIANT
324
+ else:
325
+ evaluation_reason = (
326
+ f"Patch baseline '{baseline_name}' ({operating_system}) does not have approval rules configured. "
327
+ f"Approval rules are required to automatically approve patches."
328
+ )
329
+ compliance_status = ComplianceStatus.NON_COMPLIANT
330
+
331
+ return ComplianceResult(
332
+ resource_id=baseline_id,
333
+ resource_type="AWS::SSM::PatchBaseline",
334
+ compliance_status=compliance_status,
335
+ evaluation_reason=evaluation_reason,
336
+ config_rule_name=self.rule_name,
337
+ region=region
338
+ )
339
+
340
+ def _get_rule_remediation_steps(self) -> List[str]:
341
+ """Get remediation steps for configuring patch baseline approval rules."""
342
+ return [
343
+ "1. Update an existing patch baseline with approval rules:",
344
+ " aws ssm update-patch-baseline \\",
345
+ " --baseline-id <baseline-id> \\",
346
+ " --approval-rules 'PatchRules=[{",
347
+ " PatchFilterGroup={",
348
+ " PatchFilters=[",
349
+ " {Key=CLASSIFICATION,Values=[Security,Bugfix]},",
350
+ " {Key=SEVERITY,Values=[Critical,Important]}",
351
+ " ]",
352
+ " },",
353
+ " ApprovalAfterDays=7,",
354
+ " ComplianceLevel=CRITICAL",
355
+ " }]' \\",
356
+ " --region <region>",
357
+ "",
358
+ "2. Console method:",
359
+ " - Navigate to AWS Systems Manager",
360
+ " - Click 'Patch Manager' > 'Patch baselines'",
361
+ " - Select the baseline",
362
+ " - Click 'Actions' > 'Modify patch baseline'",
363
+ " - Under 'Approval rules', click 'Add rule'",
364
+ " - Configure the rule:",
365
+ " * Product: Select OS/product",
366
+ " * Classification: Select patch types (Security, Bugfix, etc.)",
367
+ " * Severity: Select severity levels (Critical, Important, etc.)",
368
+ " * Auto-approval: Set days to wait before auto-approval",
369
+ " * Compliance reporting: Set compliance level",
370
+ " - Click 'Add rule'",
371
+ " - Click 'Save changes'",
372
+ "",
373
+ "3. Example approval rules for different OS types:",
374
+ " # Windows - Security and Critical Updates",
375
+ " PatchRules=[{",
376
+ " PatchFilterGroup={",
377
+ " PatchFilters=[",
378
+ " {Key=CLASSIFICATION,Values=[SecurityUpdates,CriticalUpdates]},",
379
+ " {Key=MSRC_SEVERITY,Values=[Critical,Important]}",
380
+ " ]",
381
+ " },",
382
+ " ApprovalAfterDays=7",
383
+ " }]",
384
+ "",
385
+ " # Linux - Security and Bugfix patches",
386
+ " PatchRules=[{",
387
+ " PatchFilterGroup={",
388
+ " PatchFilters=[",
389
+ " {Key=CLASSIFICATION,Values=[Security,Bugfix]},",
390
+ " {Key=SEVERITY,Values=[Critical,Important]}",
391
+ " ]",
392
+ " },",
393
+ " ApprovalAfterDays=7",
394
+ " }]",
395
+ "",
396
+ "4. Best practices for approval rules:",
397
+ " - Auto-approve security patches after 7 days for production",
398
+ " - Auto-approve immediately for non-production environments",
399
+ " - Include both Security and Bugfix classifications",
400
+ " - Focus on Critical and Important severity levels",
401
+ " - Set compliance level to CRITICAL for security patches",
402
+ " - Create separate rules for different patch types",
403
+ "",
404
+ "5. Verify baseline configuration:",
405
+ " aws ssm get-patch-baseline \\",
406
+ " --baseline-id <baseline-id> \\",
407
+ " --region <region>",
408
+ "",
409
+ "Priority: HIGH - Proper patch baseline configuration is essential",
410
+ "Effort: Low - Can be configured quickly via CLI or Console",
411
+ "",
412
+ "AWS Documentation:",
413
+ "https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-manager-approval-rules.html"
414
+ ]
415
+
416
+
417
+
418
+ class EC2PatchComplianceStatusAssessment(BaseConfigRuleAssessment):
419
+ """
420
+ CIS Control 2.3 - Address Unauthorized Software
421
+ AWS Config Rule: ec2-patch-compliance-status
422
+
423
+ Ensures EC2 instances managed by Systems Manager have compliant patch status
424
+ and are up to date with approved patches.
425
+ """
426
+
427
+ def __init__(self):
428
+ super().__init__(
429
+ rule_name="ec2-patch-compliance-status",
430
+ control_id="2.3",
431
+ resource_types=["AWS::EC2::Instance"]
432
+ )
433
+
434
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
435
+ """Get patch compliance status for all SSM-managed EC2 instances."""
436
+ if resource_type != "AWS::EC2::Instance":
437
+ return []
438
+
439
+ try:
440
+ ssm_client = aws_factory.get_client('ssm', region)
441
+
442
+ instances = []
443
+ paginator = ssm_client.get_paginator('describe_instance_patch_states')
444
+
445
+ for page in paginator.paginate():
446
+ for instance_state in page.get('InstancePatchStates', []):
447
+ instance_id = instance_state.get('InstanceId', '')
448
+ patch_group = instance_state.get('PatchGroup', '')
449
+ baseline_id = instance_state.get('BaselineId', '')
450
+ operation = instance_state.get('Operation', '')
451
+ operation_end_time = instance_state.get('OperationEndTime', '')
452
+
453
+ # Get patch counts
454
+ installed_count = instance_state.get('InstalledCount', 0)
455
+ installed_other_count = instance_state.get('InstalledOtherCount', 0)
456
+ installed_pending_reboot_count = instance_state.get('InstalledPendingRebootCount', 0)
457
+ installed_rejected_count = instance_state.get('InstalledRejectedCount', 0)
458
+ missing_count = instance_state.get('MissingCount', 0)
459
+ failed_count = instance_state.get('FailedCount', 0)
460
+ not_applicable_count = instance_state.get('NotApplicableCount', 0)
461
+
462
+ # Determine compliance status
463
+ is_compliant = (missing_count == 0 and failed_count == 0 and
464
+ installed_pending_reboot_count == 0)
465
+
466
+ instances.append({
467
+ 'InstanceId': instance_id,
468
+ 'PatchGroup': patch_group,
469
+ 'BaselineId': baseline_id,
470
+ 'Operation': operation,
471
+ 'OperationEndTime': str(operation_end_time) if operation_end_time else '',
472
+ 'InstalledCount': installed_count,
473
+ 'InstalledPendingRebootCount': installed_pending_reboot_count,
474
+ 'MissingCount': missing_count,
475
+ 'FailedCount': failed_count,
476
+ 'IsCompliant': is_compliant
477
+ })
478
+
479
+ logger.debug(f"Found {len(instances)} SSM-managed instances with patch state in {region}")
480
+ return instances
481
+
482
+ except ClientError as e:
483
+ error_code = e.response.get('Error', {}).get('Code', '')
484
+ if error_code == 'AccessDeniedException':
485
+ logger.warning(f"Access denied to check patch compliance in {region}")
486
+ else:
487
+ logger.error(f"Error retrieving patch compliance from {region}: {e}")
488
+ return []
489
+
490
+ def _evaluate_resource_compliance(
491
+ self,
492
+ resource: Dict[str, Any],
493
+ aws_factory: AWSClientFactory,
494
+ region: str
495
+ ) -> ComplianceResult:
496
+ """Evaluate if EC2 instance has compliant patch status."""
497
+ instance_id = resource.get('InstanceId', 'unknown')
498
+ is_compliant = resource.get('IsCompliant', False)
499
+ missing_count = resource.get('MissingCount', 0)
500
+ failed_count = resource.get('FailedCount', 0)
501
+ pending_reboot_count = resource.get('InstalledPendingRebootCount', 0)
502
+ installed_count = resource.get('InstalledCount', 0)
503
+ patch_group = resource.get('PatchGroup', 'None')
504
+
505
+ if is_compliant:
506
+ evaluation_reason = (
507
+ f"EC2 instance '{instance_id}' (Patch Group: {patch_group}) is patch compliant. "
508
+ f"Installed patches: {installed_count}, Missing: 0, Failed: 0"
509
+ )
510
+ compliance_status = ComplianceStatus.COMPLIANT
511
+ else:
512
+ issues = []
513
+ if missing_count > 0:
514
+ issues.append(f"{missing_count} missing patch(es)")
515
+ if failed_count > 0:
516
+ issues.append(f"{failed_count} failed patch(es)")
517
+ if pending_reboot_count > 0:
518
+ issues.append(f"{pending_reboot_count} patch(es) pending reboot")
519
+
520
+ evaluation_reason = (
521
+ f"EC2 instance '{instance_id}' (Patch Group: {patch_group}) is not patch compliant. "
522
+ f"Issues: {', '.join(issues)}"
523
+ )
524
+ compliance_status = ComplianceStatus.NON_COMPLIANT
525
+
526
+ return ComplianceResult(
527
+ resource_id=instance_id,
528
+ resource_type="AWS::EC2::Instance",
529
+ compliance_status=compliance_status,
530
+ evaluation_reason=evaluation_reason,
531
+ config_rule_name=self.rule_name,
532
+ region=region
533
+ )
534
+
535
+ def _get_rule_remediation_steps(self) -> List[str]:
536
+ """Get remediation steps for achieving patch compliance."""
537
+ return [
538
+ "1. Install missing patches on non-compliant instances:",
539
+ " aws ssm send-command \\",
540
+ " --document-name 'AWS-RunPatchBaseline' \\",
541
+ " --instance-ids <instance-id> \\",
542
+ " --parameters 'Operation=Install' \\",
543
+ " --region <region>",
544
+ "",
545
+ "2. Scan for patch compliance without installing:",
546
+ " aws ssm send-command \\",
547
+ " --document-name 'AWS-RunPatchBaseline' \\",
548
+ " --instance-ids <instance-id> \\",
549
+ " --parameters 'Operation=Scan' \\",
550
+ " --region <region>",
551
+ "",
552
+ "3. Check patch compliance status:",
553
+ " aws ssm describe-instance-patch-states \\",
554
+ " --instance-ids <instance-id> \\",
555
+ " --region <region>",
556
+ "",
557
+ "4. Get detailed patch information:",
558
+ " aws ssm describe-instance-patches \\",
559
+ " --instance-id <instance-id> \\",
560
+ " --region <region>",
561
+ "",
562
+ "5. Console method:",
563
+ " - Navigate to AWS Systems Manager",
564
+ " - Click 'Patch Manager' in the left menu",
565
+ " - Click 'Patch now' button",
566
+ " - Select 'Scan and install'",
567
+ " - Choose instances:",
568
+ " * Select instances manually, or",
569
+ " * Use tags to select instances, or",
570
+ " * Select by patch group",
571
+ " - Configure patching:",
572
+ " * Patching operation: Scan and install",
573
+ " * Reboot option: Reboot if needed / No reboot",
574
+ " * Concurrency: Number or percentage",
575
+ " * Error threshold: Number or percentage",
576
+ " - Click 'Patch now'",
577
+ "",
578
+ "6. Reboot instances with pending reboot patches:",
579
+ " aws ec2 reboot-instances \\",
580
+ " --instance-ids <instance-id> \\",
581
+ " --region <region>",
582
+ "",
583
+ "7. Set up automated patching with maintenance windows:",
584
+ " # Create maintenance window (if not exists)",
585
+ " aws ssm create-maintenance-window \\",
586
+ " --name 'Weekly-Patching' \\",
587
+ " --schedule 'cron(0 2 ? * SUN *)' \\",
588
+ " --duration 4 \\",
589
+ " --cutoff 1 \\",
590
+ " --region <region>",
591
+ "",
592
+ "8. Troubleshoot failed patches:",
593
+ " # View patch execution logs",
594
+ " aws ssm get-command-invocation \\",
595
+ " --command-id <command-id> \\",
596
+ " --instance-id <instance-id> \\",
597
+ " --region <region>",
598
+ "",
599
+ " # Common failure reasons:",
600
+ " - Insufficient disk space",
601
+ " - Network connectivity issues",
602
+ " - Package manager conflicts",
603
+ " - Missing dependencies",
604
+ "",
605
+ "9. Best practices:",
606
+ " - Schedule regular patching windows (weekly or monthly)",
607
+ " - Test patches in non-production first",
608
+ " - Allow automatic reboots during maintenance windows",
609
+ " - Monitor patch compliance with CloudWatch",
610
+ " - Set up SNS notifications for patch failures",
611
+ " - Use patch groups to organize instances",
612
+ " - Maintain at least 7-day approval delay for production",
613
+ "",
614
+ "10. Verify compliance after patching:",
615
+ " # Wait for patching to complete, then check status",
616
+ " aws ssm describe-instance-patch-states \\",
617
+ " --instance-ids <instance-id> \\",
618
+ " --query 'InstancePatchStates[0].{Missing:MissingCount,Failed:FailedCount,PendingReboot:InstalledPendingRebootCount}' \\",
619
+ " --region <region>",
620
+ "",
621
+ "Priority: CRITICAL - Unpatched systems are vulnerable to exploits",
622
+ "Effort: Medium - Requires patching execution and potential reboots",
623
+ "",
624
+ "AWS Documentation:",
625
+ "https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-manager-compliance.html"
626
+ ]