regscale-cli 6.25.1.0__py3-none-any.whl → 6.26.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/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +150 -96
- regscale/models/integration_models/cisa_kev_data.json +154 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Unit tests for Wiz Compliance Report Integration (no authentication required).
|
|
5
|
+
|
|
6
|
+
These tests focus on the core compliance workflow components without requiring
|
|
7
|
+
live authentication to RegScale or Wiz APIs. They test:
|
|
8
|
+
- Report parsing logic
|
|
9
|
+
- Data transformation
|
|
10
|
+
- Control consolidation
|
|
11
|
+
- Issue creation logic
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import csv
|
|
15
|
+
import os
|
|
16
|
+
import tempfile
|
|
17
|
+
import unittest
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestWizComplianceUnit(unittest.TestCase):
|
|
24
|
+
"""Unit tests for Wiz Compliance Report functionality without authentication."""
|
|
25
|
+
|
|
26
|
+
def setUp(self):
|
|
27
|
+
"""Set up test fixtures."""
|
|
28
|
+
# Sample compliance data representing the expected results format
|
|
29
|
+
self.compliance_data = [
|
|
30
|
+
# Passing controls
|
|
31
|
+
{
|
|
32
|
+
"Resource Name": "vm-production-001",
|
|
33
|
+
"Cloud Provider": "Azure",
|
|
34
|
+
"Cloud Provider ID": "/subscriptions/12345/resourceGroups/prod-rg/providers/Microsoft.Compute/virtualMachines/vm-production-001",
|
|
35
|
+
"Resource ID": "/subscriptions/12345/resourceGroups/prod-rg/providers/Microsoft.Compute/virtualMachines/vm-production-001",
|
|
36
|
+
"Resource Region": "East US 2",
|
|
37
|
+
"Subscription": "prod-subscription-123",
|
|
38
|
+
"Subscription Name": "Production Subscription",
|
|
39
|
+
"Policy Name": "Ensure VM has monitoring agent installed",
|
|
40
|
+
"Policy ID": "azure-vm-monitoring-001",
|
|
41
|
+
"Result": "Pass",
|
|
42
|
+
"Control IDs": "AC-2,AC-3",
|
|
43
|
+
"Framework": "NIST 800-53",
|
|
44
|
+
"Severity": "High",
|
|
45
|
+
"Control Family": "Access Control",
|
|
46
|
+
"Details": "Monitoring agent is properly installed and configured",
|
|
47
|
+
"Status": "Compliant",
|
|
48
|
+
},
|
|
49
|
+
# Failing control that should create an issue
|
|
50
|
+
{
|
|
51
|
+
"Resource Name": "vm-test-002",
|
|
52
|
+
"Cloud Provider": "Azure",
|
|
53
|
+
"Cloud Provider ID": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/vm-test-002",
|
|
54
|
+
"Resource ID": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/vm-test-002",
|
|
55
|
+
"Resource Region": "West US 2",
|
|
56
|
+
"Subscription": "test-subscription-456",
|
|
57
|
+
"Subscription Name": "Test Subscription",
|
|
58
|
+
"Policy Name": "Ensure disk encryption is enabled",
|
|
59
|
+
"Policy ID": "azure-disk-encryption-001",
|
|
60
|
+
"Result": "Fail",
|
|
61
|
+
"Control IDs": "SC-28,SC-13",
|
|
62
|
+
"Framework": "NIST 800-53",
|
|
63
|
+
"Severity": "Critical",
|
|
64
|
+
"Control Family": "System and Communications Protection",
|
|
65
|
+
"Details": "Disk encryption is not enabled on this virtual machine",
|
|
66
|
+
"Status": "Non-Compliant",
|
|
67
|
+
},
|
|
68
|
+
# Multiple control IDs test case
|
|
69
|
+
{
|
|
70
|
+
"Resource Name": "storage-account-001",
|
|
71
|
+
"Cloud Provider": "Azure",
|
|
72
|
+
"Cloud Provider ID": "/subscriptions/12345/resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/prodstore001",
|
|
73
|
+
"Resource ID": "/subscriptions/12345/resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/prodstore001",
|
|
74
|
+
"Resource Region": "East US",
|
|
75
|
+
"Subscription": "prod-subscription-123",
|
|
76
|
+
"Subscription Name": "Production Subscription",
|
|
77
|
+
"Policy Name": "Storage account should use secure transfer",
|
|
78
|
+
"Policy ID": "azure-storage-secure-001",
|
|
79
|
+
"Result": "Fail",
|
|
80
|
+
"Control IDs": "SC-8,SC-23,IA-7",
|
|
81
|
+
"Framework": "NIST 800-53",
|
|
82
|
+
"Severity": "Medium",
|
|
83
|
+
"Control Family": "System and Communications Protection",
|
|
84
|
+
"Details": "Secure transfer is not required for this storage account",
|
|
85
|
+
"Status": "Non-Compliant",
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
def test_wiz_compliance_item_creation(self):
|
|
90
|
+
"""Test creation of WizComplianceReportItem from CSV data."""
|
|
91
|
+
from regscale.integrations.commercial.wizv2.compliance_report import WizComplianceReportItem
|
|
92
|
+
|
|
93
|
+
csv_row = self.compliance_data[0] # Use the first test data item
|
|
94
|
+
|
|
95
|
+
compliance_item = WizComplianceReportItem(csv_row)
|
|
96
|
+
|
|
97
|
+
self.assertEqual(compliance_item.resource_name, "vm-production-001 (East US 2)")
|
|
98
|
+
self.assertEqual(compliance_item.cloud_provider, "Azure")
|
|
99
|
+
self.assertEqual(compliance_item.result, "Pass")
|
|
100
|
+
self.assertEqual(compliance_item.policy_name, "Ensure VM has monitoring agent installed")
|
|
101
|
+
|
|
102
|
+
def test_csv_data_parsing(self):
|
|
103
|
+
"""Test that CSV data is properly parsed."""
|
|
104
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as temp_file:
|
|
105
|
+
writer = csv.DictWriter(temp_file, fieldnames=self.compliance_data[0].keys())
|
|
106
|
+
writer.writeheader()
|
|
107
|
+
writer.writerows(self.compliance_data)
|
|
108
|
+
temp_file.flush()
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
with open(temp_file.name, "r") as f:
|
|
112
|
+
reader = csv.DictReader(f)
|
|
113
|
+
parsed_data = list(reader)
|
|
114
|
+
|
|
115
|
+
self.assertEqual(len(parsed_data), 3)
|
|
116
|
+
self.assertEqual(parsed_data[0]["Resource Name"], "vm-production-001")
|
|
117
|
+
self.assertEqual(parsed_data[1]["Result"], "Fail")
|
|
118
|
+
self.assertEqual(parsed_data[2]["Control IDs"], "SC-8,SC-23,IA-7")
|
|
119
|
+
|
|
120
|
+
finally:
|
|
121
|
+
os.unlink(temp_file.name)
|
|
122
|
+
|
|
123
|
+
def test_control_id_extraction(self):
|
|
124
|
+
"""Test extraction of multiple control IDs from a single compliance item."""
|
|
125
|
+
test_data = {"Control IDs": "AC-2,AC-3,AC-4", "Framework": "NIST 800-53"}
|
|
126
|
+
|
|
127
|
+
# Simulate the control ID parsing logic
|
|
128
|
+
control_ids = [cid.strip() for cid in test_data["Control IDs"].split(",")]
|
|
129
|
+
|
|
130
|
+
self.assertEqual(len(control_ids), 3)
|
|
131
|
+
self.assertIn("AC-2", control_ids)
|
|
132
|
+
self.assertIn("AC-3", control_ids)
|
|
133
|
+
self.assertIn("AC-4", control_ids)
|
|
134
|
+
|
|
135
|
+
def test_status_matching_logic(self):
|
|
136
|
+
"""Test that status values are properly mapped."""
|
|
137
|
+
# Test various status mapping scenarios
|
|
138
|
+
status_mappings = {
|
|
139
|
+
"Pass": "Compliant",
|
|
140
|
+
"Fail": "Non-Compliant",
|
|
141
|
+
"Warning": "Partially Compliant",
|
|
142
|
+
"Info": "Informational",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for result, expected_status in status_mappings.items():
|
|
146
|
+
# Simulate status mapping logic
|
|
147
|
+
if result.lower() == "pass":
|
|
148
|
+
mapped_status = "Compliant"
|
|
149
|
+
elif result.lower() == "fail":
|
|
150
|
+
mapped_status = "Non-Compliant"
|
|
151
|
+
elif result.lower() == "warning":
|
|
152
|
+
mapped_status = "Partially Compliant"
|
|
153
|
+
else:
|
|
154
|
+
mapped_status = "Informational"
|
|
155
|
+
|
|
156
|
+
self.assertEqual(mapped_status, expected_status)
|
|
157
|
+
|
|
158
|
+
def test_asset_creation_data_structure(self):
|
|
159
|
+
"""Test that asset data is properly structured for RegScale."""
|
|
160
|
+
compliance_item = self.compliance_data[0]
|
|
161
|
+
|
|
162
|
+
# Simulate asset creation logic
|
|
163
|
+
asset_data = {
|
|
164
|
+
"name": compliance_item["Resource Name"],
|
|
165
|
+
"cloudProviderId": compliance_item["Cloud Provider ID"],
|
|
166
|
+
"region": compliance_item["Resource Region"],
|
|
167
|
+
"assetType": "Cloud Resource",
|
|
168
|
+
"description": f"{compliance_item['Cloud Provider']} resource",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
self.assertEqual(asset_data["name"], "vm-production-001")
|
|
172
|
+
self.assertTrue(asset_data["cloudProviderId"].startswith("/subscriptions/"))
|
|
173
|
+
self.assertEqual(asset_data["region"], "East US 2")
|
|
174
|
+
self.assertEqual(asset_data["assetType"], "Cloud Resource")
|
|
175
|
+
|
|
176
|
+
def test_issue_creation_for_failed_controls(self):
|
|
177
|
+
"""Test that issues are properly created for failed compliance items."""
|
|
178
|
+
failed_item = self.compliance_data[1] # The failing disk encryption item
|
|
179
|
+
|
|
180
|
+
# Simulate issue creation logic
|
|
181
|
+
if failed_item["Result"].lower() == "fail":
|
|
182
|
+
issue_data = {
|
|
183
|
+
"title": f"Wiz Compliance: {failed_item['Policy Name']}",
|
|
184
|
+
"description": failed_item["Details"],
|
|
185
|
+
"severity": failed_item["Severity"],
|
|
186
|
+
"status": "Open",
|
|
187
|
+
"source": "Wiz Compliance Scan",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
self.assertEqual(issue_data["title"], "Wiz Compliance: Ensure disk encryption is enabled")
|
|
191
|
+
self.assertEqual(issue_data["severity"], "Critical")
|
|
192
|
+
self.assertEqual(issue_data["status"], "Open")
|
|
193
|
+
self.assertIn("encryption", issue_data["description"].lower())
|
|
194
|
+
|
|
195
|
+
def test_control_aggregation_threshold_logic(self):
|
|
196
|
+
"""Test control aggregation and threshold logic."""
|
|
197
|
+
|
|
198
|
+
# Test multiple items for the same control
|
|
199
|
+
control_items = [
|
|
200
|
+
{"Control IDs": "AC-2", "Result": "Pass", "Resource Name": "resource1"},
|
|
201
|
+
{"Control IDs": "AC-2", "Result": "Fail", "Resource Name": "resource2"},
|
|
202
|
+
{"Control IDs": "AC-2", "Result": "Pass", "Resource Name": "resource3"},
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
# Simulate aggregation logic
|
|
206
|
+
control_results = {}
|
|
207
|
+
for item in control_items:
|
|
208
|
+
control_id = item["Control IDs"]
|
|
209
|
+
if control_id not in control_results:
|
|
210
|
+
control_results[control_id] = {"pass": 0, "fail": 0, "total": 0}
|
|
211
|
+
|
|
212
|
+
control_results[control_id]["total"] += 1
|
|
213
|
+
if item["Result"].lower() == "pass":
|
|
214
|
+
control_results[control_id]["pass"] += 1
|
|
215
|
+
else:
|
|
216
|
+
control_results[control_id]["fail"] += 1
|
|
217
|
+
|
|
218
|
+
# Test the aggregated results
|
|
219
|
+
ac2_results = control_results["AC-2"]
|
|
220
|
+
self.assertEqual(ac2_results["total"], 3)
|
|
221
|
+
self.assertEqual(ac2_results["pass"], 2)
|
|
222
|
+
self.assertEqual(ac2_results["fail"], 1)
|
|
223
|
+
|
|
224
|
+
# Test threshold logic (e.g., >70% pass rate = compliant)
|
|
225
|
+
pass_rate = ac2_results["pass"] / ac2_results["total"]
|
|
226
|
+
control_status = "Compliant" if pass_rate > 0.7 else "Non-Compliant"
|
|
227
|
+
self.assertEqual(control_status, "Non-Compliant") # 66.7% pass rate
|
|
228
|
+
|
|
229
|
+
def test_compliance_item_creation_and_properties(self):
|
|
230
|
+
"""Test that compliance items are created with correct properties."""
|
|
231
|
+
test_item = self.compliance_data[2] # Storage account item
|
|
232
|
+
|
|
233
|
+
# Simulate compliance item creation
|
|
234
|
+
compliance_item = {
|
|
235
|
+
"control_id": "SC-8", # First control ID
|
|
236
|
+
"status": "Non-Compliant" if test_item["Result"] == "Fail" else "Compliant",
|
|
237
|
+
"implementation_guidance": test_item["Details"],
|
|
238
|
+
"assessment_date": datetime.now().isoformat(),
|
|
239
|
+
"source": "Wiz Policy Compliance",
|
|
240
|
+
"evidence": f"Policy: {test_item['Policy Name']}, Resource: {test_item['Resource Name']}",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
self.assertEqual(compliance_item["control_id"], "SC-8")
|
|
244
|
+
self.assertEqual(compliance_item["status"], "Non-Compliant")
|
|
245
|
+
self.assertIn("secure transfer", compliance_item["implementation_guidance"].lower())
|
|
246
|
+
self.assertIn("Wiz Policy", compliance_item["source"])
|
|
247
|
+
|
|
248
|
+
def test_report_reuse_functionality(self):
|
|
249
|
+
"""Test that report files can be reused and cached."""
|
|
250
|
+
# Simulate report file caching logic
|
|
251
|
+
report_cache = {}
|
|
252
|
+
report_path = "/tmp/wiz_compliance_report.csv"
|
|
253
|
+
cache_key = f"wiz_report_{hash(report_path)}"
|
|
254
|
+
|
|
255
|
+
# First access - not in cache
|
|
256
|
+
self.assertNotIn(cache_key, report_cache)
|
|
257
|
+
|
|
258
|
+
# Simulate adding to cache
|
|
259
|
+
report_cache[cache_key] = {"path": report_path, "last_modified": datetime.now(), "data": self.compliance_data}
|
|
260
|
+
|
|
261
|
+
# Second access - should be in cache
|
|
262
|
+
self.assertIn(cache_key, report_cache)
|
|
263
|
+
cached_data = report_cache[cache_key]["data"]
|
|
264
|
+
self.assertEqual(len(cached_data), 3)
|
|
265
|
+
self.assertEqual(cached_data[0]["Resource Name"], "vm-production-001")
|
|
266
|
+
|
|
267
|
+
def test_multiple_control_ids_extraction(self):
|
|
268
|
+
"""Test handling of compliance items with multiple control IDs."""
|
|
269
|
+
multi_control_item = self.compliance_data[2] # Has SC-8,SC-23,IA-7
|
|
270
|
+
|
|
271
|
+
control_ids = [cid.strip() for cid in multi_control_item["Control IDs"].split(",")]
|
|
272
|
+
|
|
273
|
+
# Verify all control IDs are extracted
|
|
274
|
+
expected_controls = ["SC-8", "SC-23", "IA-7"]
|
|
275
|
+
self.assertEqual(len(control_ids), 3)
|
|
276
|
+
|
|
277
|
+
for expected_control in expected_controls:
|
|
278
|
+
self.assertIn(expected_control, control_ids)
|
|
279
|
+
|
|
280
|
+
# Simulate creating separate compliance items for each control
|
|
281
|
+
compliance_items = []
|
|
282
|
+
for control_id in control_ids:
|
|
283
|
+
item = {
|
|
284
|
+
"control_id": control_id,
|
|
285
|
+
"resource_name": multi_control_item["Resource Name"],
|
|
286
|
+
"status": "Non-Compliant",
|
|
287
|
+
"policy_name": multi_control_item["Policy Name"],
|
|
288
|
+
}
|
|
289
|
+
compliance_items.append(item)
|
|
290
|
+
|
|
291
|
+
self.assertEqual(len(compliance_items), 3)
|
|
292
|
+
self.assertEqual(compliance_items[0]["control_id"], "SC-8")
|
|
293
|
+
self.assertEqual(compliance_items[1]["control_id"], "SC-23")
|
|
294
|
+
self.assertEqual(compliance_items[2]["control_id"], "IA-7")
|
|
295
|
+
|
|
296
|
+
def test_csv_file_parsing_edge_cases(self):
|
|
297
|
+
"""Test CSV file parsing with various edge cases."""
|
|
298
|
+
# Test data with edge cases
|
|
299
|
+
edge_case_data = [
|
|
300
|
+
{
|
|
301
|
+
"Resource Name": "vm-with,comma",
|
|
302
|
+
"Control IDs": "AC-1,AC-2, AC-3 ", # Extra spaces
|
|
303
|
+
"Details": 'Description with "quotes" and newlines\nSecond line',
|
|
304
|
+
"Result": "PASS", # Different case
|
|
305
|
+
"Policy Name": "", # Empty field
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as temp_file:
|
|
310
|
+
writer = csv.DictWriter(temp_file, fieldnames=edge_case_data[0].keys())
|
|
311
|
+
writer.writeheader()
|
|
312
|
+
writer.writerows(edge_case_data)
|
|
313
|
+
temp_file.flush()
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
with open(temp_file.name, "r") as f:
|
|
317
|
+
reader = csv.DictReader(f)
|
|
318
|
+
parsed_data = list(reader)
|
|
319
|
+
|
|
320
|
+
self.assertEqual(len(parsed_data), 1)
|
|
321
|
+
item = parsed_data[0]
|
|
322
|
+
|
|
323
|
+
# Test comma in resource name is preserved
|
|
324
|
+
self.assertEqual(item["Resource Name"], "vm-with,comma")
|
|
325
|
+
|
|
326
|
+
# Test control ID parsing with spaces
|
|
327
|
+
control_ids = [cid.strip() for cid in item["Control IDs"].split(",")]
|
|
328
|
+
self.assertEqual(control_ids, ["AC-1", "AC-2", "AC-3"])
|
|
329
|
+
|
|
330
|
+
# Test case insensitive result parsing
|
|
331
|
+
self.assertEqual(item["Result"].lower(), "pass")
|
|
332
|
+
|
|
333
|
+
# Test empty policy name
|
|
334
|
+
self.assertEqual(item["Policy Name"], "")
|
|
335
|
+
|
|
336
|
+
finally:
|
|
337
|
+
os.unlink(temp_file.name)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
unittest.main()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test Wiz control ID normalization after fix."""
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.INFO)
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WizComplianceReportItemTest:
|
|
12
|
+
"""Test version of WizComplianceReportItem with normalization methods."""
|
|
13
|
+
|
|
14
|
+
def _normalize_base_control(self, base_control: str) -> str:
|
|
15
|
+
"""Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)."""
|
|
16
|
+
if "-" in base_control:
|
|
17
|
+
prefix, number = base_control.split("-", 1)
|
|
18
|
+
try:
|
|
19
|
+
normalized_number = str(int(number))
|
|
20
|
+
return f"{prefix.upper()}-{normalized_number}"
|
|
21
|
+
except ValueError:
|
|
22
|
+
return base_control.upper()
|
|
23
|
+
else:
|
|
24
|
+
return base_control.upper()
|
|
25
|
+
|
|
26
|
+
def _format_control_id(self, base_control: str, enhancement: str) -> str:
|
|
27
|
+
"""Format control ID with optional enhancement."""
|
|
28
|
+
if enhancement:
|
|
29
|
+
# Normalize enhancement number to remove leading zeros
|
|
30
|
+
try:
|
|
31
|
+
normalized_enhancement = str(int(enhancement))
|
|
32
|
+
except ValueError:
|
|
33
|
+
normalized_enhancement = enhancement
|
|
34
|
+
return f"{base_control}({normalized_enhancement})"
|
|
35
|
+
else:
|
|
36
|
+
return base_control
|
|
37
|
+
|
|
38
|
+
def get_all_control_ids(self, compliance_check_name: str) -> list:
|
|
39
|
+
"""Extract all control IDs from compliance check name and normalize leading zeros."""
|
|
40
|
+
if not compliance_check_name:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
control_id_pattern = r"([A-Za-z]{2}-\d+)(?:\s*\(\s*(\d+)\s*\))?"
|
|
44
|
+
control_ids = []
|
|
45
|
+
|
|
46
|
+
for part in compliance_check_name.split(", "):
|
|
47
|
+
matches = re.findall(control_id_pattern, part.strip())
|
|
48
|
+
for match in matches:
|
|
49
|
+
base_control, enhancement = match
|
|
50
|
+
normalized_control = self._normalize_base_control(base_control)
|
|
51
|
+
formatted_control = self._format_control_id(normalized_control, enhancement)
|
|
52
|
+
control_ids.append(formatted_control)
|
|
53
|
+
|
|
54
|
+
return control_ids
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_normalization():
|
|
58
|
+
"""Test control ID normalization."""
|
|
59
|
+
|
|
60
|
+
test_item = WizComplianceReportItemTest()
|
|
61
|
+
|
|
62
|
+
# Test cases from actual Wiz data
|
|
63
|
+
test_cases = [
|
|
64
|
+
# Single controls
|
|
65
|
+
("AC-3 Access Enforcement", ["AC-3"]),
|
|
66
|
+
("AC-2(4) Account Management | Automated Audit Actions", ["AC-2(4)"]),
|
|
67
|
+
("SI-4(20) System Monitoring | Privileged Users", ["SI-4(20)"]),
|
|
68
|
+
# Multi-control strings
|
|
69
|
+
(
|
|
70
|
+
"AC-2(4) Account Management | Automated Audit Actions, AC-6(9) Least Privilege | Log Use of Privileged Functions, AU-12 Audit Record Generation",
|
|
71
|
+
["AC-2(4)", "AC-6(9)", "AU-12"],
|
|
72
|
+
),
|
|
73
|
+
# Edge cases with leading zeros (potential future data)
|
|
74
|
+
("AC-01 Access Control", ["AC-1"]),
|
|
75
|
+
("AC-01(04) Access Control Enhancement", ["AC-1(4)"]),
|
|
76
|
+
("SC-08(01) Transmission Security", ["SC-8(1)"]),
|
|
77
|
+
# Complex multi-control with various formats
|
|
78
|
+
("AC-01(04) Access Control, AU-3(1) Audit Content, SI-04(20) Monitoring", ["AC-1(4)", "AU-3(1)", "SI-4(20)"]),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
logger.info("Testing Wiz Control ID Normalization")
|
|
82
|
+
logger.info("=" * 60)
|
|
83
|
+
|
|
84
|
+
all_passed = True
|
|
85
|
+
|
|
86
|
+
for input_string, expected_output in test_cases:
|
|
87
|
+
result = test_item.get_all_control_ids(input_string)
|
|
88
|
+
|
|
89
|
+
logger.info(f"\nInput: {input_string}")
|
|
90
|
+
logger.info(f"Expected: {expected_output}")
|
|
91
|
+
logger.info(f"Got: {result}")
|
|
92
|
+
|
|
93
|
+
if result == expected_output:
|
|
94
|
+
logger.info("✓ PASS")
|
|
95
|
+
else:
|
|
96
|
+
logger.error("✗ FAIL")
|
|
97
|
+
all_passed = False
|
|
98
|
+
|
|
99
|
+
logger.info("\n" + "=" * 60)
|
|
100
|
+
if all_passed:
|
|
101
|
+
logger.info("All tests PASSED ✓")
|
|
102
|
+
else:
|
|
103
|
+
logger.error("Some tests FAILED ✗")
|
|
104
|
+
|
|
105
|
+
# Test specific normalization cases
|
|
106
|
+
logger.info("\n" + "=" * 60)
|
|
107
|
+
logger.info("Testing specific normalization methods:")
|
|
108
|
+
|
|
109
|
+
# Test base control normalization
|
|
110
|
+
base_tests = [
|
|
111
|
+
("AC-01", "AC-1"),
|
|
112
|
+
("AC-1", "AC-1"),
|
|
113
|
+
("AU-003", "AU-3"),
|
|
114
|
+
("sc-7", "SC-7"), # lowercase
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
for input_val, expected in base_tests:
|
|
118
|
+
result = test_item._normalize_base_control(input_val)
|
|
119
|
+
status = "✓" if result == expected else "✗"
|
|
120
|
+
logger.info(f" _normalize_base_control('{input_val}') -> '{result}' (expected: '{expected}') {status}")
|
|
121
|
+
|
|
122
|
+
# Test format control ID
|
|
123
|
+
format_tests = [
|
|
124
|
+
(("AC-1", "04"), "AC-1(4)"),
|
|
125
|
+
(("AC-1", "4"), "AC-1(4)"),
|
|
126
|
+
(("AC-1", ""), "AC-1"),
|
|
127
|
+
(("SC-7", "001"), "SC-7(1)"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
logger.info("\nTesting _format_control_id:")
|
|
131
|
+
for (base, enhancement), expected in format_tests:
|
|
132
|
+
result = test_item._format_control_id(base, enhancement)
|
|
133
|
+
status = "✓" if result == expected else "✗"
|
|
134
|
+
logger.info(f" _format_control_id('{base}', '{enhancement}') -> '{result}' (expected: '{expected}') {status}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
test_normalization()
|