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
@@ -0,0 +1,657 @@
1
+ """AWS Systems Manager resource collection."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from botocore.exceptions import ClientError
7
+
8
+ from regscale.integrations.commercial.aws.inventory.base import BaseCollector
9
+
10
+ logger = logging.getLogger("regscale")
11
+
12
+
13
+ class SystemsManagerCollector(BaseCollector):
14
+ """Collector for AWS Systems Manager resources."""
15
+
16
+ def __init__(
17
+ self, session: Any, region: str, account_id: Optional[str] = None, tags: Optional[Dict[str, str]] = None
18
+ ):
19
+ """
20
+ Initialize Systems Manager collector.
21
+
22
+ :param session: AWS session to use for API calls
23
+ :param str region: AWS region to collect from
24
+ :param str account_id: Optional AWS account ID to filter resources
25
+ :param dict tags: Optional tags to filter resources (key-value pairs)
26
+ """
27
+ super().__init__(session, region)
28
+ self.account_id = account_id
29
+ self.tags = tags or {}
30
+
31
+ def collect(self) -> Dict[str, Any]:
32
+ """
33
+ Collect AWS Systems Manager resources.
34
+
35
+ :return: Dictionary containing Systems Manager information
36
+ :rtype: Dict[str, Any]
37
+ """
38
+ result = {
39
+ "ManagedInstances": [],
40
+ "Parameters": [],
41
+ "Documents": [],
42
+ "PatchBaselines": [],
43
+ "MaintenanceWindows": [],
44
+ "Associations": [],
45
+ "InventoryEntries": [],
46
+ "ComplianceSummary": {},
47
+ }
48
+
49
+ try:
50
+ client = self._get_client("ssm")
51
+
52
+ # Get managed instances
53
+ managed_instances = self._list_managed_instances(client)
54
+ result["ManagedInstances"] = managed_instances
55
+
56
+ # Get parameters
57
+ parameters = self._list_parameters(client)
58
+ result["Parameters"] = parameters
59
+
60
+ # Get documents
61
+ documents = self._list_documents(client)
62
+ result["Documents"] = documents
63
+
64
+ # Get patch baselines
65
+ patch_baselines = self._list_patch_baselines(client)
66
+ result["PatchBaselines"] = patch_baselines
67
+
68
+ # Get maintenance windows
69
+ maintenance_windows = self._list_maintenance_windows(client)
70
+ result["MaintenanceWindows"] = maintenance_windows
71
+
72
+ # Get associations
73
+ associations = self._list_associations(client)
74
+ result["Associations"] = associations
75
+
76
+ # Get compliance summary
77
+ compliance_summary = self._get_compliance_summary(client)
78
+ result["ComplianceSummary"] = compliance_summary
79
+
80
+ logger.info(
81
+ f"Collected {len(managed_instances)} managed instance(s), "
82
+ f"{len(parameters)} parameter(s) from {self.region}"
83
+ )
84
+
85
+ except ClientError as e:
86
+ self._handle_error(e, "Systems Manager resources")
87
+ except Exception as e:
88
+ logger.error(f"Unexpected error collecting Systems Manager resources: {e}", exc_info=True)
89
+
90
+ return result
91
+
92
+ def _list_managed_instances(self, client: Any) -> List[Dict[str, Any]]:
93
+ """
94
+ List managed instances.
95
+
96
+ :param client: SSM client
97
+ :return: List of managed instance information
98
+ :rtype: List[Dict[str, Any]]
99
+ """
100
+ instances = []
101
+ try:
102
+ paginator = client.get_paginator("describe_instance_information")
103
+
104
+ for page in paginator.paginate():
105
+ for instance in page.get("InstanceInformationList", []):
106
+ instance_dict = {
107
+ "Region": self.region,
108
+ "InstanceId": instance.get("InstanceId"),
109
+ "PingStatus": instance.get("PingStatus"),
110
+ "LastPingDateTime": str(instance.get("LastPingDateTime")),
111
+ "AgentVersion": instance.get("AgentVersion"),
112
+ "IsLatestVersion": instance.get("IsLatestVersion", False),
113
+ "PlatformType": instance.get("PlatformType"),
114
+ "PlatformName": instance.get("PlatformName"),
115
+ "PlatformVersion": instance.get("PlatformVersion"),
116
+ "ResourceType": instance.get("ResourceType"),
117
+ "IPAddress": instance.get("IPAddress"),
118
+ "ComputerName": instance.get("ComputerName"),
119
+ "AssociationStatus": instance.get("AssociationStatus"),
120
+ "LastAssociationExecutionDate": (
121
+ str(instance.get("LastAssociationExecutionDate"))
122
+ if instance.get("LastAssociationExecutionDate")
123
+ else None
124
+ ),
125
+ "LastSuccessfulAssociationExecutionDate": (
126
+ str(instance.get("LastSuccessfulAssociationExecutionDate"))
127
+ if instance.get("LastSuccessfulAssociationExecutionDate")
128
+ else None
129
+ ),
130
+ }
131
+
132
+ # Get instance patches
133
+ patches = self._get_instance_patches(client, instance["InstanceId"])
134
+ instance_dict["PatchSummary"] = patches
135
+
136
+ instances.append(instance_dict)
137
+
138
+ except ClientError as e:
139
+ if e.response["Error"]["Code"] == "AccessDeniedException":
140
+ logger.warning(f"Access denied to list managed instances in {self.region}")
141
+ else:
142
+ logger.error(f"Error listing managed instances: {e}")
143
+
144
+ return instances
145
+
146
+ def _get_instance_patches(self, client: Any, instance_id: str) -> Dict[str, Any]:
147
+ """
148
+ Get patch summary for an instance.
149
+
150
+ :param client: SSM client
151
+ :param str instance_id: Instance ID
152
+ :return: Patch summary
153
+ :rtype: Dict[str, Any]
154
+ """
155
+ try:
156
+ response = client.describe_instance_patches(InstanceId=instance_id, MaxResults=50)
157
+ patches = response.get("Patches", [])
158
+
159
+ summary = {
160
+ "TotalPatches": len(patches),
161
+ "Installed": sum(1 for p in patches if p.get("State") == "Installed"),
162
+ "InstalledOther": sum(1 for p in patches if p.get("State") == "InstalledOther"),
163
+ "Missing": sum(1 for p in patches if p.get("State") == "Missing"),
164
+ "Failed": sum(1 for p in patches if p.get("State") == "Failed"),
165
+ "NotApplicable": sum(1 for p in patches if p.get("State") == "NotApplicable"),
166
+ }
167
+ return summary
168
+ except ClientError as e:
169
+ if e.response["Error"]["Code"] not in ["AccessDeniedException", "InvalidInstanceId"]:
170
+ logger.debug(f"Error getting patches for instance {instance_id}: {e}")
171
+ return {}
172
+
173
+ def _list_parameters(self, client: Any) -> List[Dict[str, Any]]:
174
+ """
175
+ List SSM parameters.
176
+
177
+ :param client: SSM client
178
+ :return: List of parameter information
179
+ :rtype: List[Dict[str, Any]]
180
+ """
181
+ parameters = []
182
+ try:
183
+ paginator = client.get_paginator("describe_parameters")
184
+
185
+ for page in paginator.paginate():
186
+ for param in page.get("Parameters", []):
187
+ param_name = param.get("Name")
188
+
189
+ # Get tags for filtering
190
+ param_tags = self._get_resource_tags(client, "Parameter", param_name)
191
+
192
+ # Filter by tags if specified
193
+ if self.tags and not self._matches_tags(param_tags):
194
+ logger.debug(f"Skipping parameter {param_name} - does not match tag filters")
195
+ continue
196
+
197
+ parameters.append(
198
+ {
199
+ "Region": self.region,
200
+ "Name": param_name,
201
+ "Type": param.get("Type"),
202
+ "KeyId": param.get("KeyId"),
203
+ "LastModifiedDate": str(param.get("LastModifiedDate")),
204
+ "Description": param.get("Description"),
205
+ "Version": param.get("Version"),
206
+ "Tier": param.get("Tier"),
207
+ "Policies": param.get("Policies", []),
208
+ "DataType": param.get("DataType"),
209
+ "Tags": param_tags,
210
+ }
211
+ )
212
+
213
+ except ClientError as e:
214
+ if e.response["Error"]["Code"] == "AccessDeniedException":
215
+ logger.warning(f"Access denied to list parameters in {self.region}")
216
+ else:
217
+ logger.error(f"Error listing parameters: {e}")
218
+
219
+ return parameters
220
+
221
+ def _list_documents(self, client: Any) -> List[Dict[str, Any]]:
222
+ """
223
+ List SSM documents.
224
+
225
+ :param client: SSM client
226
+ :return: List of document information
227
+ :rtype: List[Dict[str, Any]]
228
+ """
229
+ documents = []
230
+ try:
231
+ filters = self._build_document_filters()
232
+ paginator = client.get_paginator("list_documents")
233
+
234
+ for page in paginator.paginate(Filters=filters):
235
+ for doc in page.get("DocumentIdentifiers", []):
236
+ if not self._should_include_document(doc):
237
+ continue
238
+
239
+ document_dict = self._build_document_dict(doc)
240
+ documents.append(document_dict)
241
+
242
+ except ClientError as e:
243
+ self._handle_document_error(e)
244
+
245
+ return documents
246
+
247
+ def _build_document_filters(self) -> List[Dict[str, Any]]:
248
+ """
249
+ Build filters for listing documents.
250
+
251
+ :return: List of filters for document pagination
252
+ :rtype: List[Dict[str, Any]]
253
+ """
254
+ filters = []
255
+ if self.account_id:
256
+ filters.append({"Key": "Owner", "Values": [self.account_id]})
257
+ return filters
258
+
259
+ def _should_include_document(self, doc: Dict[str, Any]) -> bool:
260
+ """
261
+ Check if document should be included based on filters.
262
+
263
+ :param dict doc: Document information from AWS API
264
+ :return: True if document should be included
265
+ :rtype: bool
266
+ """
267
+ if not self._matches_account_filter(doc):
268
+ return False
269
+
270
+ doc_tags = self._extract_document_tags(doc)
271
+ if not self._matches_tag_filter(doc, doc_tags):
272
+ return False
273
+
274
+ return True
275
+
276
+ def _matches_account_filter(self, doc: Dict[str, Any]) -> bool:
277
+ """
278
+ Check if document matches account filter.
279
+
280
+ :param dict doc: Document information from AWS API
281
+ :return: True if document matches account filter
282
+ :rtype: bool
283
+ """
284
+ if not self.account_id:
285
+ return True
286
+
287
+ owner = doc.get("Owner", "")
288
+ if not owner:
289
+ return True
290
+
291
+ is_amazon_document = owner.startswith("Amazon")
292
+ is_account_owner = owner == self.account_id
293
+ return is_amazon_document or is_account_owner
294
+
295
+ def _extract_document_tags(self, doc: Dict[str, Any]) -> Dict[str, str]:
296
+ """
297
+ Extract tags from document as a dictionary.
298
+
299
+ :param dict doc: Document information from AWS API
300
+ :return: Dictionary of tags (Key -> Value)
301
+ :rtype: Dict[str, str]
302
+ """
303
+ doc_tags_list = doc.get("Tags", [])
304
+ return {tag["Key"]: tag["Value"] for tag in doc_tags_list}
305
+
306
+ def _matches_tag_filter(self, doc: Dict[str, Any], doc_tags: Dict[str, str]) -> bool:
307
+ """
308
+ Check if document matches tag filters.
309
+
310
+ :param dict doc: Document information from AWS API
311
+ :param dict doc_tags: Extracted document tags
312
+ :return: True if document matches tag filters
313
+ :rtype: bool
314
+ """
315
+ if not self.tags:
316
+ return True
317
+
318
+ if not self._matches_tags(doc_tags):
319
+ doc_name = doc.get("Name")
320
+ logger.debug(f"Skipping document {doc_name} - does not match tag filters")
321
+ return False
322
+
323
+ return True
324
+
325
+ def _build_document_dict(self, doc: Dict[str, Any]) -> Dict[str, Any]:
326
+ """
327
+ Build document dictionary for output.
328
+
329
+ :param dict doc: Document information from AWS API
330
+ :return: Formatted document dictionary
331
+ :rtype: Dict[str, Any]
332
+ """
333
+ doc_tags_list = doc.get("Tags", [])
334
+ return {
335
+ "Region": self.region,
336
+ "Name": doc.get("Name"),
337
+ "Owner": doc.get("Owner"),
338
+ "VersionName": doc.get("VersionName"),
339
+ "PlatformTypes": doc.get("PlatformTypes", []),
340
+ "DocumentVersion": doc.get("DocumentVersion"),
341
+ "DocumentType": doc.get("DocumentType"),
342
+ "SchemaVersion": doc.get("SchemaVersion"),
343
+ "DocumentFormat": doc.get("DocumentFormat"),
344
+ "TargetType": doc.get("TargetType"),
345
+ "Tags": doc_tags_list,
346
+ }
347
+
348
+ def _handle_document_error(self, error: ClientError) -> None:
349
+ """
350
+ Handle errors when listing documents.
351
+
352
+ :param ClientError error: The client error to handle
353
+ """
354
+ if error.response["Error"]["Code"] == "AccessDeniedException":
355
+ logger.warning(f"Access denied to list documents in {self.region}")
356
+ else:
357
+ logger.error(f"Error listing documents: {error}")
358
+
359
+ def _list_patch_baselines(self, client: Any) -> List[Dict[str, Any]]:
360
+ """
361
+ List patch baselines.
362
+
363
+ :param client: SSM client
364
+ :return: List of patch baseline information
365
+ :rtype: List[Dict[str, Any]]
366
+ """
367
+ baselines = []
368
+ try:
369
+ filters = self._build_baseline_filters()
370
+ paginator = client.get_paginator("describe_patch_baselines")
371
+
372
+ for page in paginator.paginate(Filters=filters):
373
+ for baseline in page.get("BaselineIdentities", []):
374
+ baseline_dict = self._process_baseline(client, baseline)
375
+ if baseline_dict:
376
+ baselines.append(baseline_dict)
377
+
378
+ except ClientError as e:
379
+ self._handle_baseline_error(e)
380
+
381
+ return baselines
382
+
383
+ def _build_baseline_filters(self) -> List[Dict[str, Any]]:
384
+ """
385
+ Build filters for listing patch baselines.
386
+
387
+ :return: List of filters for baseline pagination
388
+ :rtype: List[Dict[str, Any]]
389
+ """
390
+ filters = []
391
+ if self.account_id:
392
+ filters.append({"Key": "OWNER", "Values": [self.account_id]})
393
+ return filters
394
+
395
+ def _process_baseline(self, client: Any, baseline: Dict[str, Any]) -> Optional[Dict[str, Any]]:
396
+ """
397
+ Process a single patch baseline.
398
+
399
+ :param client: SSM client
400
+ :param dict baseline: Baseline information from AWS API
401
+ :return: Formatted baseline dictionary or None if filtered out
402
+ :rtype: Optional[Dict[str, Any]]
403
+ """
404
+ try:
405
+ baseline_id = baseline["BaselineId"]
406
+ baseline_detail = client.get_patch_baseline(BaselineId=baseline_id)
407
+ baseline_tags = self._get_resource_tags(client, "PatchBaseline", baseline_id)
408
+
409
+ if not self._should_include_baseline(baseline_id, baseline_tags):
410
+ return None
411
+
412
+ return self._build_baseline_dict(baseline, baseline_detail, baseline_tags)
413
+
414
+ except ClientError as e:
415
+ self._handle_baseline_processing_error(e, baseline)
416
+ return None
417
+
418
+ def _should_include_baseline(self, baseline_id: str, baseline_tags: Dict[str, str]) -> bool:
419
+ """
420
+ Check if baseline should be included based on tag filters.
421
+
422
+ :param str baseline_id: Baseline identifier
423
+ :param dict baseline_tags: Baseline tags
424
+ :return: True if baseline should be included
425
+ :rtype: bool
426
+ """
427
+ if not self.tags:
428
+ return True
429
+
430
+ if not self._matches_tags(baseline_tags):
431
+ logger.debug(f"Skipping patch baseline {baseline_id} - does not match tag filters")
432
+ return False
433
+
434
+ return True
435
+
436
+ def _build_baseline_dict(
437
+ self, baseline: Dict[str, Any], baseline_detail: Dict[str, Any], baseline_tags: Dict[str, str]
438
+ ) -> Dict[str, Any]:
439
+ """
440
+ Build baseline dictionary for output.
441
+
442
+ :param dict baseline: Baseline identity from AWS API
443
+ :param dict baseline_detail: Detailed baseline information from AWS API
444
+ :param dict baseline_tags: Baseline tags
445
+ :return: Formatted baseline dictionary
446
+ :rtype: Dict[str, Any]
447
+ """
448
+ return {
449
+ "Region": self.region,
450
+ "BaselineId": baseline["BaselineId"],
451
+ "BaselineName": baseline.get("BaselineName"),
452
+ "OperatingSystem": baseline.get("OperatingSystem"),
453
+ "DefaultBaseline": baseline.get("DefaultBaseline", False),
454
+ "Description": baseline_detail.get("BaselineDescription"),
455
+ "ApprovalRules": baseline_detail.get("ApprovalRules", {}),
456
+ "ApprovedPatches": baseline_detail.get("ApprovedPatches", []),
457
+ "RejectedPatches": baseline_detail.get("RejectedPatches", []),
458
+ "CreatedDate": self._format_date(baseline_detail.get("CreatedDate")),
459
+ "ModifiedDate": self._format_date(baseline_detail.get("ModifiedDate")),
460
+ "Tags": baseline_tags,
461
+ }
462
+
463
+ def _format_date(self, date_value: Any) -> Optional[str]:
464
+ """
465
+ Format date value to string.
466
+
467
+ :param date_value: Date value to format
468
+ :return: Formatted date string or None
469
+ :rtype: Optional[str]
470
+ """
471
+ if date_value:
472
+ return str(date_value)
473
+ return None
474
+
475
+ def _handle_baseline_processing_error(self, error: ClientError, baseline: Dict[str, Any]) -> None:
476
+ """
477
+ Handle errors when processing individual baseline.
478
+
479
+ :param ClientError error: The client error to handle
480
+ :param dict baseline: The baseline being processed
481
+ """
482
+ error_code = error.response["Error"]["Code"]
483
+ if error_code not in ["AccessDeniedException", "DoesNotExistException"]:
484
+ baseline_id = baseline.get("BaselineId", "unknown")
485
+ logger.error(f"Error getting baseline {baseline_id}: {error}")
486
+
487
+ def _handle_baseline_error(self, error: ClientError) -> None:
488
+ """
489
+ Handle errors when listing patch baselines.
490
+
491
+ :param ClientError error: The client error to handle
492
+ """
493
+ if error.response["Error"]["Code"] == "AccessDeniedException":
494
+ logger.warning(f"Access denied to list patch baselines in {self.region}")
495
+ else:
496
+ logger.error(f"Error listing patch baselines: {error}")
497
+
498
+ def _list_maintenance_windows(self, client: Any) -> List[Dict[str, Any]]:
499
+ """
500
+ List maintenance windows.
501
+
502
+ :param client: SSM client
503
+ :return: List of maintenance window information
504
+ :rtype: List[Dict[str, Any]]
505
+ """
506
+ windows = []
507
+ try:
508
+ paginator = client.get_paginator("describe_maintenance_windows")
509
+
510
+ for page in paginator.paginate():
511
+ for window in page.get("WindowIdentities", []):
512
+ window_id = window.get("WindowId")
513
+
514
+ # Get tags for filtering
515
+ window_tags = self._get_resource_tags(client, "MaintenanceWindow", window_id)
516
+
517
+ # Filter by tags if specified
518
+ if self.tags and not self._matches_tags(window_tags):
519
+ logger.debug(f"Skipping maintenance window {window_id} - does not match tag filters")
520
+ continue
521
+
522
+ windows.append(
523
+ {
524
+ "Region": self.region,
525
+ "WindowId": window_id,
526
+ "Name": window.get("Name"),
527
+ "Description": window.get("Description"),
528
+ "Enabled": window.get("Enabled", False),
529
+ "Duration": window.get("Duration"),
530
+ "Cutoff": window.get("Cutoff"),
531
+ "Schedule": window.get("Schedule"),
532
+ "ScheduleTimezone": window.get("ScheduleTimezone"),
533
+ "NextExecutionTime": window.get("NextExecutionTime"),
534
+ "Tags": window_tags,
535
+ }
536
+ )
537
+
538
+ except ClientError as e:
539
+ if e.response["Error"]["Code"] == "AccessDeniedException":
540
+ logger.warning(f"Access denied to list maintenance windows in {self.region}")
541
+ else:
542
+ logger.error(f"Error listing maintenance windows: {e}")
543
+
544
+ return windows
545
+
546
+ def _list_associations(self, client: Any) -> List[Dict[str, Any]]:
547
+ """
548
+ List associations.
549
+
550
+ :param client: SSM client
551
+ :return: List of association information
552
+ :rtype: List[Dict[str, Any]]
553
+ """
554
+ associations = []
555
+ try:
556
+ paginator = client.get_paginator("list_associations")
557
+
558
+ for page in paginator.paginate():
559
+ for assoc in page.get("Associations", []):
560
+ associations.append(
561
+ {
562
+ "Region": self.region,
563
+ "AssociationId": assoc.get("AssociationId"),
564
+ "AssociationName": assoc.get("AssociationName"),
565
+ "InstanceId": assoc.get("InstanceId"),
566
+ "DocumentVersion": assoc.get("DocumentVersion"),
567
+ "Targets": assoc.get("Targets", []),
568
+ "LastExecutionDate": (
569
+ str(assoc.get("LastExecutionDate")) if assoc.get("LastExecutionDate") else None
570
+ ),
571
+ "ScheduleExpression": assoc.get("ScheduleExpression"),
572
+ "AssociationVersion": assoc.get("AssociationVersion"),
573
+ }
574
+ )
575
+
576
+ except ClientError as e:
577
+ if e.response["Error"]["Code"] == "AccessDeniedException":
578
+ logger.warning(f"Access denied to list associations in {self.region}")
579
+ else:
580
+ logger.error(f"Error listing associations: {e}")
581
+
582
+ return associations
583
+
584
+ def _get_compliance_summary(self, client: Any) -> Dict[str, Any]:
585
+ """
586
+ Get compliance summary.
587
+
588
+ :param client: SSM client
589
+ :return: Compliance summary
590
+ :rtype: Dict[str, Any]
591
+ """
592
+ try:
593
+ response = client.list_compliance_summaries(MaxResults=50)
594
+ summaries = response.get("ComplianceSummaryItems", [])
595
+
596
+ if not summaries:
597
+ return {}
598
+
599
+ # Aggregate compliance data
600
+ total_compliant = sum(item.get("CompliantCount", 0) for item in summaries)
601
+ total_non_compliant = sum(item.get("NonCompliantCount", 0) for item in summaries)
602
+
603
+ return {
604
+ "TotalCompliant": total_compliant,
605
+ "TotalNonCompliant": total_non_compliant,
606
+ "ComplianceTypes": [
607
+ {
608
+ "ComplianceType": item.get("ComplianceType"),
609
+ "CompliantCount": item.get("CompliantCount", 0),
610
+ "NonCompliantCount": item.get("NonCompliantCount", 0),
611
+ }
612
+ for item in summaries
613
+ ],
614
+ }
615
+ except ClientError as e:
616
+ if e.response["Error"]["Code"] == "AccessDeniedException":
617
+ logger.warning(f"Access denied to get compliance summary in {self.region}")
618
+ else:
619
+ logger.debug(f"Error getting compliance summary: {e}")
620
+ return {}
621
+
622
+ def _get_resource_tags(self, client: Any, resource_type: str, resource_id: str) -> Dict[str, str]:
623
+ """
624
+ Get tags for a Systems Manager resource.
625
+
626
+ :param client: SSM client
627
+ :param str resource_type: Resource type (e.g., 'Parameter', 'Document', 'PatchBaseline', 'MaintenanceWindow')
628
+ :param str resource_id: Resource identifier (name, ID, or ARN)
629
+ :return: Dictionary of tags (Key -> Value)
630
+ :rtype: Dict[str, str]
631
+ """
632
+ try:
633
+ response = client.list_tags_for_resource(ResourceType=resource_type, ResourceId=resource_id)
634
+ tags_list = response.get("TagList", [])
635
+ return {tag["Key"]: tag["Value"] for tag in tags_list}
636
+ except ClientError as e:
637
+ if e.response["Error"]["Code"] not in ["AccessDeniedException", "InvalidResourceId"]:
638
+ logger.debug(f"Error getting tags for {resource_type} {resource_id}: {e}")
639
+ return {}
640
+
641
+ def _matches_tags(self, resource_tags: Dict[str, str]) -> bool:
642
+ """
643
+ Check if resource tags match the specified filter tags.
644
+
645
+ :param dict resource_tags: Tags on the resource
646
+ :return: True if all filter tags match
647
+ :rtype: bool
648
+ """
649
+ if not self.tags:
650
+ return True
651
+
652
+ # All filter tags must match
653
+ for key, value in self.tags.items():
654
+ if resource_tags.get(key) != value:
655
+ return False
656
+
657
+ return True