regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.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 +19 -4
- regscale/core/app/internal/evidence.py +419 -2
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- 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/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +64 -79
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +39 -99
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +60 -41
- regscale/integrations/control_matcher.py +377 -0
- regscale/integrations/due_date_handler.py +14 -8
- 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/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +277 -153
- regscale/models/integration_models/cisa_kev_data.json +282 -9
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +47 -22
- regscale/models/regscale_models/issue.py +256 -95
- 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.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
- 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 +2204 -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 +1365 -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/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -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_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -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/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +1397 -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/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive unit tests for policy_assessment.py module.
|
|
3
|
+
|
|
4
|
+
This test suite covers:
|
|
5
|
+
- WizDataCache: caching, TTL validation, file operations
|
|
6
|
+
- WizApiClient: async fetching, requests-based fetching, pagination, error handling
|
|
7
|
+
- PolicyAssessmentFetcher: main fetching logic, filtering, data cleaning
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from unittest.mock import MagicMock, Mock, patch, call
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
import requests
|
|
20
|
+
|
|
21
|
+
from regscale.integrations.commercial.wizv2.fetchers.policy_assessment import (
|
|
22
|
+
WizDataCache,
|
|
23
|
+
WizApiClient,
|
|
24
|
+
PolicyAssessmentFetcher,
|
|
25
|
+
)
|
|
26
|
+
from regscale.integrations.commercial.wizv2.core.constants import WizVulnerabilityType, WIZ_POLICY_QUERY
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("regscale")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestWizDataCache(unittest.TestCase):
|
|
32
|
+
"""Test cases for WizDataCache class."""
|
|
33
|
+
|
|
34
|
+
def setUp(self):
|
|
35
|
+
"""Set up test fixtures."""
|
|
36
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
37
|
+
self.wiz_project_id = "test-project-123"
|
|
38
|
+
self.framework_id = "wf-id-4"
|
|
39
|
+
|
|
40
|
+
def tearDown(self):
|
|
41
|
+
"""Clean up test artifacts."""
|
|
42
|
+
import shutil
|
|
43
|
+
|
|
44
|
+
if os.path.exists(self.temp_dir):
|
|
45
|
+
shutil.rmtree(self.temp_dir)
|
|
46
|
+
|
|
47
|
+
def test_init_default_values(self):
|
|
48
|
+
"""Test WizDataCache initialization with default values."""
|
|
49
|
+
cache = WizDataCache(self.temp_dir)
|
|
50
|
+
self.assertEqual(cache.cache_dir, self.temp_dir)
|
|
51
|
+
self.assertEqual(cache.cache_duration_minutes, 0)
|
|
52
|
+
self.assertFalse(cache.force_refresh)
|
|
53
|
+
|
|
54
|
+
def test_init_custom_values(self):
|
|
55
|
+
"""Test WizDataCache initialization with custom values."""
|
|
56
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
57
|
+
self.assertEqual(cache.cache_dir, self.temp_dir)
|
|
58
|
+
self.assertEqual(cache.cache_duration_minutes, 60)
|
|
59
|
+
self.assertFalse(cache.force_refresh)
|
|
60
|
+
|
|
61
|
+
def test_get_cache_file_path(self):
|
|
62
|
+
"""Test cache file path generation."""
|
|
63
|
+
cache = WizDataCache(self.temp_dir)
|
|
64
|
+
cache_path = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
65
|
+
|
|
66
|
+
expected_filename = f"policy_assessments_{self.wiz_project_id}_{self.framework_id}.json"
|
|
67
|
+
expected_path = os.path.join(self.temp_dir, expected_filename)
|
|
68
|
+
|
|
69
|
+
self.assertEqual(cache_path, expected_path)
|
|
70
|
+
self.assertTrue(os.path.exists(self.temp_dir))
|
|
71
|
+
|
|
72
|
+
def test_is_cache_valid_disabled(self):
|
|
73
|
+
"""Test cache validation when caching is disabled (duration = 0)."""
|
|
74
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=0)
|
|
75
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
76
|
+
|
|
77
|
+
self.assertFalse(cache.is_cache_valid(cache_file))
|
|
78
|
+
|
|
79
|
+
def test_is_cache_valid_force_refresh(self):
|
|
80
|
+
"""Test cache validation when force_refresh is enabled."""
|
|
81
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
82
|
+
cache.force_refresh = True
|
|
83
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
84
|
+
|
|
85
|
+
self.assertFalse(cache.is_cache_valid(cache_file))
|
|
86
|
+
|
|
87
|
+
def test_is_cache_valid_file_not_exists(self):
|
|
88
|
+
"""Test cache validation when cache file does not exist."""
|
|
89
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
90
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
91
|
+
|
|
92
|
+
self.assertFalse(cache.is_cache_valid(cache_file))
|
|
93
|
+
|
|
94
|
+
def test_is_cache_valid_fresh_file(self):
|
|
95
|
+
"""Test cache validation with a fresh cache file."""
|
|
96
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
97
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
98
|
+
|
|
99
|
+
# Create a fresh cache file
|
|
100
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
101
|
+
json.dump({"timestamp": datetime.now().isoformat(), "nodes": []}, f)
|
|
102
|
+
|
|
103
|
+
self.assertTrue(cache.is_cache_valid(cache_file))
|
|
104
|
+
|
|
105
|
+
def test_is_cache_valid_expired_file(self):
|
|
106
|
+
"""Test cache validation with an expired cache file."""
|
|
107
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=1)
|
|
108
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
109
|
+
|
|
110
|
+
# Create cache file and modify timestamp to be old
|
|
111
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
112
|
+
json.dump({"timestamp": datetime.now().isoformat(), "nodes": []}, f)
|
|
113
|
+
|
|
114
|
+
# Modify file time to be 2 minutes old
|
|
115
|
+
old_time = (datetime.now() - timedelta(minutes=2)).timestamp()
|
|
116
|
+
os.utime(cache_file, (old_time, old_time))
|
|
117
|
+
|
|
118
|
+
self.assertFalse(cache.is_cache_valid(cache_file))
|
|
119
|
+
|
|
120
|
+
def test_is_cache_valid_exception_handling(self):
|
|
121
|
+
"""Test cache validation handles exceptions gracefully."""
|
|
122
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
123
|
+
|
|
124
|
+
# Test with invalid path that will cause exception
|
|
125
|
+
with patch("os.path.exists", return_value=True):
|
|
126
|
+
with patch("os.path.getmtime", side_effect=OSError("Permission denied")):
|
|
127
|
+
self.assertFalse(cache.is_cache_valid("/invalid/path/cache.json"))
|
|
128
|
+
|
|
129
|
+
def test_load_from_cache_success_nodes_key(self):
|
|
130
|
+
"""Test loading cache with 'nodes' key."""
|
|
131
|
+
cache = WizDataCache(self.temp_dir)
|
|
132
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
133
|
+
|
|
134
|
+
test_nodes = [{"id": "1", "name": "test"}]
|
|
135
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
136
|
+
json.dump({"timestamp": datetime.now().isoformat(), "nodes": test_nodes}, f)
|
|
137
|
+
|
|
138
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
139
|
+
self.assertEqual(loaded_nodes, test_nodes)
|
|
140
|
+
|
|
141
|
+
def test_load_from_cache_success_assessments_key(self):
|
|
142
|
+
"""Test loading cache with 'assessments' key."""
|
|
143
|
+
cache = WizDataCache(self.temp_dir)
|
|
144
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
145
|
+
|
|
146
|
+
test_nodes = [{"id": "2", "name": "assessment"}]
|
|
147
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
148
|
+
json.dump({"timestamp": datetime.now().isoformat(), "assessments": test_nodes}, f)
|
|
149
|
+
|
|
150
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
151
|
+
self.assertEqual(loaded_nodes, test_nodes)
|
|
152
|
+
|
|
153
|
+
def test_load_from_cache_empty_nodes(self):
|
|
154
|
+
"""Test loading cache with empty nodes."""
|
|
155
|
+
cache = WizDataCache(self.temp_dir)
|
|
156
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
157
|
+
|
|
158
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
159
|
+
json.dump({"timestamp": datetime.now().isoformat(), "nodes": []}, f)
|
|
160
|
+
|
|
161
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
162
|
+
self.assertEqual(loaded_nodes, [])
|
|
163
|
+
|
|
164
|
+
def test_load_from_cache_no_nodes_key(self):
|
|
165
|
+
"""Test loading cache without nodes or assessments key."""
|
|
166
|
+
cache = WizDataCache(self.temp_dir)
|
|
167
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
168
|
+
|
|
169
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
170
|
+
json.dump({"timestamp": datetime.now().isoformat()}, f)
|
|
171
|
+
|
|
172
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
173
|
+
self.assertEqual(loaded_nodes, [])
|
|
174
|
+
|
|
175
|
+
def test_load_from_cache_invalid_json(self):
|
|
176
|
+
"""Test loading cache with invalid JSON."""
|
|
177
|
+
cache = WizDataCache(self.temp_dir)
|
|
178
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
179
|
+
|
|
180
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
181
|
+
f.write("invalid json content")
|
|
182
|
+
|
|
183
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
184
|
+
self.assertIsNone(loaded_nodes)
|
|
185
|
+
|
|
186
|
+
def test_load_from_cache_file_not_exists(self):
|
|
187
|
+
"""Test loading cache when file does not exist."""
|
|
188
|
+
cache = WizDataCache(self.temp_dir)
|
|
189
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
190
|
+
|
|
191
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
192
|
+
self.assertIsNone(loaded_nodes)
|
|
193
|
+
|
|
194
|
+
def test_load_from_cache_non_list_nodes(self):
|
|
195
|
+
"""Test loading cache when nodes is not a list."""
|
|
196
|
+
cache = WizDataCache(self.temp_dir)
|
|
197
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
198
|
+
|
|
199
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
200
|
+
json.dump({"timestamp": datetime.now().isoformat(), "nodes": "not a list"}, f)
|
|
201
|
+
|
|
202
|
+
loaded_nodes = cache.load_from_cache(cache_file)
|
|
203
|
+
self.assertIsNone(loaded_nodes)
|
|
204
|
+
|
|
205
|
+
def test_save_to_cache_success(self):
|
|
206
|
+
"""Test saving data to cache."""
|
|
207
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
208
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
209
|
+
|
|
210
|
+
test_nodes = [{"id": "3", "name": "save_test"}]
|
|
211
|
+
cache.save_to_cache(cache_file, test_nodes, self.wiz_project_id, self.framework_id)
|
|
212
|
+
|
|
213
|
+
self.assertTrue(os.path.exists(cache_file))
|
|
214
|
+
|
|
215
|
+
with open(cache_file, "r", encoding="utf-8") as f:
|
|
216
|
+
saved_data = json.load(f)
|
|
217
|
+
|
|
218
|
+
self.assertIn("timestamp", saved_data)
|
|
219
|
+
self.assertEqual(saved_data["wiz_project_id"], self.wiz_project_id)
|
|
220
|
+
self.assertEqual(saved_data["framework_id"], self.framework_id)
|
|
221
|
+
self.assertEqual(saved_data["nodes"], test_nodes)
|
|
222
|
+
|
|
223
|
+
def test_save_to_cache_disabled(self):
|
|
224
|
+
"""Test saving to cache when caching is disabled."""
|
|
225
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=0)
|
|
226
|
+
cache_file = cache.get_cache_file_path(self.wiz_project_id, self.framework_id)
|
|
227
|
+
|
|
228
|
+
test_nodes = [{"id": "4", "name": "no_save_test"}]
|
|
229
|
+
cache.save_to_cache(cache_file, test_nodes, self.wiz_project_id, self.framework_id)
|
|
230
|
+
|
|
231
|
+
self.assertFalse(os.path.exists(cache_file))
|
|
232
|
+
|
|
233
|
+
def test_save_to_cache_exception_handling(self):
|
|
234
|
+
"""Test save to cache handles exceptions gracefully."""
|
|
235
|
+
cache = WizDataCache(self.temp_dir, cache_duration_minutes=60)
|
|
236
|
+
|
|
237
|
+
# Use invalid path to trigger exception
|
|
238
|
+
with patch("builtins.open", side_effect=PermissionError("No permission")):
|
|
239
|
+
# Should not raise exception
|
|
240
|
+
cache.save_to_cache("/invalid/path/cache.json", [], self.wiz_project_id, self.framework_id)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestWizApiClient(unittest.TestCase):
|
|
244
|
+
"""Test cases for WizApiClient class."""
|
|
245
|
+
|
|
246
|
+
def setUp(self):
|
|
247
|
+
"""Set up test fixtures."""
|
|
248
|
+
self.endpoint = "https://api.wiz.io/graphql"
|
|
249
|
+
self.access_token = "test_token_12345"
|
|
250
|
+
self.client = WizApiClient(self.endpoint, self.access_token)
|
|
251
|
+
|
|
252
|
+
def test_init(self):
|
|
253
|
+
"""Test WizApiClient initialization."""
|
|
254
|
+
self.assertEqual(self.client.endpoint, self.endpoint)
|
|
255
|
+
self.assertEqual(self.client.access_token, self.access_token)
|
|
256
|
+
|
|
257
|
+
def test_get_headers(self):
|
|
258
|
+
"""Test header generation."""
|
|
259
|
+
headers = self.client.get_headers()
|
|
260
|
+
|
|
261
|
+
self.assertIn("Authorization", headers)
|
|
262
|
+
self.assertIn("Content-Type", headers)
|
|
263
|
+
self.assertEqual(headers["Authorization"], f"Bearer {self.access_token}")
|
|
264
|
+
self.assertEqual(headers["Content-Type"], "application/json")
|
|
265
|
+
|
|
266
|
+
@patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
|
|
267
|
+
@patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
|
|
268
|
+
def test_fetch_policy_assessments_async_success(self, mock_progress, mock_run_async):
|
|
269
|
+
"""Test async policy assessment fetching - success case."""
|
|
270
|
+
mock_progress.__enter__ = Mock(return_value=mock_progress)
|
|
271
|
+
mock_progress.__exit__ = Mock(return_value=False)
|
|
272
|
+
mock_progress.add_task = Mock(return_value="task_id")
|
|
273
|
+
mock_progress.update = Mock()
|
|
274
|
+
|
|
275
|
+
test_nodes = [{"id": "node1"}, {"id": "node2"}]
|
|
276
|
+
mock_run_async.return_value = [(WizVulnerabilityType.CONFIGURATION.value, test_nodes, None)]
|
|
277
|
+
|
|
278
|
+
result = self.client.fetch_policy_assessments_async()
|
|
279
|
+
|
|
280
|
+
self.assertEqual(result, test_nodes)
|
|
281
|
+
mock_run_async.assert_called_once()
|
|
282
|
+
|
|
283
|
+
call_args = mock_run_async.call_args
|
|
284
|
+
self.assertEqual(call_args.kwargs["endpoint"], self.endpoint)
|
|
285
|
+
self.assertIn("Authorization", call_args.kwargs["headers"])
|
|
286
|
+
|
|
287
|
+
@patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
|
|
288
|
+
@patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
|
|
289
|
+
def test_fetch_policy_assessments_async_empty_results(self, mock_progress, mock_run_async):
|
|
290
|
+
"""Test async policy assessment fetching - empty results."""
|
|
291
|
+
mock_progress.__enter__ = Mock(return_value=mock_progress)
|
|
292
|
+
mock_progress.__exit__ = Mock(return_value=False)
|
|
293
|
+
mock_progress.add_task = Mock(return_value="task_id")
|
|
294
|
+
mock_progress.update = Mock()
|
|
295
|
+
|
|
296
|
+
mock_run_async.return_value = []
|
|
297
|
+
|
|
298
|
+
result = self.client.fetch_policy_assessments_async()
|
|
299
|
+
|
|
300
|
+
self.assertEqual(result, [])
|
|
301
|
+
|
|
302
|
+
@patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
|
|
303
|
+
@patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
|
|
304
|
+
def test_fetch_policy_assessments_async_with_error(self, mock_progress, mock_run_async):
|
|
305
|
+
"""Test async policy assessment fetching - with error."""
|
|
306
|
+
mock_progress.__enter__ = Mock(return_value=mock_progress)
|
|
307
|
+
mock_progress.__exit__ = Mock(return_value=False)
|
|
308
|
+
mock_progress.add_task = Mock(return_value="task_id")
|
|
309
|
+
mock_progress.update = Mock()
|
|
310
|
+
|
|
311
|
+
mock_run_async.return_value = [(WizVulnerabilityType.CONFIGURATION.value, [], Exception("API Error"))]
|
|
312
|
+
|
|
313
|
+
result = self.client.fetch_policy_assessments_async()
|
|
314
|
+
|
|
315
|
+
self.assertEqual(result, [])
|
|
316
|
+
|
|
317
|
+
@patch("regscale.integrations.commercial.wizv2.fetchers.policy_assessment.run_async_queries")
|
|
318
|
+
@patch("regscale.integrations.commercial.wizv2.utils.compliance_job_progress")
|
|
319
|
+
def test_fetch_policy_assessments_async_exception(self, mock_progress, mock_run_async):
|
|
320
|
+
"""Test async policy assessment fetching - exception raised."""
|
|
321
|
+
mock_progress.__enter__ = Mock(return_value=mock_progress)
|
|
322
|
+
mock_progress.__exit__ = Mock(return_value=False)
|
|
323
|
+
mock_progress.add_task = Mock(return_value="task_id")
|
|
324
|
+
mock_progress.update = Mock()
|
|
325
|
+
|
|
326
|
+
mock_run_async.side_effect = Exception("Async client error")
|
|
327
|
+
|
|
328
|
+
with self.assertRaises(Exception) as context:
|
|
329
|
+
self.client.fetch_policy_assessments_async()
|
|
330
|
+
|
|
331
|
+
self.assertIn("Async client error", str(context.exception))
|
|
332
|
+
|
|
333
|
+
def test_create_requests_session(self):
|
|
334
|
+
"""Test requests session creation with retry logic."""
|
|
335
|
+
session = self.client._create_requests_session()
|
|
336
|
+
|
|
337
|
+
self.assertIsInstance(session, requests.Session)
|
|
338
|
+
# Verify retry adapter is configured
|
|
339
|
+
adapter = session.get_adapter("https://")
|
|
340
|
+
self.assertIsNotNone(adapter)
|
|
341
|
+
|
|
342
|
+
@patch.object(WizApiClient, "_execute_paginated_query")
|
|
343
|
+
def test_fetch_policy_assessments_requests_success_first_variant(self, mock_execute):
|
|
344
|
+
"""Test requests-based fetching - success with first filter variant."""
|
|
345
|
+
test_nodes = [{"id": "req1"}, {"id": "req2"}]
|
|
346
|
+
mock_execute.return_value = test_nodes
|
|
347
|
+
|
|
348
|
+
base_variables = {"first": 100}
|
|
349
|
+
filter_variants = [{"project": ["proj1"]}, {"projectId": ["proj1"]}, None]
|
|
350
|
+
|
|
351
|
+
result = self.client.fetch_policy_assessments_requests(base_variables, filter_variants)
|
|
352
|
+
|
|
353
|
+
self.assertEqual(result, test_nodes)
|
|
354
|
+
mock_execute.assert_called_once()
|
|
355
|
+
|
|
356
|
+
@patch.object(WizApiClient, "_execute_paginated_query")
|
|
357
|
+
def test_fetch_policy_assessments_requests_fallback_to_second_variant(self, mock_execute):
|
|
358
|
+
"""Test requests-based fetching - fallback to second filter variant."""
|
|
359
|
+
test_nodes = [{"id": "req3"}]
|
|
360
|
+
|
|
361
|
+
def side_effect(*args, **kwargs):
|
|
362
|
+
if mock_execute.call_count == 1:
|
|
363
|
+
raise Exception("First variant failed")
|
|
364
|
+
return test_nodes
|
|
365
|
+
|
|
366
|
+
mock_execute.side_effect = side_effect
|
|
367
|
+
|
|
368
|
+
base_variables = {"first": 100}
|
|
369
|
+
filter_variants = [{"project": ["proj1"]}, {"projectId": ["proj1"]}, None]
|
|
370
|
+
|
|
371
|
+
result = self.client.fetch_policy_assessments_requests(base_variables, filter_variants)
|
|
372
|
+
|
|
373
|
+
self.assertEqual(result, test_nodes)
|
|
374
|
+
self.assertEqual(mock_execute.call_count, 2)
|
|
375
|
+
|
|
376
|
+
@patch.object(WizApiClient, "_execute_paginated_query")
|
|
377
|
+
def test_fetch_policy_assessments_requests_all_variants_fail(self, mock_execute):
|
|
378
|
+
"""Test requests-based fetching - all filter variants fail."""
|
|
379
|
+
mock_execute.side_effect = Exception("API Error")
|
|
380
|
+
|
|
381
|
+
base_variables = {"first": 100}
|
|
382
|
+
filter_variants = [{"project": ["proj1"]}, None]
|
|
383
|
+
|
|
384
|
+
with self.assertRaises(RuntimeError) as context:
|
|
385
|
+
self.client.fetch_policy_assessments_requests(base_variables, filter_variants)
|
|
386
|
+
|
|
387
|
+
self.assertIn("All filter variants failed", str(context.exception))
|
|
388
|
+
|
|
389
|
+
@patch.object(WizApiClient, "_execute_paginated_query")
|
|
390
|
+
def test_fetch_policy_assessments_requests_with_callback(self, mock_execute):
|
|
391
|
+
"""Test requests-based fetching with progress callback."""
|
|
392
|
+
test_nodes = [{"id": "req4"}]
|
|
393
|
+
mock_execute.return_value = test_nodes
|
|
394
|
+
|
|
395
|
+
mock_callback = Mock()
|
|
396
|
+
base_variables = {"first": 100}
|
|
397
|
+
filter_variants = [{"project": ["proj1"]}]
|
|
398
|
+
|
|
399
|
+
result = self.client.fetch_policy_assessments_requests(base_variables, filter_variants, mock_callback)
|
|
400
|
+
|
|
401
|
+
self.assertEqual(result, test_nodes)
|
|
402
|
+
|
|
403
|
+
@patch("requests.Session.post")
|
|
404
|
+
def test_execute_paginated_query_single_page(self, mock_post):
|
|
405
|
+
"""Test paginated query execution - single page."""
|
|
406
|
+
mock_response = Mock()
|
|
407
|
+
mock_response.status_code = 200
|
|
408
|
+
mock_response.json.return_value = {
|
|
409
|
+
"data": {
|
|
410
|
+
"policyAssessments": {
|
|
411
|
+
"nodes": [{"id": "page1_node1"}],
|
|
412
|
+
"pageInfo": {"hasNextPage": False, "endCursor": None},
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
mock_post.return_value = mock_response
|
|
417
|
+
|
|
418
|
+
session = self.client._create_requests_session()
|
|
419
|
+
variables = {"first": 100}
|
|
420
|
+
|
|
421
|
+
result = self.client._execute_paginated_query(session, variables)
|
|
422
|
+
|
|
423
|
+
self.assertEqual(len(result), 1)
|
|
424
|
+
self.assertEqual(result[0]["id"], "page1_node1")
|
|
425
|
+
mock_post.assert_called_once()
|
|
426
|
+
|
|
427
|
+
@patch("requests.Session.post")
|
|
428
|
+
def test_execute_paginated_query_multiple_pages(self, mock_post):
|
|
429
|
+
"""Test paginated query execution - multiple pages."""
|
|
430
|
+
# First page response
|
|
431
|
+
response1 = Mock()
|
|
432
|
+
response1.status_code = 200
|
|
433
|
+
response1.json.return_value = {
|
|
434
|
+
"data": {
|
|
435
|
+
"policyAssessments": {
|
|
436
|
+
"nodes": [{"id": "page1_node1"}],
|
|
437
|
+
"pageInfo": {"hasNextPage": True, "endCursor": "cursor1"},
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# Second page response
|
|
443
|
+
response2 = Mock()
|
|
444
|
+
response2.status_code = 200
|
|
445
|
+
response2.json.return_value = {
|
|
446
|
+
"data": {
|
|
447
|
+
"policyAssessments": {
|
|
448
|
+
"nodes": [{"id": "page2_node1"}],
|
|
449
|
+
"pageInfo": {"hasNextPage": False, "endCursor": None},
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
mock_post.side_effect = [response1, response2]
|
|
455
|
+
|
|
456
|
+
session = self.client._create_requests_session()
|
|
457
|
+
variables = {"first": 100}
|
|
458
|
+
|
|
459
|
+
result = self.client._execute_paginated_query(session, variables)
|
|
460
|
+
|
|
461
|
+
self.assertEqual(len(result), 2)
|
|
462
|
+
self.assertEqual(mock_post.call_count, 2)
|
|
463
|
+
|
|
464
|
+
@patch("requests.Session.post")
|
|
465
|
+
def test_execute_paginated_query_with_progress_callback(self, mock_post):
|
|
466
|
+
"""Test paginated query execution with progress callback."""
|
|
467
|
+
mock_response = Mock()
|
|
468
|
+
mock_response.status_code = 200
|
|
469
|
+
mock_response.json.return_value = {
|
|
470
|
+
"data": {
|
|
471
|
+
"policyAssessments": {
|
|
472
|
+
"nodes": [{"id": "callback_node"}],
|
|
473
|
+
"pageInfo": {"hasNextPage": False, "endCursor": None},
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
mock_post.return_value = mock_response
|
|
478
|
+
|
|
479
|
+
mock_callback = Mock()
|
|
480
|
+
session = self.client._create_requests_session()
|
|
481
|
+
variables = {"first": 100}
|
|
482
|
+
|
|
483
|
+
result = self.client._execute_paginated_query(session, variables, mock_callback)
|
|
484
|
+
|
|
485
|
+
self.assertEqual(len(result), 1)
|
|
486
|
+
mock_callback.assert_called()
|
|
487
|
+
|
|
488
|
+
@patch("requests.Session.post")
|
|
489
|
+
def test_execute_paginated_query_http_error(self, mock_post):
|
|
490
|
+
"""Test paginated query execution - HTTP error."""
|
|
491
|
+
mock_response = Mock()
|
|
492
|
+
mock_response.status_code = 500
|
|
493
|
+
mock_response.text = "Internal Server Error"
|
|
494
|
+
mock_post.return_value = mock_response
|
|
495
|
+
|
|
496
|
+
session = self.client._create_requests_session()
|
|
497
|
+
variables = {"first": 100}
|
|
498
|
+
|
|
499
|
+
with self.assertRaises(requests.HTTPError):
|
|
500
|
+
self.client._execute_paginated_query(session, variables)
|
|
501
|
+
|
|
502
|
+
@patch("requests.Session.post")
|
|
503
|
+
def test_execute_paginated_query_graphql_errors(self, mock_post):
|
|
504
|
+
"""Test paginated query execution - GraphQL errors."""
|
|
505
|
+
mock_response = Mock()
|
|
506
|
+
mock_response.status_code = 200
|
|
507
|
+
mock_response.json.return_value = {"errors": [{"message": "GraphQL error"}]}
|
|
508
|
+
mock_post.return_value = mock_response
|
|
509
|
+
|
|
510
|
+
session = self.client._create_requests_session()
|
|
511
|
+
variables = {"first": 100}
|
|
512
|
+
|
|
513
|
+
with self.assertRaises(RuntimeError) as context:
|
|
514
|
+
self.client._execute_paginated_query(session, variables)
|
|
515
|
+
|
|
516
|
+
self.assertIn("GraphQL error", str(context.exception))
|
|
517
|
+
|
|
518
|
+
@patch("requests.Session.post")
|
|
519
|
+
def test_execute_paginated_query_callback_exception(self, mock_post):
|
|
520
|
+
"""Test paginated query execution - callback raises exception (should be caught)."""
|
|
521
|
+
mock_response = Mock()
|
|
522
|
+
mock_response.status_code = 200
|
|
523
|
+
mock_response.json.return_value = {
|
|
524
|
+
"data": {
|
|
525
|
+
"policyAssessments": {
|
|
526
|
+
"nodes": [{"id": "callback_error_node"}],
|
|
527
|
+
"pageInfo": {"hasNextPage": False, "endCursor": None},
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
mock_post.return_value = mock_response
|
|
532
|
+
|
|
533
|
+
mock_callback = Mock(side_effect=Exception("Callback error"))
|
|
534
|
+
session = self.client._create_requests_session()
|
|
535
|
+
variables = {"first": 100}
|
|
536
|
+
|
|
537
|
+
# Should not raise exception despite callback error
|
|
538
|
+
result = self.client._execute_paginated_query(session, variables, mock_callback)
|
|
539
|
+
|
|
540
|
+
self.assertEqual(len(result), 1)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class TestPolicyAssessmentFetcher(unittest.TestCase):
|
|
544
|
+
"""Test cases for PolicyAssessmentFetcher class."""
|
|
545
|
+
|
|
546
|
+
def setUp(self):
|
|
547
|
+
"""Set up test fixtures."""
|
|
548
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
549
|
+
self.wiz_endpoint = "https://api.wiz.io/graphql"
|
|
550
|
+
self.access_token = "test_token"
|
|
551
|
+
self.wiz_project_id = "proj-123"
|
|
552
|
+
self.framework_id = "wf-id-4"
|
|
553
|
+
|
|
554
|
+
# Create fetcher without mocking cache
|
|
555
|
+
self.fetcher = PolicyAssessmentFetcher(
|
|
556
|
+
self.wiz_endpoint, self.access_token, self.wiz_project_id, self.framework_id, cache_duration_minutes=0
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def tearDown(self):
|
|
560
|
+
"""Clean up test artifacts."""
|
|
561
|
+
import shutil
|
|
562
|
+
|
|
563
|
+
if os.path.exists(self.temp_dir):
|
|
564
|
+
shutil.rmtree(self.temp_dir)
|
|
565
|
+
|
|
566
|
+
def test_init(self):
|
|
567
|
+
"""Test PolicyAssessmentFetcher initialization."""
|
|
568
|
+
self.assertIsNotNone(self.fetcher.api_client)
|
|
569
|
+
self.assertEqual(self.fetcher.wiz_project_id, self.wiz_project_id)
|
|
570
|
+
self.assertEqual(self.fetcher.framework_id, self.framework_id)
|
|
571
|
+
self.assertIsNotNone(self.fetcher.cache)
|
|
572
|
+
|
|
573
|
+
def test_fetch_policy_assessments_no_cache(self):
|
|
574
|
+
"""Test fetching policy assessments without cache."""
|
|
575
|
+
test_nodes = [
|
|
576
|
+
{
|
|
577
|
+
"id": "assess1",
|
|
578
|
+
"policy": {
|
|
579
|
+
"securitySubCategories": [
|
|
580
|
+
{"externalId": " AC-1 ", "category": {"framework": {"id": self.framework_id}}}
|
|
581
|
+
]
|
|
582
|
+
},
|
|
583
|
+
}
|
|
584
|
+
]
|
|
585
|
+
|
|
586
|
+
with patch.object(self.fetcher.cache, "is_cache_valid", return_value=False):
|
|
587
|
+
with patch.object(self.fetcher.cache, "get_cache_file_path", return_value="/tmp/cache.json"):
|
|
588
|
+
with patch.object(self.fetcher.cache, "save_to_cache") as mock_save:
|
|
589
|
+
with patch.object(
|
|
590
|
+
self.fetcher.api_client, "fetch_policy_assessments_async", return_value=test_nodes
|
|
591
|
+
):
|
|
592
|
+
result = self.fetcher.fetch_policy_assessments()
|
|
593
|
+
|
|
594
|
+
self.assertEqual(len(result), 1)
|
|
595
|
+
mock_save.assert_called_once()
|
|
596
|
+
|
|
597
|
+
def test_fetch_policy_assessments_with_valid_cache(self):
|
|
598
|
+
"""Test fetching policy assessments with valid cache."""
|
|
599
|
+
cached_nodes = [{"id": "cached1"}]
|
|
600
|
+
|
|
601
|
+
with patch.object(self.fetcher.cache, "is_cache_valid", return_value=True):
|
|
602
|
+
with patch.object(self.fetcher.cache, "get_cache_file_path", return_value="/tmp/cache.json"):
|
|
603
|
+
with patch.object(self.fetcher.cache, "load_from_cache", return_value=cached_nodes) as mock_load:
|
|
604
|
+
result = self.fetcher.fetch_policy_assessments()
|
|
605
|
+
|
|
606
|
+
self.assertEqual(result, cached_nodes)
|
|
607
|
+
mock_load.assert_called_once()
|
|
608
|
+
|
|
609
|
+
def test_fetch_policy_assessments_async_fallback_to_requests(self):
|
|
610
|
+
"""Test fallback to requests when async fails."""
|
|
611
|
+
test_nodes = [{"id": "fallback1"}]
|
|
612
|
+
|
|
613
|
+
with patch.object(self.fetcher.cache, "is_cache_valid", return_value=False):
|
|
614
|
+
with patch.object(self.fetcher.cache, "get_cache_file_path", return_value="/tmp/cache.json"):
|
|
615
|
+
with patch.object(self.fetcher.cache, "save_to_cache"):
|
|
616
|
+
with patch.object(
|
|
617
|
+
self.fetcher.api_client, "fetch_policy_assessments_async", side_effect=Exception("Async failed")
|
|
618
|
+
) as mock_async:
|
|
619
|
+
with patch.object(
|
|
620
|
+
self.fetcher.api_client, "fetch_policy_assessments_requests", return_value=test_nodes
|
|
621
|
+
) as mock_requests:
|
|
622
|
+
result = self.fetcher.fetch_policy_assessments()
|
|
623
|
+
|
|
624
|
+
self.assertEqual(len(result), 1)
|
|
625
|
+
mock_async.assert_called_once()
|
|
626
|
+
mock_requests.assert_called_once()
|
|
627
|
+
|
|
628
|
+
def test_filter_nodes_to_framework_matching(self):
|
|
629
|
+
"""Test filtering nodes to framework - matching framework."""
|
|
630
|
+
nodes = [
|
|
631
|
+
{
|
|
632
|
+
"id": "node1",
|
|
633
|
+
"policy": {"securitySubCategories": [{"category": {"framework": {"id": self.framework_id}}}]},
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"id": "node2",
|
|
637
|
+
"policy": {"securitySubCategories": [{"category": {"framework": {"id": "other-framework"}}}]},
|
|
638
|
+
},
|
|
639
|
+
]
|
|
640
|
+
|
|
641
|
+
filtered = self.fetcher._filter_nodes_to_framework(nodes)
|
|
642
|
+
|
|
643
|
+
self.assertEqual(len(filtered), 1)
|
|
644
|
+
self.assertEqual(filtered[0]["id"], "node1")
|
|
645
|
+
|
|
646
|
+
def test_filter_nodes_to_framework_no_subcategories(self):
|
|
647
|
+
"""Test filtering nodes to framework - no subcategories (should include)."""
|
|
648
|
+
nodes = [{"id": "node1", "policy": {"securitySubCategories": []}}]
|
|
649
|
+
|
|
650
|
+
filtered = self.fetcher._filter_nodes_to_framework(nodes)
|
|
651
|
+
|
|
652
|
+
self.assertEqual(len(filtered), 1)
|
|
653
|
+
|
|
654
|
+
def test_filter_nodes_to_framework_exception_handling(self):
|
|
655
|
+
"""Test filtering nodes to framework - exception handling."""
|
|
656
|
+
nodes = [{"id": "node1", "policy": None}]
|
|
657
|
+
|
|
658
|
+
filtered = self.fetcher._filter_nodes_to_framework(nodes)
|
|
659
|
+
|
|
660
|
+
# Should include node on error (defensive)
|
|
661
|
+
self.assertEqual(len(filtered), 1)
|
|
662
|
+
|
|
663
|
+
def test_clean_node_data_trim_whitespace(self):
|
|
664
|
+
"""Test cleaning node data - trim whitespace from externalId."""
|
|
665
|
+
nodes = [
|
|
666
|
+
{
|
|
667
|
+
"id": "node1",
|
|
668
|
+
"policy": {
|
|
669
|
+
"securitySubCategories": [
|
|
670
|
+
{"externalId": " AC-1 ", "category": {"framework": {"id": "fw1"}}},
|
|
671
|
+
{"externalId": "AC-2", "category": {"framework": {"id": "fw1"}}},
|
|
672
|
+
]
|
|
673
|
+
},
|
|
674
|
+
}
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
cleaned = self.fetcher._clean_node_data(nodes)
|
|
678
|
+
|
|
679
|
+
self.assertEqual(cleaned[0]["policy"]["securitySubCategories"][0]["externalId"], "AC-1")
|
|
680
|
+
self.assertEqual(cleaned[0]["policy"]["securitySubCategories"][1]["externalId"], "AC-2")
|
|
681
|
+
|
|
682
|
+
def test_clean_node_data_no_policy(self):
|
|
683
|
+
"""Test cleaning node data - no policy field."""
|
|
684
|
+
nodes = [{"id": "node1"}]
|
|
685
|
+
|
|
686
|
+
cleaned = self.fetcher._clean_node_data(nodes)
|
|
687
|
+
|
|
688
|
+
self.assertEqual(len(cleaned), 1)
|
|
689
|
+
self.assertEqual(cleaned[0]["id"], "node1")
|
|
690
|
+
|
|
691
|
+
def test_clean_node_data_exception_handling(self):
|
|
692
|
+
"""Test cleaning node data - exception handling."""
|
|
693
|
+
nodes = [{"id": "node1", "policy": {"securitySubCategories": "invalid"}}]
|
|
694
|
+
|
|
695
|
+
cleaned = self.fetcher._clean_node_data(nodes)
|
|
696
|
+
|
|
697
|
+
# Should include original node on error
|
|
698
|
+
self.assertEqual(len(cleaned), 1)
|
|
699
|
+
|
|
700
|
+
def test_clean_single_node(self):
|
|
701
|
+
"""Test cleaning a single node."""
|
|
702
|
+
node = {
|
|
703
|
+
"id": "node1",
|
|
704
|
+
"policy": {"securitySubCategories": [{"externalId": " AC-1 "}]},
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
cleaned = self.fetcher._clean_single_node(node)
|
|
708
|
+
|
|
709
|
+
self.assertEqual(cleaned["policy"]["securitySubCategories"][0]["externalId"], "AC-1")
|
|
710
|
+
|
|
711
|
+
def test_should_clean_policy_true(self):
|
|
712
|
+
"""Test should_clean_policy returns True."""
|
|
713
|
+
policy = {"securitySubCategories": []}
|
|
714
|
+
|
|
715
|
+
result = self.fetcher._should_clean_policy(policy)
|
|
716
|
+
|
|
717
|
+
self.assertTrue(result)
|
|
718
|
+
|
|
719
|
+
def test_should_clean_policy_false_no_policy(self):
|
|
720
|
+
"""Test should_clean_policy returns False - no policy."""
|
|
721
|
+
result = self.fetcher._should_clean_policy(None)
|
|
722
|
+
|
|
723
|
+
self.assertFalse(result)
|
|
724
|
+
|
|
725
|
+
def test_should_clean_policy_false_no_subcategories(self):
|
|
726
|
+
"""Test should_clean_policy returns False - no subcategories key."""
|
|
727
|
+
policy = {"other_key": "value"}
|
|
728
|
+
|
|
729
|
+
result = self.fetcher._should_clean_policy(policy)
|
|
730
|
+
|
|
731
|
+
self.assertFalse(result)
|
|
732
|
+
|
|
733
|
+
def test_clean_policy_subcategories(self):
|
|
734
|
+
"""Test cleaning policy subcategories."""
|
|
735
|
+
policy = {
|
|
736
|
+
"id": "policy1",
|
|
737
|
+
"securitySubCategories": [
|
|
738
|
+
{"externalId": " AC-1 "},
|
|
739
|
+
{"externalId": " AC-2 "},
|
|
740
|
+
],
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
cleaned = self.fetcher._clean_policy_subcategories(policy)
|
|
744
|
+
|
|
745
|
+
self.assertEqual(cleaned["securitySubCategories"][0]["externalId"], "AC-1")
|
|
746
|
+
self.assertEqual(cleaned["securitySubCategories"][1]["externalId"], "AC-2")
|
|
747
|
+
self.assertEqual(cleaned["id"], "policy1")
|
|
748
|
+
|
|
749
|
+
def test_clean_subcategory(self):
|
|
750
|
+
"""Test cleaning a single subcategory."""
|
|
751
|
+
subcat = {"externalId": " AC-1 ", "title": "Access Control"}
|
|
752
|
+
|
|
753
|
+
cleaned = self.fetcher._clean_subcategory(subcat)
|
|
754
|
+
|
|
755
|
+
self.assertEqual(cleaned["externalId"], "AC-1")
|
|
756
|
+
self.assertEqual(cleaned["title"], "Access Control")
|
|
757
|
+
|
|
758
|
+
def test_clean_subcategory_no_external_id(self):
|
|
759
|
+
"""Test cleaning subcategory without externalId."""
|
|
760
|
+
subcat = {"title": "Access Control"}
|
|
761
|
+
|
|
762
|
+
cleaned = self.fetcher._clean_subcategory(subcat)
|
|
763
|
+
|
|
764
|
+
self.assertNotIn("externalId", cleaned)
|
|
765
|
+
|
|
766
|
+
def test_clean_subcategory_non_string_external_id(self):
|
|
767
|
+
"""Test cleaning subcategory with non-string externalId."""
|
|
768
|
+
subcat = {"externalId": 123, "title": "Test"}
|
|
769
|
+
|
|
770
|
+
cleaned = self.fetcher._clean_subcategory(subcat)
|
|
771
|
+
|
|
772
|
+
self.assertEqual(cleaned["externalId"], 123)
|
|
773
|
+
|
|
774
|
+
@patch.object(WizApiClient, "fetch_policy_assessments_async")
|
|
775
|
+
def test_fetch_with_async_client(self, mock_async):
|
|
776
|
+
"""Test _fetch_with_async_client."""
|
|
777
|
+
test_nodes = [{"id": "async1"}]
|
|
778
|
+
mock_async.return_value = test_nodes
|
|
779
|
+
|
|
780
|
+
result = self.fetcher._fetch_with_async_client()
|
|
781
|
+
|
|
782
|
+
self.assertEqual(result, test_nodes)
|
|
783
|
+
mock_async.assert_called_once()
|
|
784
|
+
|
|
785
|
+
@patch.object(WizApiClient, "fetch_policy_assessments_requests")
|
|
786
|
+
def test_fetch_with_requests(self, mock_requests):
|
|
787
|
+
"""Test _fetch_with_requests with multiple filter variants."""
|
|
788
|
+
test_nodes = [{"id": "req1"}]
|
|
789
|
+
mock_requests.return_value = test_nodes
|
|
790
|
+
|
|
791
|
+
result = self.fetcher._fetch_with_requests()
|
|
792
|
+
|
|
793
|
+
self.assertEqual(result, test_nodes)
|
|
794
|
+
mock_requests.assert_called_once()
|
|
795
|
+
|
|
796
|
+
# Verify filter variants were passed
|
|
797
|
+
call_args = mock_requests.call_args
|
|
798
|
+
filter_variants = call_args[0][1]
|
|
799
|
+
self.assertIn({"project": [self.wiz_project_id]}, filter_variants)
|
|
800
|
+
self.assertIn({"projectId": [self.wiz_project_id]}, filter_variants)
|
|
801
|
+
self.assertIn(None, filter_variants)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
if __name__ == "__main__":
|
|
805
|
+
unittest.main()
|