regscale-cli 6.27.3.0__py3-none-any.whl → 6.28.1.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/utils/app_utils.py +11 -2
- 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 +851 -206
- 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/ticketing.py +27 -0
- regscale/integrations/compliance_integration.py +308 -38
- regscale/integrations/due_date_handler.py +3 -0
- regscale/integrations/scanner_integration.py +399 -84
- regscale/models/integration_models/cisa_kev_data.json +65 -5
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +17 -9
- regscale/models/regscale_models/assessment.py +2 -1
- 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_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/RECORD +113 -34
- 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
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for AWS Systems Manager resource collector."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from botocore.exceptions import ClientError
|
|
11
|
+
|
|
12
|
+
from regscale.integrations.commercial.aws.inventory.resources.systems_manager import SystemsManagerCollector
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("regscale")
|
|
15
|
+
|
|
16
|
+
PATH = "regscale.integrations.commercial.aws.inventory.resources.systems_manager"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestSystemsManagerCollector:
|
|
20
|
+
"""Test suite for AWS Systems Manager resource collector."""
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_session(self):
|
|
24
|
+
"""Create a mock boto3 session."""
|
|
25
|
+
session = MagicMock()
|
|
26
|
+
return session
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mock_ssm_client(self):
|
|
30
|
+
"""Create a mock SSM client."""
|
|
31
|
+
client = MagicMock()
|
|
32
|
+
return client
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def collector(self, mock_session):
|
|
36
|
+
"""Create a SystemsManagerCollector instance."""
|
|
37
|
+
return SystemsManagerCollector(session=mock_session, region="us-east-1", account_id="123456789012")
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def collector_no_account(self, mock_session):
|
|
41
|
+
"""Create a SystemsManagerCollector instance without account_id."""
|
|
42
|
+
return SystemsManagerCollector(session=mock_session, region="us-east-1", account_id=None)
|
|
43
|
+
|
|
44
|
+
def test_init_with_account_id(self, mock_session):
|
|
45
|
+
"""Test initialization with account ID."""
|
|
46
|
+
collector = SystemsManagerCollector(session=mock_session, region="us-west-2", account_id="123456789012")
|
|
47
|
+
|
|
48
|
+
assert collector.session == mock_session
|
|
49
|
+
assert collector.region == "us-west-2"
|
|
50
|
+
assert collector.account_id == "123456789012"
|
|
51
|
+
|
|
52
|
+
def test_init_without_account_id(self, mock_session):
|
|
53
|
+
"""Test initialization without account ID."""
|
|
54
|
+
collector = SystemsManagerCollector(session=mock_session, region="us-east-1")
|
|
55
|
+
|
|
56
|
+
assert collector.session == mock_session
|
|
57
|
+
assert collector.region == "us-east-1"
|
|
58
|
+
assert collector.account_id is None
|
|
59
|
+
|
|
60
|
+
@patch(f"{PATH}.SystemsManagerCollector._get_compliance_summary")
|
|
61
|
+
@patch(f"{PATH}.SystemsManagerCollector._list_associations")
|
|
62
|
+
@patch(f"{PATH}.SystemsManagerCollector._list_maintenance_windows")
|
|
63
|
+
@patch(f"{PATH}.SystemsManagerCollector._list_patch_baselines")
|
|
64
|
+
@patch(f"{PATH}.SystemsManagerCollector._list_documents")
|
|
65
|
+
@patch(f"{PATH}.SystemsManagerCollector._list_parameters")
|
|
66
|
+
@patch(f"{PATH}.SystemsManagerCollector._list_managed_instances")
|
|
67
|
+
def test_collect_success(
|
|
68
|
+
self,
|
|
69
|
+
mock_list_instances,
|
|
70
|
+
mock_list_params,
|
|
71
|
+
mock_list_docs,
|
|
72
|
+
mock_list_baselines,
|
|
73
|
+
mock_list_windows,
|
|
74
|
+
mock_list_assocs,
|
|
75
|
+
mock_get_compliance,
|
|
76
|
+
collector,
|
|
77
|
+
mock_ssm_client,
|
|
78
|
+
):
|
|
79
|
+
"""Test successful collection of all Systems Manager resources."""
|
|
80
|
+
collector.session.client.return_value = mock_ssm_client
|
|
81
|
+
|
|
82
|
+
mock_list_instances.return_value = [
|
|
83
|
+
{"InstanceId": "i-12345", "PingStatus": "Online"},
|
|
84
|
+
{"InstanceId": "i-67890", "PingStatus": "Online"},
|
|
85
|
+
]
|
|
86
|
+
mock_list_params.return_value = [{"Name": "/test/param1"}]
|
|
87
|
+
mock_list_docs.return_value = [{"Name": "TestDocument"}]
|
|
88
|
+
mock_list_baselines.return_value = [{"BaselineId": "pb-12345"}]
|
|
89
|
+
mock_list_windows.return_value = [{"WindowId": "mw-12345"}]
|
|
90
|
+
mock_list_assocs.return_value = [{"AssociationId": "assoc-12345"}]
|
|
91
|
+
mock_get_compliance.return_value = {"TotalCompliant": 10, "TotalNonCompliant": 2}
|
|
92
|
+
|
|
93
|
+
result = collector.collect()
|
|
94
|
+
|
|
95
|
+
assert result["ManagedInstances"] == mock_list_instances.return_value
|
|
96
|
+
assert result["Parameters"] == mock_list_params.return_value
|
|
97
|
+
assert result["Documents"] == mock_list_docs.return_value
|
|
98
|
+
assert result["PatchBaselines"] == mock_list_baselines.return_value
|
|
99
|
+
assert result["MaintenanceWindows"] == mock_list_windows.return_value
|
|
100
|
+
assert result["Associations"] == mock_list_assocs.return_value
|
|
101
|
+
assert result["ComplianceSummary"] == mock_get_compliance.return_value
|
|
102
|
+
assert len(result["ManagedInstances"]) == 2
|
|
103
|
+
|
|
104
|
+
collector.session.client.assert_called_once_with("ssm", region_name="us-east-1")
|
|
105
|
+
mock_list_instances.assert_called_once_with(mock_ssm_client)
|
|
106
|
+
mock_list_params.assert_called_once_with(mock_ssm_client)
|
|
107
|
+
mock_list_docs.assert_called_once_with(mock_ssm_client)
|
|
108
|
+
mock_list_baselines.assert_called_once_with(mock_ssm_client)
|
|
109
|
+
mock_list_windows.assert_called_once_with(mock_ssm_client)
|
|
110
|
+
mock_list_assocs.assert_called_once_with(mock_ssm_client)
|
|
111
|
+
mock_get_compliance.assert_called_once_with(mock_ssm_client)
|
|
112
|
+
|
|
113
|
+
@patch(f"{PATH}.SystemsManagerCollector._handle_error")
|
|
114
|
+
def test_collect_client_error(self, mock_handle_error, collector, mock_ssm_client):
|
|
115
|
+
"""Test collect handles ClientError from _get_client."""
|
|
116
|
+
error = ClientError({"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "CreateClient")
|
|
117
|
+
collector.session.client.side_effect = error
|
|
118
|
+
|
|
119
|
+
result = collector.collect()
|
|
120
|
+
|
|
121
|
+
assert result["ManagedInstances"] == []
|
|
122
|
+
assert result["Parameters"] == []
|
|
123
|
+
assert result["Documents"] == []
|
|
124
|
+
mock_handle_error.assert_called_once_with(error, "Systems Manager resources")
|
|
125
|
+
|
|
126
|
+
@patch(f"{PATH}.logger")
|
|
127
|
+
def test_collect_unexpected_error(self, mock_logger, collector, mock_ssm_client):
|
|
128
|
+
"""Test collect handles unexpected errors."""
|
|
129
|
+
collector.session.client.return_value = mock_ssm_client
|
|
130
|
+
error = ValueError("Unexpected error")
|
|
131
|
+
|
|
132
|
+
mock_ssm_client.get_paginator.side_effect = error
|
|
133
|
+
|
|
134
|
+
result = collector.collect()
|
|
135
|
+
|
|
136
|
+
assert result["ManagedInstances"] == []
|
|
137
|
+
assert result["Parameters"] == []
|
|
138
|
+
mock_logger.error.assert_called()
|
|
139
|
+
assert "Unexpected error collecting Systems Manager resources" in str(mock_logger.error.call_args)
|
|
140
|
+
|
|
141
|
+
def test_list_managed_instances_success(self, collector, mock_ssm_client):
|
|
142
|
+
"""Test successful listing of managed instances with pagination."""
|
|
143
|
+
mock_paginator = MagicMock()
|
|
144
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
145
|
+
|
|
146
|
+
test_datetime = datetime(2024, 1, 1, 12, 0, 0)
|
|
147
|
+
mock_paginator.paginate.return_value = [
|
|
148
|
+
{
|
|
149
|
+
"InstanceInformationList": [
|
|
150
|
+
{
|
|
151
|
+
"InstanceId": "i-12345",
|
|
152
|
+
"PingStatus": "Online",
|
|
153
|
+
"LastPingDateTime": test_datetime,
|
|
154
|
+
"AgentVersion": "3.1.0",
|
|
155
|
+
"IsLatestVersion": True,
|
|
156
|
+
"PlatformType": "Linux",
|
|
157
|
+
"PlatformName": "Amazon Linux",
|
|
158
|
+
"PlatformVersion": "2",
|
|
159
|
+
"ResourceType": "EC2Instance",
|
|
160
|
+
"IPAddress": "10.0.1.5",
|
|
161
|
+
"ComputerName": "test-instance",
|
|
162
|
+
"AssociationStatus": "Success",
|
|
163
|
+
"LastAssociationExecutionDate": test_datetime,
|
|
164
|
+
"LastSuccessfulAssociationExecutionDate": test_datetime,
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"InstanceInformationList": [
|
|
170
|
+
{
|
|
171
|
+
"InstanceId": "i-67890",
|
|
172
|
+
"PingStatus": "ConnectionLost",
|
|
173
|
+
"LastPingDateTime": test_datetime,
|
|
174
|
+
"AgentVersion": "3.0.0",
|
|
175
|
+
"PlatformType": "Windows",
|
|
176
|
+
"PlatformName": "Windows Server",
|
|
177
|
+
"PlatformVersion": "2019",
|
|
178
|
+
"ResourceType": "ManagedInstance",
|
|
179
|
+
"IPAddress": "10.0.1.6",
|
|
180
|
+
"ComputerName": "windows-server",
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
with patch.object(collector, "_get_instance_patches") as mock_get_patches:
|
|
187
|
+
mock_get_patches.side_effect = [
|
|
188
|
+
{"TotalPatches": 10, "Installed": 8, "Missing": 2},
|
|
189
|
+
{"TotalPatches": 15, "Installed": 15, "Missing": 0},
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
result = collector._list_managed_instances(mock_ssm_client)
|
|
193
|
+
|
|
194
|
+
assert len(result) == 2
|
|
195
|
+
assert result[0]["InstanceId"] == "i-12345"
|
|
196
|
+
assert result[0]["Region"] == "us-east-1"
|
|
197
|
+
assert result[0]["PingStatus"] == "Online"
|
|
198
|
+
assert result[0]["AgentVersion"] == "3.1.0"
|
|
199
|
+
assert result[0]["IsLatestVersion"] is True
|
|
200
|
+
assert result[0]["PatchSummary"]["TotalPatches"] == 10
|
|
201
|
+
assert result[1]["InstanceId"] == "i-67890"
|
|
202
|
+
assert result[1]["PatchSummary"]["TotalPatches"] == 15
|
|
203
|
+
|
|
204
|
+
mock_ssm_client.get_paginator.assert_called_once_with("describe_instance_information")
|
|
205
|
+
assert mock_get_patches.call_count == 2
|
|
206
|
+
|
|
207
|
+
@patch(f"{PATH}.logger")
|
|
208
|
+
def test_list_managed_instances_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
209
|
+
"""Test listing managed instances with access denied error."""
|
|
210
|
+
mock_paginator = MagicMock()
|
|
211
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
212
|
+
error = ClientError(
|
|
213
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeInstanceInformation"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
mock_paginator.paginate.side_effect = error
|
|
217
|
+
|
|
218
|
+
result = collector._list_managed_instances(mock_ssm_client)
|
|
219
|
+
|
|
220
|
+
assert result == []
|
|
221
|
+
mock_logger.warning.assert_called_once()
|
|
222
|
+
assert "Access denied to list managed instances in us-east-1" in str(mock_logger.warning.call_args)
|
|
223
|
+
|
|
224
|
+
@patch(f"{PATH}.logger")
|
|
225
|
+
def test_list_managed_instances_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
226
|
+
"""Test listing managed instances with other ClientError."""
|
|
227
|
+
mock_paginator = MagicMock()
|
|
228
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
229
|
+
error = ClientError(
|
|
230
|
+
{"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeInstanceInformation"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
mock_paginator.paginate.side_effect = error
|
|
234
|
+
|
|
235
|
+
result = collector._list_managed_instances(mock_ssm_client)
|
|
236
|
+
|
|
237
|
+
assert result == []
|
|
238
|
+
mock_logger.error.assert_called_once()
|
|
239
|
+
assert "Error listing managed instances" in str(mock_logger.error.call_args)
|
|
240
|
+
|
|
241
|
+
def test_get_instance_patches_success(self, collector, mock_ssm_client):
|
|
242
|
+
"""Test successful retrieval of instance patches."""
|
|
243
|
+
mock_ssm_client.describe_instance_patches.return_value = {
|
|
244
|
+
"Patches": [
|
|
245
|
+
{"State": "Installed"},
|
|
246
|
+
{"State": "Installed"},
|
|
247
|
+
{"State": "InstalledOther"},
|
|
248
|
+
{"State": "Missing"},
|
|
249
|
+
{"State": "Missing"},
|
|
250
|
+
{"State": "Failed"},
|
|
251
|
+
{"State": "NotApplicable"},
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
result = collector._get_instance_patches(mock_ssm_client, "i-12345")
|
|
256
|
+
|
|
257
|
+
assert result["TotalPatches"] == 7
|
|
258
|
+
assert result["Installed"] == 2
|
|
259
|
+
assert result["InstalledOther"] == 1
|
|
260
|
+
assert result["Missing"] == 2
|
|
261
|
+
assert result["Failed"] == 1
|
|
262
|
+
assert result["NotApplicable"] == 1
|
|
263
|
+
|
|
264
|
+
mock_ssm_client.describe_instance_patches.assert_called_once_with(InstanceId="i-12345", MaxResults=50)
|
|
265
|
+
|
|
266
|
+
def test_get_instance_patches_empty(self, collector, mock_ssm_client):
|
|
267
|
+
"""Test instance patches with no patches returned."""
|
|
268
|
+
mock_ssm_client.describe_instance_patches.return_value = {"Patches": []}
|
|
269
|
+
|
|
270
|
+
result = collector._get_instance_patches(mock_ssm_client, "i-12345")
|
|
271
|
+
|
|
272
|
+
assert result["TotalPatches"] == 0
|
|
273
|
+
assert result["Installed"] == 0
|
|
274
|
+
assert result["Missing"] == 0
|
|
275
|
+
|
|
276
|
+
@patch(f"{PATH}.logger")
|
|
277
|
+
def test_get_instance_patches_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
278
|
+
"""Test instance patches with access denied error."""
|
|
279
|
+
error = ClientError(
|
|
280
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeInstancePatches"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
mock_ssm_client.describe_instance_patches.side_effect = error
|
|
284
|
+
|
|
285
|
+
result = collector._get_instance_patches(mock_ssm_client, "i-12345")
|
|
286
|
+
|
|
287
|
+
assert result == {}
|
|
288
|
+
mock_logger.debug.assert_not_called()
|
|
289
|
+
|
|
290
|
+
@patch(f"{PATH}.logger")
|
|
291
|
+
def test_get_instance_patches_invalid_instance(self, mock_logger, collector, mock_ssm_client):
|
|
292
|
+
"""Test instance patches with invalid instance ID error."""
|
|
293
|
+
error = ClientError(
|
|
294
|
+
{"Error": {"Code": "InvalidInstanceId", "Message": "Invalid instance"}}, "DescribeInstancePatches"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
mock_ssm_client.describe_instance_patches.side_effect = error
|
|
298
|
+
|
|
299
|
+
result = collector._get_instance_patches(mock_ssm_client, "i-invalid")
|
|
300
|
+
|
|
301
|
+
assert result == {}
|
|
302
|
+
mock_logger.debug.assert_not_called()
|
|
303
|
+
|
|
304
|
+
@patch(f"{PATH}.logger")
|
|
305
|
+
def test_get_instance_patches_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
306
|
+
"""Test instance patches with other ClientError."""
|
|
307
|
+
error = ClientError(
|
|
308
|
+
{"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeInstancePatches"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
mock_ssm_client.describe_instance_patches.side_effect = error
|
|
312
|
+
|
|
313
|
+
result = collector._get_instance_patches(mock_ssm_client, "i-12345")
|
|
314
|
+
|
|
315
|
+
assert result == {}
|
|
316
|
+
mock_logger.debug.assert_called_once()
|
|
317
|
+
assert "Error getting patches for instance i-12345" in str(mock_logger.debug.call_args)
|
|
318
|
+
|
|
319
|
+
def test_list_parameters_success(self, collector, mock_ssm_client):
|
|
320
|
+
"""Test successful listing of parameters with pagination."""
|
|
321
|
+
mock_paginator = MagicMock()
|
|
322
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
323
|
+
|
|
324
|
+
test_datetime = datetime(2024, 1, 1, 12, 0, 0)
|
|
325
|
+
mock_paginator.paginate.return_value = [
|
|
326
|
+
{
|
|
327
|
+
"Parameters": [
|
|
328
|
+
{
|
|
329
|
+
"Name": "/test/param1",
|
|
330
|
+
"Type": "String",
|
|
331
|
+
"KeyId": "key-12345",
|
|
332
|
+
"LastModifiedDate": test_datetime,
|
|
333
|
+
"Description": "Test parameter 1",
|
|
334
|
+
"Version": 1,
|
|
335
|
+
"Tier": "Standard",
|
|
336
|
+
"Policies": [],
|
|
337
|
+
"DataType": "text",
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"Name": "/test/param2",
|
|
341
|
+
"Type": "SecureString",
|
|
342
|
+
"LastModifiedDate": test_datetime,
|
|
343
|
+
"Version": 2,
|
|
344
|
+
"Tier": "Advanced",
|
|
345
|
+
},
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
result = collector._list_parameters(mock_ssm_client)
|
|
351
|
+
|
|
352
|
+
assert len(result) == 2
|
|
353
|
+
assert result[0]["Name"] == "/test/param1"
|
|
354
|
+
assert result[0]["Type"] == "String"
|
|
355
|
+
assert result[0]["Region"] == "us-east-1"
|
|
356
|
+
assert result[0]["KeyId"] == "key-12345"
|
|
357
|
+
assert result[1]["Name"] == "/test/param2"
|
|
358
|
+
assert result[1]["Type"] == "SecureString"
|
|
359
|
+
|
|
360
|
+
mock_ssm_client.get_paginator.assert_called_once_with("describe_parameters")
|
|
361
|
+
|
|
362
|
+
@patch(f"{PATH}.logger")
|
|
363
|
+
def test_list_parameters_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
364
|
+
"""Test listing parameters with access denied error."""
|
|
365
|
+
mock_paginator = MagicMock()
|
|
366
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
367
|
+
error = ClientError(
|
|
368
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeParameters"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
mock_paginator.paginate.side_effect = error
|
|
372
|
+
|
|
373
|
+
result = collector._list_parameters(mock_ssm_client)
|
|
374
|
+
|
|
375
|
+
assert result == []
|
|
376
|
+
mock_logger.warning.assert_called_once()
|
|
377
|
+
assert "Access denied to list parameters in us-east-1" in str(mock_logger.warning.call_args)
|
|
378
|
+
|
|
379
|
+
@patch(f"{PATH}.logger")
|
|
380
|
+
def test_list_parameters_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
381
|
+
"""Test listing parameters with other ClientError."""
|
|
382
|
+
mock_paginator = MagicMock()
|
|
383
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
384
|
+
error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeParameters")
|
|
385
|
+
|
|
386
|
+
mock_paginator.paginate.side_effect = error
|
|
387
|
+
|
|
388
|
+
result = collector._list_parameters(mock_ssm_client)
|
|
389
|
+
|
|
390
|
+
assert result == []
|
|
391
|
+
mock_logger.error.assert_called_once()
|
|
392
|
+
assert "Error listing parameters" in str(mock_logger.error.call_args)
|
|
393
|
+
|
|
394
|
+
def test_list_documents_with_account_filter(self, collector, mock_ssm_client):
|
|
395
|
+
"""Test listing documents with account ID filtering."""
|
|
396
|
+
mock_paginator = MagicMock()
|
|
397
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
398
|
+
|
|
399
|
+
mock_paginator.paginate.return_value = [
|
|
400
|
+
{
|
|
401
|
+
"DocumentIdentifiers": [
|
|
402
|
+
{
|
|
403
|
+
"Name": "MyDocument",
|
|
404
|
+
"Owner": "123456789012",
|
|
405
|
+
"VersionName": "v1",
|
|
406
|
+
"PlatformTypes": ["Linux"],
|
|
407
|
+
"DocumentVersion": "1",
|
|
408
|
+
"DocumentType": "Command",
|
|
409
|
+
"SchemaVersion": "2.2",
|
|
410
|
+
"DocumentFormat": "JSON",
|
|
411
|
+
"TargetType": "/AWS::EC2::Instance",
|
|
412
|
+
"Tags": [{"Key": "Environment", "Value": "Test"}],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
"Name": "AmazonDocument",
|
|
416
|
+
"Owner": "Amazon",
|
|
417
|
+
"PlatformTypes": ["Windows", "Linux"],
|
|
418
|
+
"DocumentType": "Automation",
|
|
419
|
+
"SchemaVersion": "0.3",
|
|
420
|
+
"DocumentFormat": "YAML",
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"Name": "OtherAccountDoc",
|
|
424
|
+
"Owner": "999999999999",
|
|
425
|
+
"PlatformTypes": ["Linux"],
|
|
426
|
+
"DocumentType": "Command",
|
|
427
|
+
},
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
result = collector._list_documents(mock_ssm_client)
|
|
433
|
+
|
|
434
|
+
assert len(result) == 2
|
|
435
|
+
assert result[0]["Name"] == "MyDocument"
|
|
436
|
+
assert result[0]["Owner"] == "123456789012"
|
|
437
|
+
assert result[0]["Region"] == "us-east-1"
|
|
438
|
+
assert result[1]["Name"] == "AmazonDocument"
|
|
439
|
+
assert result[1]["Owner"] == "Amazon"
|
|
440
|
+
|
|
441
|
+
mock_paginator.paginate.assert_called_once_with(Filters=[{"Key": "Owner", "Values": ["123456789012"]}])
|
|
442
|
+
|
|
443
|
+
def test_list_documents_without_account_filter(self, collector_no_account, mock_ssm_client):
|
|
444
|
+
"""Test listing documents without account ID filtering."""
|
|
445
|
+
mock_paginator = MagicMock()
|
|
446
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
447
|
+
|
|
448
|
+
mock_paginator.paginate.return_value = [
|
|
449
|
+
{
|
|
450
|
+
"DocumentIdentifiers": [
|
|
451
|
+
{
|
|
452
|
+
"Name": "Document1",
|
|
453
|
+
"Owner": "123456789012",
|
|
454
|
+
"PlatformTypes": ["Linux"],
|
|
455
|
+
"DocumentType": "Command",
|
|
456
|
+
"SchemaVersion": "2.2",
|
|
457
|
+
"DocumentFormat": "JSON",
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
result = collector_no_account._list_documents(mock_ssm_client)
|
|
464
|
+
|
|
465
|
+
assert len(result) == 1
|
|
466
|
+
assert result[0]["Name"] == "Document1"
|
|
467
|
+
|
|
468
|
+
mock_paginator.paginate.assert_called_once_with(Filters=[])
|
|
469
|
+
|
|
470
|
+
@patch(f"{PATH}.logger")
|
|
471
|
+
def test_list_documents_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
472
|
+
"""Test listing documents with access denied error."""
|
|
473
|
+
mock_paginator = MagicMock()
|
|
474
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
475
|
+
error = ClientError({"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListDocuments")
|
|
476
|
+
|
|
477
|
+
mock_paginator.paginate.side_effect = error
|
|
478
|
+
|
|
479
|
+
result = collector._list_documents(mock_ssm_client)
|
|
480
|
+
|
|
481
|
+
assert result == []
|
|
482
|
+
mock_logger.warning.assert_called_once()
|
|
483
|
+
assert "Access denied to list documents in us-east-1" in str(mock_logger.warning.call_args)
|
|
484
|
+
|
|
485
|
+
@patch(f"{PATH}.logger")
|
|
486
|
+
def test_list_documents_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
487
|
+
"""Test listing documents with other ClientError."""
|
|
488
|
+
mock_paginator = MagicMock()
|
|
489
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
490
|
+
error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "ListDocuments")
|
|
491
|
+
|
|
492
|
+
mock_paginator.paginate.side_effect = error
|
|
493
|
+
|
|
494
|
+
result = collector._list_documents(mock_ssm_client)
|
|
495
|
+
|
|
496
|
+
assert result == []
|
|
497
|
+
mock_logger.error.assert_called_once()
|
|
498
|
+
assert "Error listing documents" in str(mock_logger.error.call_args)
|
|
499
|
+
|
|
500
|
+
def test_list_patch_baselines_with_account_filter(self, collector, mock_ssm_client):
|
|
501
|
+
"""Test listing patch baselines with account ID filtering."""
|
|
502
|
+
mock_paginator = MagicMock()
|
|
503
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
504
|
+
|
|
505
|
+
test_datetime = datetime(2024, 1, 1, 12, 0, 0)
|
|
506
|
+
mock_paginator.paginate.return_value = [
|
|
507
|
+
{
|
|
508
|
+
"BaselineIdentities": [
|
|
509
|
+
{
|
|
510
|
+
"BaselineId": "pb-12345",
|
|
511
|
+
"BaselineName": "MyBaseline",
|
|
512
|
+
"OperatingSystem": "AMAZON_LINUX_2",
|
|
513
|
+
"DefaultBaseline": True,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"BaselineId": "pb-67890",
|
|
517
|
+
"BaselineName": "WindowsBaseline",
|
|
518
|
+
"OperatingSystem": "WINDOWS",
|
|
519
|
+
"DefaultBaseline": False,
|
|
520
|
+
},
|
|
521
|
+
]
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
mock_ssm_client.get_patch_baseline.side_effect = [
|
|
526
|
+
{
|
|
527
|
+
"BaselineDescription": "Test baseline for Linux",
|
|
528
|
+
"ApprovalRules": {"PatchRules": [{"PatchFilterGroup": {}}]},
|
|
529
|
+
"ApprovedPatches": ["KB123456"],
|
|
530
|
+
"RejectedPatches": ["KB999999"],
|
|
531
|
+
"CreatedDate": test_datetime,
|
|
532
|
+
"ModifiedDate": test_datetime,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"BaselineDescription": "Test baseline for Windows",
|
|
536
|
+
"ApprovalRules": {},
|
|
537
|
+
"ApprovedPatches": [],
|
|
538
|
+
"RejectedPatches": [],
|
|
539
|
+
"CreatedDate": test_datetime,
|
|
540
|
+
},
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
result = collector._list_patch_baselines(mock_ssm_client)
|
|
544
|
+
|
|
545
|
+
assert len(result) == 2
|
|
546
|
+
assert result[0]["BaselineId"] == "pb-12345"
|
|
547
|
+
assert result[0]["BaselineName"] == "MyBaseline"
|
|
548
|
+
assert result[0]["Region"] == "us-east-1"
|
|
549
|
+
assert result[0]["DefaultBaseline"] is True
|
|
550
|
+
assert result[0]["Description"] == "Test baseline for Linux"
|
|
551
|
+
assert result[1]["BaselineId"] == "pb-67890"
|
|
552
|
+
assert result[1]["OperatingSystem"] == "WINDOWS"
|
|
553
|
+
|
|
554
|
+
mock_paginator.paginate.assert_called_once_with(Filters=[{"Key": "OWNER", "Values": ["123456789012"]}])
|
|
555
|
+
assert mock_ssm_client.get_patch_baseline.call_count == 2
|
|
556
|
+
|
|
557
|
+
def test_list_patch_baselines_without_account_filter(self, collector_no_account, mock_ssm_client):
|
|
558
|
+
"""Test listing patch baselines without account ID filtering."""
|
|
559
|
+
mock_paginator = MagicMock()
|
|
560
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
561
|
+
|
|
562
|
+
mock_paginator.paginate.return_value = [
|
|
563
|
+
{
|
|
564
|
+
"BaselineIdentities": [
|
|
565
|
+
{
|
|
566
|
+
"BaselineId": "pb-12345",
|
|
567
|
+
"BaselineName": "Baseline1",
|
|
568
|
+
"OperatingSystem": "AMAZON_LINUX_2",
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
}
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
mock_ssm_client.get_patch_baseline.return_value = {
|
|
575
|
+
"BaselineDescription": "Test baseline",
|
|
576
|
+
"ApprovalRules": {},
|
|
577
|
+
"ApprovedPatches": [],
|
|
578
|
+
"RejectedPatches": [],
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
result = collector_no_account._list_patch_baselines(mock_ssm_client)
|
|
582
|
+
|
|
583
|
+
assert len(result) == 1
|
|
584
|
+
assert result[0]["BaselineId"] == "pb-12345"
|
|
585
|
+
|
|
586
|
+
mock_paginator.paginate.assert_called_once_with(Filters=[])
|
|
587
|
+
|
|
588
|
+
@patch(f"{PATH}.logger")
|
|
589
|
+
def test_list_patch_baselines_baseline_detail_error(self, mock_logger, collector, mock_ssm_client):
|
|
590
|
+
"""Test listing patch baselines with error getting baseline details."""
|
|
591
|
+
mock_paginator = MagicMock()
|
|
592
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
593
|
+
|
|
594
|
+
mock_paginator.paginate.return_value = [
|
|
595
|
+
{
|
|
596
|
+
"BaselineIdentities": [
|
|
597
|
+
{"BaselineId": "pb-12345", "BaselineName": "Baseline1", "OperatingSystem": "AMAZON_LINUX_2"},
|
|
598
|
+
{"BaselineId": "pb-67890", "BaselineName": "Baseline2", "OperatingSystem": "WINDOWS"},
|
|
599
|
+
]
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
error = ClientError({"Error": {"Code": "DoesNotExistException", "Message": "Not found"}}, "GetPatchBaseline")
|
|
604
|
+
mock_ssm_client.get_patch_baseline.side_effect = error
|
|
605
|
+
|
|
606
|
+
result = collector._list_patch_baselines(mock_ssm_client)
|
|
607
|
+
|
|
608
|
+
assert result == []
|
|
609
|
+
mock_logger.error.assert_not_called()
|
|
610
|
+
|
|
611
|
+
@patch(f"{PATH}.logger")
|
|
612
|
+
def test_list_patch_baselines_baseline_detail_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
613
|
+
"""Test listing patch baselines with access denied getting baseline details."""
|
|
614
|
+
mock_paginator = MagicMock()
|
|
615
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
616
|
+
|
|
617
|
+
mock_paginator.paginate.return_value = [
|
|
618
|
+
{
|
|
619
|
+
"BaselineIdentities": [
|
|
620
|
+
{"BaselineId": "pb-12345", "BaselineName": "Baseline1", "OperatingSystem": "AMAZON_LINUX_2"}
|
|
621
|
+
]
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
error = ClientError(
|
|
626
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "GetPatchBaseline"
|
|
627
|
+
)
|
|
628
|
+
mock_ssm_client.get_patch_baseline.side_effect = error
|
|
629
|
+
|
|
630
|
+
result = collector._list_patch_baselines(mock_ssm_client)
|
|
631
|
+
|
|
632
|
+
assert result == []
|
|
633
|
+
mock_logger.error.assert_not_called()
|
|
634
|
+
|
|
635
|
+
@patch(f"{PATH}.logger")
|
|
636
|
+
def test_list_patch_baselines_baseline_detail_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
637
|
+
"""Test listing patch baselines with other error getting baseline details."""
|
|
638
|
+
mock_paginator = MagicMock()
|
|
639
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
640
|
+
|
|
641
|
+
mock_paginator.paginate.return_value = [
|
|
642
|
+
{
|
|
643
|
+
"BaselineIdentities": [
|
|
644
|
+
{"BaselineId": "pb-12345", "BaselineName": "Baseline1", "OperatingSystem": "AMAZON_LINUX_2"}
|
|
645
|
+
]
|
|
646
|
+
}
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "GetPatchBaseline")
|
|
650
|
+
mock_ssm_client.get_patch_baseline.side_effect = error
|
|
651
|
+
|
|
652
|
+
result = collector._list_patch_baselines(mock_ssm_client)
|
|
653
|
+
|
|
654
|
+
assert result == []
|
|
655
|
+
mock_logger.error.assert_called_once()
|
|
656
|
+
assert "Error getting baseline pb-12345" in str(mock_logger.error.call_args)
|
|
657
|
+
|
|
658
|
+
@patch(f"{PATH}.logger")
|
|
659
|
+
def test_list_patch_baselines_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
660
|
+
"""Test listing patch baselines with access denied error."""
|
|
661
|
+
mock_paginator = MagicMock()
|
|
662
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
663
|
+
error = ClientError(
|
|
664
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribePatchBaselines"
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
mock_paginator.paginate.side_effect = error
|
|
668
|
+
|
|
669
|
+
result = collector._list_patch_baselines(mock_ssm_client)
|
|
670
|
+
|
|
671
|
+
assert result == []
|
|
672
|
+
mock_logger.warning.assert_called_once()
|
|
673
|
+
assert "Access denied to list patch baselines in us-east-1" in str(mock_logger.warning.call_args)
|
|
674
|
+
|
|
675
|
+
@patch(f"{PATH}.logger")
|
|
676
|
+
def test_list_patch_baselines_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
677
|
+
"""Test listing patch baselines with other ClientError."""
|
|
678
|
+
mock_paginator = MagicMock()
|
|
679
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
680
|
+
error = ClientError(
|
|
681
|
+
{"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribePatchBaselines"
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
mock_paginator.paginate.side_effect = error
|
|
685
|
+
|
|
686
|
+
result = collector._list_patch_baselines(mock_ssm_client)
|
|
687
|
+
|
|
688
|
+
assert result == []
|
|
689
|
+
mock_logger.error.assert_called_once()
|
|
690
|
+
assert "Error listing patch baselines" in str(mock_logger.error.call_args)
|
|
691
|
+
|
|
692
|
+
def test_list_maintenance_windows_success(self, collector, mock_ssm_client):
|
|
693
|
+
"""Test successful listing of maintenance windows with pagination."""
|
|
694
|
+
mock_paginator = MagicMock()
|
|
695
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
696
|
+
|
|
697
|
+
mock_paginator.paginate.return_value = [
|
|
698
|
+
{
|
|
699
|
+
"WindowIdentities": [
|
|
700
|
+
{
|
|
701
|
+
"WindowId": "mw-12345",
|
|
702
|
+
"Name": "PatchWindow",
|
|
703
|
+
"Description": "Weekly patching window",
|
|
704
|
+
"Enabled": True,
|
|
705
|
+
"Duration": 2,
|
|
706
|
+
"Cutoff": 0,
|
|
707
|
+
"Schedule": "cron(0 2 ? * SUN *)",
|
|
708
|
+
"ScheduleTimezone": "America/New_York",
|
|
709
|
+
"NextExecutionTime": "2024-01-07T02:00:00Z",
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
"WindowId": "mw-67890",
|
|
713
|
+
"Name": "MaintenanceWindow",
|
|
714
|
+
"Enabled": False,
|
|
715
|
+
"Duration": 4,
|
|
716
|
+
"Cutoff": 1,
|
|
717
|
+
"Schedule": "rate(7 days)",
|
|
718
|
+
},
|
|
719
|
+
]
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
|
|
723
|
+
result = collector._list_maintenance_windows(mock_ssm_client)
|
|
724
|
+
|
|
725
|
+
assert len(result) == 2
|
|
726
|
+
assert result[0]["WindowId"] == "mw-12345"
|
|
727
|
+
assert result[0]["Name"] == "PatchWindow"
|
|
728
|
+
assert result[0]["Region"] == "us-east-1"
|
|
729
|
+
assert result[0]["Enabled"] is True
|
|
730
|
+
assert result[0]["Duration"] == 2
|
|
731
|
+
assert result[1]["WindowId"] == "mw-67890"
|
|
732
|
+
assert result[1]["Enabled"] is False
|
|
733
|
+
|
|
734
|
+
mock_ssm_client.get_paginator.assert_called_once_with("describe_maintenance_windows")
|
|
735
|
+
|
|
736
|
+
@patch(f"{PATH}.logger")
|
|
737
|
+
def test_list_maintenance_windows_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
738
|
+
"""Test listing maintenance windows with access denied error."""
|
|
739
|
+
mock_paginator = MagicMock()
|
|
740
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
741
|
+
error = ClientError(
|
|
742
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeMaintenanceWindows"
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
mock_paginator.paginate.side_effect = error
|
|
746
|
+
|
|
747
|
+
result = collector._list_maintenance_windows(mock_ssm_client)
|
|
748
|
+
|
|
749
|
+
assert result == []
|
|
750
|
+
mock_logger.warning.assert_called_once()
|
|
751
|
+
assert "Access denied to list maintenance windows in us-east-1" in str(mock_logger.warning.call_args)
|
|
752
|
+
|
|
753
|
+
@patch(f"{PATH}.logger")
|
|
754
|
+
def test_list_maintenance_windows_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
755
|
+
"""Test listing maintenance windows with other ClientError."""
|
|
756
|
+
mock_paginator = MagicMock()
|
|
757
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
758
|
+
error = ClientError(
|
|
759
|
+
{"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeMaintenanceWindows"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
mock_paginator.paginate.side_effect = error
|
|
763
|
+
|
|
764
|
+
result = collector._list_maintenance_windows(mock_ssm_client)
|
|
765
|
+
|
|
766
|
+
assert result == []
|
|
767
|
+
mock_logger.error.assert_called_once()
|
|
768
|
+
assert "Error listing maintenance windows" in str(mock_logger.error.call_args)
|
|
769
|
+
|
|
770
|
+
def test_list_associations_success(self, collector, mock_ssm_client):
|
|
771
|
+
"""Test successful listing of associations with pagination."""
|
|
772
|
+
mock_paginator = MagicMock()
|
|
773
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
774
|
+
|
|
775
|
+
test_datetime = datetime(2024, 1, 1, 12, 0, 0)
|
|
776
|
+
mock_paginator.paginate.return_value = [
|
|
777
|
+
{
|
|
778
|
+
"Associations": [
|
|
779
|
+
{
|
|
780
|
+
"AssociationId": "assoc-12345",
|
|
781
|
+
"AssociationName": "PatchAssociation",
|
|
782
|
+
"InstanceId": "i-12345",
|
|
783
|
+
"DocumentVersion": "1",
|
|
784
|
+
"Targets": [{"Key": "tag:Environment", "Values": ["Production"]}],
|
|
785
|
+
"LastExecutionDate": test_datetime,
|
|
786
|
+
"ScheduleExpression": "rate(1 day)",
|
|
787
|
+
"AssociationVersion": "1",
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
"AssociationId": "assoc-67890",
|
|
791
|
+
"AssociationName": "ConfigureAssociation",
|
|
792
|
+
"DocumentVersion": "2",
|
|
793
|
+
"Targets": [],
|
|
794
|
+
"AssociationVersion": "2",
|
|
795
|
+
},
|
|
796
|
+
]
|
|
797
|
+
}
|
|
798
|
+
]
|
|
799
|
+
|
|
800
|
+
result = collector._list_associations(mock_ssm_client)
|
|
801
|
+
|
|
802
|
+
assert len(result) == 2
|
|
803
|
+
assert result[0]["AssociationId"] == "assoc-12345"
|
|
804
|
+
assert result[0]["AssociationName"] == "PatchAssociation"
|
|
805
|
+
assert result[0]["Region"] == "us-east-1"
|
|
806
|
+
assert result[0]["InstanceId"] == "i-12345"
|
|
807
|
+
assert len(result[0]["Targets"]) == 1
|
|
808
|
+
assert result[1]["AssociationId"] == "assoc-67890"
|
|
809
|
+
assert result[1]["Targets"] == []
|
|
810
|
+
|
|
811
|
+
mock_ssm_client.get_paginator.assert_called_once_with("list_associations")
|
|
812
|
+
|
|
813
|
+
@patch(f"{PATH}.logger")
|
|
814
|
+
def test_list_associations_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
815
|
+
"""Test listing associations with access denied error."""
|
|
816
|
+
mock_paginator = MagicMock()
|
|
817
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
818
|
+
error = ClientError(
|
|
819
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListAssociations"
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
mock_paginator.paginate.side_effect = error
|
|
823
|
+
|
|
824
|
+
result = collector._list_associations(mock_ssm_client)
|
|
825
|
+
|
|
826
|
+
assert result == []
|
|
827
|
+
mock_logger.warning.assert_called_once()
|
|
828
|
+
assert "Access denied to list associations in us-east-1" in str(mock_logger.warning.call_args)
|
|
829
|
+
|
|
830
|
+
@patch(f"{PATH}.logger")
|
|
831
|
+
def test_list_associations_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
832
|
+
"""Test listing associations with other ClientError."""
|
|
833
|
+
mock_paginator = MagicMock()
|
|
834
|
+
mock_ssm_client.get_paginator.return_value = mock_paginator
|
|
835
|
+
error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "ListAssociations")
|
|
836
|
+
|
|
837
|
+
mock_paginator.paginate.side_effect = error
|
|
838
|
+
|
|
839
|
+
result = collector._list_associations(mock_ssm_client)
|
|
840
|
+
|
|
841
|
+
assert result == []
|
|
842
|
+
mock_logger.error.assert_called_once()
|
|
843
|
+
assert "Error listing associations" in str(mock_logger.error.call_args)
|
|
844
|
+
|
|
845
|
+
def test_get_compliance_summary_success(self, collector, mock_ssm_client):
|
|
846
|
+
"""Test successful retrieval of compliance summary."""
|
|
847
|
+
mock_ssm_client.list_compliance_summaries.return_value = {
|
|
848
|
+
"ComplianceSummaryItems": [
|
|
849
|
+
{"ComplianceType": "Patch", "CompliantCount": 50, "NonCompliantCount": 5},
|
|
850
|
+
{"ComplianceType": "Association", "CompliantCount": 30, "NonCompliantCount": 2},
|
|
851
|
+
{"ComplianceType": "Custom:Security", "CompliantCount": 20, "NonCompliantCount": 0},
|
|
852
|
+
]
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
result = collector._get_compliance_summary(mock_ssm_client)
|
|
856
|
+
|
|
857
|
+
assert result["TotalCompliant"] == 100
|
|
858
|
+
assert result["TotalNonCompliant"] == 7
|
|
859
|
+
assert len(result["ComplianceTypes"]) == 3
|
|
860
|
+
assert result["ComplianceTypes"][0]["ComplianceType"] == "Patch"
|
|
861
|
+
assert result["ComplianceTypes"][0]["CompliantCount"] == 50
|
|
862
|
+
assert result["ComplianceTypes"][1]["NonCompliantCount"] == 2
|
|
863
|
+
|
|
864
|
+
mock_ssm_client.list_compliance_summaries.assert_called_once_with(MaxResults=50)
|
|
865
|
+
|
|
866
|
+
def test_get_compliance_summary_empty(self, collector, mock_ssm_client):
|
|
867
|
+
"""Test compliance summary with no summaries returned."""
|
|
868
|
+
mock_ssm_client.list_compliance_summaries.return_value = {"ComplianceSummaryItems": []}
|
|
869
|
+
|
|
870
|
+
result = collector._get_compliance_summary(mock_ssm_client)
|
|
871
|
+
|
|
872
|
+
assert result == {}
|
|
873
|
+
|
|
874
|
+
def test_get_compliance_summary_missing_counts(self, collector, mock_ssm_client):
|
|
875
|
+
"""Test compliance summary with missing count fields."""
|
|
876
|
+
mock_ssm_client.list_compliance_summaries.return_value = {
|
|
877
|
+
"ComplianceSummaryItems": [
|
|
878
|
+
{"ComplianceType": "Patch"},
|
|
879
|
+
{"ComplianceType": "Association", "CompliantCount": 10},
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
result = collector._get_compliance_summary(mock_ssm_client)
|
|
884
|
+
|
|
885
|
+
assert result["TotalCompliant"] == 10
|
|
886
|
+
assert result["TotalNonCompliant"] == 0
|
|
887
|
+
assert result["ComplianceTypes"][0]["CompliantCount"] == 0
|
|
888
|
+
assert result["ComplianceTypes"][0]["NonCompliantCount"] == 0
|
|
889
|
+
|
|
890
|
+
@patch(f"{PATH}.logger")
|
|
891
|
+
def test_get_compliance_summary_access_denied(self, mock_logger, collector, mock_ssm_client):
|
|
892
|
+
"""Test compliance summary with access denied error."""
|
|
893
|
+
error = ClientError(
|
|
894
|
+
{"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListComplianceSummaries"
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
mock_ssm_client.list_compliance_summaries.side_effect = error
|
|
898
|
+
|
|
899
|
+
result = collector._get_compliance_summary(mock_ssm_client)
|
|
900
|
+
|
|
901
|
+
assert result == {}
|
|
902
|
+
mock_logger.warning.assert_called_once()
|
|
903
|
+
assert "Access denied to get compliance summary in us-east-1" in str(mock_logger.warning.call_args)
|
|
904
|
+
|
|
905
|
+
@patch(f"{PATH}.logger")
|
|
906
|
+
def test_get_compliance_summary_other_error(self, mock_logger, collector, mock_ssm_client):
|
|
907
|
+
"""Test compliance summary with other ClientError."""
|
|
908
|
+
error = ClientError(
|
|
909
|
+
{"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "ListComplianceSummaries"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
mock_ssm_client.list_compliance_summaries.side_effect = error
|
|
913
|
+
|
|
914
|
+
result = collector._get_compliance_summary(mock_ssm_client)
|
|
915
|
+
|
|
916
|
+
assert result == {}
|
|
917
|
+
mock_logger.debug.assert_called_once()
|
|
918
|
+
assert "Error getting compliance summary" in str(mock_logger.debug.call_args)
|