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.
- 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/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 +34 -4
- 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.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/RECORD +112 -33
- 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.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.3.0.dist-info → regscale_cli-6.28.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for AWS Systems Manager Control Mappings."""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from regscale.integrations.commercial.aws.ssm_control_mappings import (
|
|
8
|
+
SSM_CONTROL_MAPPINGS,
|
|
9
|
+
SSMControlMapper,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSSMControlMappings:
|
|
14
|
+
"""Test SSM control mappings constants."""
|
|
15
|
+
|
|
16
|
+
def test_ssm_control_mappings_exist(self):
|
|
17
|
+
"""Test that SSM control mappings dictionary exists and has content."""
|
|
18
|
+
assert len(SSM_CONTROL_MAPPINGS) > 0
|
|
19
|
+
assert "CM-2" in SSM_CONTROL_MAPPINGS
|
|
20
|
+
assert "CM-6" in SSM_CONTROL_MAPPINGS
|
|
21
|
+
assert "SI-2" in SSM_CONTROL_MAPPINGS
|
|
22
|
+
assert "CM-3" in SSM_CONTROL_MAPPINGS
|
|
23
|
+
assert "CM-8" in SSM_CONTROL_MAPPINGS
|
|
24
|
+
|
|
25
|
+
def test_cm2_mapping_structure(self):
|
|
26
|
+
"""Test CM-2 mapping structure."""
|
|
27
|
+
cm2 = SSM_CONTROL_MAPPINGS["CM-2"]
|
|
28
|
+
assert "name" in cm2
|
|
29
|
+
assert "description" in cm2
|
|
30
|
+
assert "checks" in cm2
|
|
31
|
+
assert "managed_instances" in cm2["checks"]
|
|
32
|
+
assert "inventory_collection" in cm2["checks"]
|
|
33
|
+
assert "state_manager" in cm2["checks"]
|
|
34
|
+
|
|
35
|
+
def test_cm6_mapping_structure(self):
|
|
36
|
+
"""Test CM-6 mapping structure."""
|
|
37
|
+
cm6 = SSM_CONTROL_MAPPINGS["CM-6"]
|
|
38
|
+
assert "name" in cm6
|
|
39
|
+
assert "description" in cm6
|
|
40
|
+
assert "checks" in cm6
|
|
41
|
+
assert "ssm_documents" in cm6["checks"]
|
|
42
|
+
assert "parameters" in cm6["checks"]
|
|
43
|
+
assert "associations" in cm6["checks"]
|
|
44
|
+
|
|
45
|
+
def test_si2_mapping_structure(self):
|
|
46
|
+
"""Test SI-2 mapping structure."""
|
|
47
|
+
si2 = SSM_CONTROL_MAPPINGS["SI-2"]
|
|
48
|
+
assert "name" in si2
|
|
49
|
+
assert "description" in si2
|
|
50
|
+
assert "checks" in si2
|
|
51
|
+
assert "patch_baselines" in si2["checks"]
|
|
52
|
+
assert "patch_compliance" in si2["checks"]
|
|
53
|
+
assert "maintenance_windows" in si2["checks"]
|
|
54
|
+
|
|
55
|
+
def test_cm3_mapping_structure(self):
|
|
56
|
+
"""Test CM-3 mapping structure."""
|
|
57
|
+
cm3 = SSM_CONTROL_MAPPINGS["CM-3"]
|
|
58
|
+
assert "name" in cm3
|
|
59
|
+
assert "description" in cm3
|
|
60
|
+
assert "checks" in cm3
|
|
61
|
+
assert "automation_documents" in cm3["checks"]
|
|
62
|
+
|
|
63
|
+
def test_cm8_mapping_structure(self):
|
|
64
|
+
"""Test CM-8 mapping structure."""
|
|
65
|
+
cm8 = SSM_CONTROL_MAPPINGS["CM-8"]
|
|
66
|
+
assert "name" in cm8
|
|
67
|
+
assert "description" in cm8
|
|
68
|
+
assert "checks" in cm8
|
|
69
|
+
assert "inventory_data" in cm8["checks"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestSSMControlMapperInitialization:
|
|
73
|
+
"""Test SSMControlMapper initialization."""
|
|
74
|
+
|
|
75
|
+
def test_init_with_nist_framework(self):
|
|
76
|
+
"""Test initialization with NIST framework."""
|
|
77
|
+
mapper = SSMControlMapper(framework="NIST800-53R5")
|
|
78
|
+
assert mapper.framework == "NIST800-53R5"
|
|
79
|
+
assert mapper.mappings == SSM_CONTROL_MAPPINGS
|
|
80
|
+
|
|
81
|
+
def test_init_default_framework(self):
|
|
82
|
+
"""Test initialization with default framework."""
|
|
83
|
+
mapper = SSMControlMapper()
|
|
84
|
+
assert mapper.framework == "NIST800-53R5"
|
|
85
|
+
assert mapper.mappings == SSM_CONTROL_MAPPINGS
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestAssessSSMCompliance:
|
|
89
|
+
"""Test assess_ssm_compliance method."""
|
|
90
|
+
|
|
91
|
+
def test_assess_compliant_ssm_configuration(self):
|
|
92
|
+
"""Test assessing a fully compliant SSM configuration."""
|
|
93
|
+
mapper = SSMControlMapper()
|
|
94
|
+
ssm_data = {
|
|
95
|
+
"ManagedInstances": [
|
|
96
|
+
{"InstanceId": "i-123", "PingStatus": "Online", "PatchSummary": {"Missing": 0}},
|
|
97
|
+
{"InstanceId": "i-456", "PingStatus": "Online", "PatchSummary": {"Missing": 0}},
|
|
98
|
+
],
|
|
99
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
100
|
+
"Documents": [
|
|
101
|
+
{"Name": "ConfigDoc", "DocumentType": "Command"},
|
|
102
|
+
{"Name": "AutoDoc", "DocumentType": "Automation"},
|
|
103
|
+
],
|
|
104
|
+
"Parameters": [{"Name": "param1", "Type": "String"}],
|
|
105
|
+
"PatchBaselines": [{"BaselineId": "pb-123", "OperatingSystem": "AMAZON_LINUX_2"}],
|
|
106
|
+
"MaintenanceWindows": [{"WindowId": "mw-123"}],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
results = mapper.assess_ssm_compliance(ssm_data)
|
|
110
|
+
|
|
111
|
+
assert results["CM-2"] == "PASS"
|
|
112
|
+
assert results["CM-6"] == "PASS"
|
|
113
|
+
assert results["SI-2"] == "PASS"
|
|
114
|
+
assert results["CM-3"] == "PASS"
|
|
115
|
+
assert results["CM-8"] == "PASS"
|
|
116
|
+
|
|
117
|
+
def test_assess_ssm_without_managed_instances(self):
|
|
118
|
+
"""Test assessing SSM without managed instances."""
|
|
119
|
+
mapper = SSMControlMapper()
|
|
120
|
+
ssm_data = {
|
|
121
|
+
"ManagedInstances": [],
|
|
122
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
123
|
+
"Documents": [{"Name": "Doc1", "DocumentType": "Command"}],
|
|
124
|
+
"Parameters": [{"Name": "param1"}],
|
|
125
|
+
"PatchBaselines": [{"BaselineId": "pb-123"}],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
results = mapper.assess_ssm_compliance(ssm_data)
|
|
129
|
+
|
|
130
|
+
assert results["CM-2"] == "FAIL"
|
|
131
|
+
assert results["CM-8"] == "FAIL"
|
|
132
|
+
|
|
133
|
+
def test_assess_ssm_without_patch_baselines(self):
|
|
134
|
+
"""Test assessing SSM without patch baselines."""
|
|
135
|
+
mapper = SSMControlMapper()
|
|
136
|
+
ssm_data = {
|
|
137
|
+
"ManagedInstances": [{"InstanceId": "i-123", "PingStatus": "Online"}],
|
|
138
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
139
|
+
"Documents": [{"Name": "Doc1", "DocumentType": "Automation"}],
|
|
140
|
+
"Parameters": [{"Name": "param1"}],
|
|
141
|
+
"PatchBaselines": [],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
results = mapper.assess_ssm_compliance(ssm_data)
|
|
145
|
+
|
|
146
|
+
assert results["SI-2"] == "FAIL"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestAssessCM2:
|
|
150
|
+
"""Test _assess_cm2 method."""
|
|
151
|
+
|
|
152
|
+
def test_ssm_with_online_instances_and_associations_passes(self):
|
|
153
|
+
"""Test SSM with online instances and associations passes."""
|
|
154
|
+
mapper = SSMControlMapper()
|
|
155
|
+
ssm_data = {
|
|
156
|
+
"ManagedInstances": [{"InstanceId": "i-123", "PingStatus": "Online"}],
|
|
157
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
result = mapper._assess_cm2(ssm_data)
|
|
161
|
+
assert result == "PASS"
|
|
162
|
+
|
|
163
|
+
def test_ssm_without_managed_instances_fails(self):
|
|
164
|
+
"""Test SSM without managed instances fails."""
|
|
165
|
+
mapper = SSMControlMapper()
|
|
166
|
+
ssm_data = {"ManagedInstances": [], "Associations": [{"AssociationId": "assoc-1"}]}
|
|
167
|
+
|
|
168
|
+
result = mapper._assess_cm2(ssm_data)
|
|
169
|
+
assert result == "FAIL"
|
|
170
|
+
|
|
171
|
+
def test_ssm_with_offline_instances_fails(self):
|
|
172
|
+
"""Test SSM with only offline instances fails."""
|
|
173
|
+
mapper = SSMControlMapper()
|
|
174
|
+
ssm_data = {
|
|
175
|
+
"ManagedInstances": [{"InstanceId": "i-123", "PingStatus": "ConnectionLost"}],
|
|
176
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
result = mapper._assess_cm2(ssm_data)
|
|
180
|
+
assert result == "FAIL"
|
|
181
|
+
|
|
182
|
+
def test_ssm_without_associations_fails(self):
|
|
183
|
+
"""Test SSM without associations fails."""
|
|
184
|
+
mapper = SSMControlMapper()
|
|
185
|
+
ssm_data = {"ManagedInstances": [{"InstanceId": "i-123", "PingStatus": "Online"}], "Associations": []}
|
|
186
|
+
|
|
187
|
+
result = mapper._assess_cm2(ssm_data)
|
|
188
|
+
assert result == "FAIL"
|
|
189
|
+
|
|
190
|
+
def test_ssm_with_mixed_instance_status_passes(self):
|
|
191
|
+
"""Test SSM with mixed instance status passes if at least one is online."""
|
|
192
|
+
mapper = SSMControlMapper()
|
|
193
|
+
ssm_data = {
|
|
194
|
+
"ManagedInstances": [
|
|
195
|
+
{"InstanceId": "i-123", "PingStatus": "Online"},
|
|
196
|
+
{"InstanceId": "i-456", "PingStatus": "ConnectionLost"},
|
|
197
|
+
],
|
|
198
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
result = mapper._assess_cm2(ssm_data)
|
|
202
|
+
assert result == "PASS"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestAssessCM6:
|
|
206
|
+
"""Test _assess_cm6 method."""
|
|
207
|
+
|
|
208
|
+
def test_ssm_with_documents_parameters_and_associations_passes(self):
|
|
209
|
+
"""Test SSM with documents, parameters, and associations passes."""
|
|
210
|
+
mapper = SSMControlMapper()
|
|
211
|
+
ssm_data = {
|
|
212
|
+
"Documents": [{"Name": "Doc1"}],
|
|
213
|
+
"Parameters": [{"Name": "param1"}],
|
|
214
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result = mapper._assess_cm6(ssm_data)
|
|
218
|
+
assert result == "PASS"
|
|
219
|
+
|
|
220
|
+
def test_ssm_without_documents_fails(self):
|
|
221
|
+
"""Test SSM without documents fails."""
|
|
222
|
+
mapper = SSMControlMapper()
|
|
223
|
+
ssm_data = {"Documents": [], "Parameters": [{"Name": "param1"}], "Associations": [{"AssociationId": "assoc-1"}]}
|
|
224
|
+
|
|
225
|
+
result = mapper._assess_cm6(ssm_data)
|
|
226
|
+
assert result == "FAIL"
|
|
227
|
+
|
|
228
|
+
def test_ssm_without_parameters_fails(self):
|
|
229
|
+
"""Test SSM without parameters fails."""
|
|
230
|
+
mapper = SSMControlMapper()
|
|
231
|
+
ssm_data = {"Documents": [{"Name": "Doc1"}], "Parameters": [], "Associations": [{"AssociationId": "assoc-1"}]}
|
|
232
|
+
|
|
233
|
+
result = mapper._assess_cm6(ssm_data)
|
|
234
|
+
assert result == "FAIL"
|
|
235
|
+
|
|
236
|
+
def test_ssm_without_associations_fails(self):
|
|
237
|
+
"""Test SSM without associations fails."""
|
|
238
|
+
mapper = SSMControlMapper()
|
|
239
|
+
ssm_data = {"Documents": [{"Name": "Doc1"}], "Parameters": [{"Name": "param1"}], "Associations": []}
|
|
240
|
+
|
|
241
|
+
result = mapper._assess_cm6(ssm_data)
|
|
242
|
+
assert result == "FAIL"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class TestAssessSI2:
|
|
246
|
+
"""Test _assess_si2 method."""
|
|
247
|
+
|
|
248
|
+
def test_ssm_with_patch_baselines_and_compliant_instances_passes(self):
|
|
249
|
+
"""Test SSM with patch baselines and compliant instances passes."""
|
|
250
|
+
mapper = SSMControlMapper()
|
|
251
|
+
ssm_data = {
|
|
252
|
+
"PatchBaselines": [{"BaselineId": "pb-123"}],
|
|
253
|
+
"ManagedInstances": [{"InstanceId": "i-123", "PatchSummary": {"Missing": 0}}],
|
|
254
|
+
"MaintenanceWindows": [{"WindowId": "mw-123"}],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
result = mapper._assess_si2(ssm_data)
|
|
258
|
+
assert result == "PASS"
|
|
259
|
+
|
|
260
|
+
def test_ssm_without_patch_baselines_fails(self):
|
|
261
|
+
"""Test SSM without patch baselines fails."""
|
|
262
|
+
mapper = SSMControlMapper()
|
|
263
|
+
ssm_data = {
|
|
264
|
+
"PatchBaselines": [],
|
|
265
|
+
"ManagedInstances": [{"InstanceId": "i-123", "PatchSummary": {"Missing": 0}}],
|
|
266
|
+
"MaintenanceWindows": [],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
result = mapper._assess_si2(ssm_data)
|
|
270
|
+
assert result == "FAIL"
|
|
271
|
+
|
|
272
|
+
def test_ssm_without_patch_data_fails(self):
|
|
273
|
+
"""Test SSM without patch data on instances fails."""
|
|
274
|
+
mapper = SSMControlMapper()
|
|
275
|
+
ssm_data = {
|
|
276
|
+
"PatchBaselines": [{"BaselineId": "pb-123"}],
|
|
277
|
+
"ManagedInstances": [{"InstanceId": "i-123"}],
|
|
278
|
+
"MaintenanceWindows": [],
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
result = mapper._assess_si2(ssm_data)
|
|
282
|
+
assert result == "FAIL"
|
|
283
|
+
|
|
284
|
+
def test_ssm_with_missing_patches_fails(self):
|
|
285
|
+
"""Test SSM with missing patches fails."""
|
|
286
|
+
mapper = SSMControlMapper()
|
|
287
|
+
ssm_data = {
|
|
288
|
+
"PatchBaselines": [{"BaselineId": "pb-123"}],
|
|
289
|
+
"ManagedInstances": [
|
|
290
|
+
{"InstanceId": "i-123", "PatchSummary": {"Missing": 5}},
|
|
291
|
+
{"InstanceId": "i-456", "PatchSummary": {"Missing": 3}},
|
|
292
|
+
],
|
|
293
|
+
"MaintenanceWindows": [],
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
result = mapper._assess_si2(ssm_data)
|
|
297
|
+
assert result == "FAIL"
|
|
298
|
+
|
|
299
|
+
def test_ssm_with_patch_baselines_but_no_instances_passes(self):
|
|
300
|
+
"""Test SSM with patch baselines but no instances passes."""
|
|
301
|
+
mapper = SSMControlMapper()
|
|
302
|
+
ssm_data = {"PatchBaselines": [{"BaselineId": "pb-123"}], "ManagedInstances": [], "MaintenanceWindows": []}
|
|
303
|
+
|
|
304
|
+
result = mapper._assess_si2(ssm_data)
|
|
305
|
+
assert result == "PASS"
|
|
306
|
+
|
|
307
|
+
def test_ssm_without_maintenance_windows_passes_with_note(self):
|
|
308
|
+
"""Test SSM without maintenance windows passes but notes recommendation."""
|
|
309
|
+
mapper = SSMControlMapper()
|
|
310
|
+
ssm_data = {
|
|
311
|
+
"PatchBaselines": [{"BaselineId": "pb-123"}],
|
|
312
|
+
"ManagedInstances": [{"InstanceId": "i-123", "PatchSummary": {"Missing": 0}}],
|
|
313
|
+
"MaintenanceWindows": [],
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
result = mapper._assess_si2(ssm_data)
|
|
317
|
+
assert result == "PASS"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TestAssessCM3:
|
|
321
|
+
"""Test _assess_cm3 method."""
|
|
322
|
+
|
|
323
|
+
def test_ssm_with_automation_documents_passes(self):
|
|
324
|
+
"""Test SSM with automation documents passes."""
|
|
325
|
+
mapper = SSMControlMapper()
|
|
326
|
+
ssm_data = {"Documents": [{"Name": "AutoDoc", "DocumentType": "Automation"}]}
|
|
327
|
+
|
|
328
|
+
result = mapper._assess_cm3(ssm_data)
|
|
329
|
+
assert result == "PASS"
|
|
330
|
+
|
|
331
|
+
def test_ssm_without_automation_documents_fails(self):
|
|
332
|
+
"""Test SSM without automation documents fails."""
|
|
333
|
+
mapper = SSMControlMapper()
|
|
334
|
+
ssm_data = {"Documents": [{"Name": "CommandDoc", "DocumentType": "Command"}]}
|
|
335
|
+
|
|
336
|
+
result = mapper._assess_cm3(ssm_data)
|
|
337
|
+
assert result == "FAIL"
|
|
338
|
+
|
|
339
|
+
def test_ssm_with_no_documents_fails(self):
|
|
340
|
+
"""Test SSM with no documents fails."""
|
|
341
|
+
mapper = SSMControlMapper()
|
|
342
|
+
ssm_data = {"Documents": []}
|
|
343
|
+
|
|
344
|
+
result = mapper._assess_cm3(ssm_data)
|
|
345
|
+
assert result == "FAIL"
|
|
346
|
+
|
|
347
|
+
def test_ssm_with_multiple_automation_documents_passes(self):
|
|
348
|
+
"""Test SSM with multiple automation documents passes."""
|
|
349
|
+
mapper = SSMControlMapper()
|
|
350
|
+
ssm_data = {
|
|
351
|
+
"Documents": [
|
|
352
|
+
{"Name": "AutoDoc1", "DocumentType": "Automation"},
|
|
353
|
+
{"Name": "AutoDoc2", "DocumentType": "Automation"},
|
|
354
|
+
{"Name": "CommandDoc", "DocumentType": "Command"},
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
result = mapper._assess_cm3(ssm_data)
|
|
359
|
+
assert result == "PASS"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class TestAssessCM8:
|
|
363
|
+
"""Test _assess_cm8 method."""
|
|
364
|
+
|
|
365
|
+
def test_ssm_with_online_instances_passes(self):
|
|
366
|
+
"""Test SSM with online instances passes."""
|
|
367
|
+
mapper = SSMControlMapper()
|
|
368
|
+
ssm_data = {"ManagedInstances": [{"InstanceId": "i-123", "PingStatus": "Online"}]}
|
|
369
|
+
|
|
370
|
+
result = mapper._assess_cm8(ssm_data)
|
|
371
|
+
assert result == "PASS"
|
|
372
|
+
|
|
373
|
+
def test_ssm_without_managed_instances_fails(self):
|
|
374
|
+
"""Test SSM without managed instances fails."""
|
|
375
|
+
mapper = SSMControlMapper()
|
|
376
|
+
ssm_data = {"ManagedInstances": []}
|
|
377
|
+
|
|
378
|
+
result = mapper._assess_cm8(ssm_data)
|
|
379
|
+
assert result == "FAIL"
|
|
380
|
+
|
|
381
|
+
def test_ssm_with_offline_instances_fails(self):
|
|
382
|
+
"""Test SSM with only offline instances fails."""
|
|
383
|
+
mapper = SSMControlMapper()
|
|
384
|
+
ssm_data = {
|
|
385
|
+
"ManagedInstances": [
|
|
386
|
+
{"InstanceId": "i-123", "PingStatus": "ConnectionLost"},
|
|
387
|
+
{"InstanceId": "i-456", "PingStatus": "Inactive"},
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
result = mapper._assess_cm8(ssm_data)
|
|
392
|
+
assert result == "FAIL"
|
|
393
|
+
|
|
394
|
+
def test_ssm_with_mixed_instance_status_passes(self):
|
|
395
|
+
"""Test SSM with mixed instance status passes if at least one is online."""
|
|
396
|
+
mapper = SSMControlMapper()
|
|
397
|
+
ssm_data = {
|
|
398
|
+
"ManagedInstances": [
|
|
399
|
+
{"InstanceId": "i-123", "PingStatus": "Online"},
|
|
400
|
+
{"InstanceId": "i-456", "PingStatus": "ConnectionLost"},
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
result = mapper._assess_cm8(ssm_data)
|
|
405
|
+
assert result == "PASS"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class TestGetControlDescription:
|
|
409
|
+
"""Test get_control_description method."""
|
|
410
|
+
|
|
411
|
+
def test_get_cm2_description(self):
|
|
412
|
+
"""Test getting CM-2 description."""
|
|
413
|
+
mapper = SSMControlMapper()
|
|
414
|
+
description = mapper.get_control_description("CM-2")
|
|
415
|
+
|
|
416
|
+
assert description is not None
|
|
417
|
+
assert "Baseline Configuration" in description
|
|
418
|
+
|
|
419
|
+
def test_get_cm6_description(self):
|
|
420
|
+
"""Test getting CM-6 description."""
|
|
421
|
+
mapper = SSMControlMapper()
|
|
422
|
+
description = mapper.get_control_description("CM-6")
|
|
423
|
+
|
|
424
|
+
assert description is not None
|
|
425
|
+
assert "Configuration Settings" in description
|
|
426
|
+
|
|
427
|
+
def test_get_si2_description(self):
|
|
428
|
+
"""Test getting SI-2 description."""
|
|
429
|
+
mapper = SSMControlMapper()
|
|
430
|
+
description = mapper.get_control_description("SI-2")
|
|
431
|
+
|
|
432
|
+
assert description is not None
|
|
433
|
+
assert "Flaw Remediation" in description
|
|
434
|
+
|
|
435
|
+
def test_get_cm3_description(self):
|
|
436
|
+
"""Test getting CM-3 description."""
|
|
437
|
+
mapper = SSMControlMapper()
|
|
438
|
+
description = mapper.get_control_description("CM-3")
|
|
439
|
+
|
|
440
|
+
assert description is not None
|
|
441
|
+
assert "Configuration Change Control" in description
|
|
442
|
+
|
|
443
|
+
def test_get_cm8_description(self):
|
|
444
|
+
"""Test getting CM-8 description."""
|
|
445
|
+
mapper = SSMControlMapper()
|
|
446
|
+
description = mapper.get_control_description("CM-8")
|
|
447
|
+
|
|
448
|
+
assert description is not None
|
|
449
|
+
assert "System Component Inventory" in description
|
|
450
|
+
|
|
451
|
+
def test_get_unknown_control_description(self):
|
|
452
|
+
"""Test getting description for unknown control."""
|
|
453
|
+
mapper = SSMControlMapper()
|
|
454
|
+
description = mapper.get_control_description("UNKNOWN-1")
|
|
455
|
+
|
|
456
|
+
assert description is None
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class TestGetMappedControls:
|
|
460
|
+
"""Test get_mapped_controls method."""
|
|
461
|
+
|
|
462
|
+
def test_get_mapped_controls(self):
|
|
463
|
+
"""Test getting all mapped controls."""
|
|
464
|
+
mapper = SSMControlMapper()
|
|
465
|
+
controls = mapper.get_mapped_controls()
|
|
466
|
+
|
|
467
|
+
assert len(controls) == 5
|
|
468
|
+
assert "CM-2" in controls
|
|
469
|
+
assert "CM-6" in controls
|
|
470
|
+
assert "SI-2" in controls
|
|
471
|
+
assert "CM-3" in controls
|
|
472
|
+
assert "CM-8" in controls
|
|
473
|
+
|
|
474
|
+
def test_controls_are_unique(self):
|
|
475
|
+
"""Test that returned controls are unique."""
|
|
476
|
+
mapper = SSMControlMapper()
|
|
477
|
+
controls = mapper.get_mapped_controls()
|
|
478
|
+
|
|
479
|
+
assert len(controls) == len(set(controls))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class TestGetCheckDetails:
|
|
483
|
+
"""Test get_check_details method."""
|
|
484
|
+
|
|
485
|
+
def test_get_cm2_check_details(self):
|
|
486
|
+
"""Test getting CM-2 check details."""
|
|
487
|
+
mapper = SSMControlMapper()
|
|
488
|
+
details = mapper.get_check_details("CM-2")
|
|
489
|
+
|
|
490
|
+
assert details is not None
|
|
491
|
+
assert "managed_instances" in details
|
|
492
|
+
assert "inventory_collection" in details
|
|
493
|
+
assert "state_manager" in details
|
|
494
|
+
assert details["managed_instances"]["weight"] == 100
|
|
495
|
+
|
|
496
|
+
def test_get_si2_check_details(self):
|
|
497
|
+
"""Test getting SI-2 check details."""
|
|
498
|
+
mapper = SSMControlMapper()
|
|
499
|
+
details = mapper.get_check_details("SI-2")
|
|
500
|
+
|
|
501
|
+
assert details is not None
|
|
502
|
+
assert "patch_baselines" in details
|
|
503
|
+
assert "patch_compliance" in details
|
|
504
|
+
assert "maintenance_windows" in details
|
|
505
|
+
|
|
506
|
+
def test_get_cm3_check_details(self):
|
|
507
|
+
"""Test getting CM-3 check details."""
|
|
508
|
+
mapper = SSMControlMapper()
|
|
509
|
+
details = mapper.get_check_details("CM-3")
|
|
510
|
+
|
|
511
|
+
assert details is not None
|
|
512
|
+
assert "automation_documents" in details
|
|
513
|
+
|
|
514
|
+
def test_get_unknown_control_check_details(self):
|
|
515
|
+
"""Test getting check details for unknown control."""
|
|
516
|
+
mapper = SSMControlMapper()
|
|
517
|
+
details = mapper.get_check_details("UNKNOWN-1")
|
|
518
|
+
|
|
519
|
+
assert details is None
|
|
520
|
+
|
|
521
|
+
def test_check_details_structure(self):
|
|
522
|
+
"""Test check details have required structure."""
|
|
523
|
+
mapper = SSMControlMapper()
|
|
524
|
+
details = mapper.get_check_details("CM-2")
|
|
525
|
+
|
|
526
|
+
for check_name, check_data in details.items():
|
|
527
|
+
assert "weight" in check_data
|
|
528
|
+
assert "pass_criteria" in check_data
|
|
529
|
+
assert "fail_criteria" in check_data
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class TestEdgeCases:
|
|
533
|
+
"""Test edge cases and error handling."""
|
|
534
|
+
|
|
535
|
+
def test_empty_ssm_data(self):
|
|
536
|
+
"""Test assessment with completely empty SSM data."""
|
|
537
|
+
mapper = SSMControlMapper()
|
|
538
|
+
ssm_data = {}
|
|
539
|
+
|
|
540
|
+
results = mapper.assess_ssm_compliance(ssm_data)
|
|
541
|
+
|
|
542
|
+
# All controls should fail with empty data
|
|
543
|
+
assert results["CM-2"] == "FAIL"
|
|
544
|
+
assert results["CM-6"] == "FAIL"
|
|
545
|
+
assert results["SI-2"] == "FAIL"
|
|
546
|
+
assert results["CM-3"] == "FAIL"
|
|
547
|
+
assert results["CM-8"] == "FAIL"
|
|
548
|
+
|
|
549
|
+
def test_ssm_data_with_none_values(self):
|
|
550
|
+
"""Test assessment with None values in SSM data."""
|
|
551
|
+
mapper = SSMControlMapper()
|
|
552
|
+
ssm_data = {
|
|
553
|
+
"ManagedInstances": None,
|
|
554
|
+
"Associations": None,
|
|
555
|
+
"Documents": None,
|
|
556
|
+
"Parameters": None,
|
|
557
|
+
"PatchBaselines": None,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# The source code doesn't handle None values, so this will raise TypeError
|
|
561
|
+
# Testing that the code fails appropriately when given None instead of lists
|
|
562
|
+
with pytest.raises(TypeError):
|
|
563
|
+
mapper.assess_ssm_compliance(ssm_data)
|
|
564
|
+
|
|
565
|
+
def test_missing_ping_status(self):
|
|
566
|
+
"""Test instances without PingStatus attribute."""
|
|
567
|
+
mapper = SSMControlMapper()
|
|
568
|
+
ssm_data = {
|
|
569
|
+
"ManagedInstances": [{"InstanceId": "i-123"}],
|
|
570
|
+
"Associations": [{"AssociationId": "assoc-1"}],
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
result = mapper._assess_cm2(ssm_data)
|
|
574
|
+
# Should fail because no instances have Online status
|
|
575
|
+
assert result == "FAIL"
|
|
576
|
+
|
|
577
|
+
def test_missing_document_type(self):
|
|
578
|
+
"""Test documents without DocumentType attribute."""
|
|
579
|
+
mapper = SSMControlMapper()
|
|
580
|
+
ssm_data = {"Documents": [{"Name": "Doc1"}]}
|
|
581
|
+
|
|
582
|
+
result = mapper._assess_cm3(ssm_data)
|
|
583
|
+
# Should fail because no Automation documents found
|
|
584
|
+
assert result == "FAIL"
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
if __name__ == "__main__":
|
|
588
|
+
pytest.main([__file__, "-v"])
|