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
|
@@ -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
|