regscale-cli 6.26.0.0__py3-none-any.whl → 6.27.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -1
- regscale/core/app/internal/evidence.py +419 -2
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/__init__.py +0 -1
- 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/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 +44 -59
- 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 +10 -9
- 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 +40 -100
- 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 +0 -46
- regscale/integrations/control_matcher.py +22 -3
- regscale/integrations/due_date_handler.py +14 -8
- 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/scanner_integration.py +127 -57
- regscale/models/integration_models/cisa_kev_data.json +132 -9
- 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/regscale_models/control_implementation.py +1 -1
- regscale/models/regscale_models/issue.py +0 -1
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/METADATA +1 -17
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/RECORD +94 -61
- tests/regscale/integrations/commercial/test_jira.py +481 -91
- tests/regscale/integrations/commercial/test_wiz.py +96 -200
- tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
- 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_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
- 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 +1 -1
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
- 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_wizv2.py +946 -78
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
- 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/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +83 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Comprehensive unit tests for Wiz V2 utility functions in utils/main.py"""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import codecs
|
|
7
|
+
import datetime
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
import unittest
|
|
12
|
+
from io import StringIO
|
|
13
|
+
from typing import Dict, List
|
|
14
|
+
from unittest.mock import MagicMock, Mock, patch, mock_open, call
|
|
15
|
+
from zipfile import ZipFile
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
import requests
|
|
19
|
+
from pydantic import ValidationError
|
|
20
|
+
|
|
21
|
+
from regscale.core.app.utils.app_utils import error_and_exit
|
|
22
|
+
from regscale.integrations.commercial.wizv2.utils.main import (
|
|
23
|
+
is_report_expired,
|
|
24
|
+
get_notes_from_wiz_props,
|
|
25
|
+
handle_management_type,
|
|
26
|
+
create_asset_type,
|
|
27
|
+
map_category,
|
|
28
|
+
convert_first_seen_to_days,
|
|
29
|
+
fetch_report_by_id,
|
|
30
|
+
download_file,
|
|
31
|
+
fetch_sbom_report,
|
|
32
|
+
fetch_report_id,
|
|
33
|
+
get_framework_names,
|
|
34
|
+
check_reports_for_frameworks,
|
|
35
|
+
create_report_if_needed,
|
|
36
|
+
fetch_and_process_report_data,
|
|
37
|
+
get_or_create_report_id,
|
|
38
|
+
fetch_report_data,
|
|
39
|
+
process_single_report,
|
|
40
|
+
fetch_framework_report,
|
|
41
|
+
fetch_frameworks,
|
|
42
|
+
query_reports,
|
|
43
|
+
send_request,
|
|
44
|
+
create_compliance_report,
|
|
45
|
+
get_report_url_and_status,
|
|
46
|
+
download_report,
|
|
47
|
+
rerun_expired_report,
|
|
48
|
+
check_compliance,
|
|
49
|
+
create_assessment_from_compliance_report,
|
|
50
|
+
create_report_assessment,
|
|
51
|
+
update_implementation_status,
|
|
52
|
+
get_wiz_compliance_settings,
|
|
53
|
+
report_result_to_implementation_status,
|
|
54
|
+
create_vulnerabilities_from_wiz_findings,
|
|
55
|
+
create_single_vulnerability_from_wiz_data,
|
|
56
|
+
_get_category_from_cpe,
|
|
57
|
+
_get_category_from_hardware_types,
|
|
58
|
+
_get_category_from_asset_type,
|
|
59
|
+
_handle_report_response,
|
|
60
|
+
_handle_rate_limit_error,
|
|
61
|
+
_add_controls_to_controls_to_report_dict,
|
|
62
|
+
_clean_passing_list,
|
|
63
|
+
_create_aggregated_assessment_report,
|
|
64
|
+
_try_get_status_from_settings,
|
|
65
|
+
_match_label_to_result,
|
|
66
|
+
_get_default_status_mapping,
|
|
67
|
+
)
|
|
68
|
+
from regscale.models import regscale_models
|
|
69
|
+
from regscale.models.integration_models.wizv2 import ComplianceReport, ComplianceCheckStatus
|
|
70
|
+
|
|
71
|
+
logger = logging.getLogger("regscale")
|
|
72
|
+
PATH = "regscale.integrations.commercial.wizv2.utils.main"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ==================== Date and Reporting Tests ====================
|
|
76
|
+
class TestDateAndReporting(unittest.TestCase):
|
|
77
|
+
"""Test date handling and report validation functions"""
|
|
78
|
+
|
|
79
|
+
def test_is_report_expired_not_expired(self):
|
|
80
|
+
"""Test report that is not expired"""
|
|
81
|
+
# Report from 5 days ago
|
|
82
|
+
five_days_ago = (datetime.datetime.now() - datetime.timedelta(days=5)).isoformat()
|
|
83
|
+
result = is_report_expired(five_days_ago, max_age_days=10)
|
|
84
|
+
self.assertFalse(result)
|
|
85
|
+
|
|
86
|
+
def test_is_report_expired_expired(self):
|
|
87
|
+
"""Test report that is expired"""
|
|
88
|
+
# Report from 20 days ago
|
|
89
|
+
twenty_days_ago = (datetime.datetime.now() - datetime.timedelta(days=20)).isoformat()
|
|
90
|
+
result = is_report_expired(twenty_days_ago, max_age_days=15)
|
|
91
|
+
self.assertTrue(result)
|
|
92
|
+
|
|
93
|
+
def test_is_report_expired_exact_boundary(self):
|
|
94
|
+
"""Test report at exact boundary (should be expired)"""
|
|
95
|
+
# Report from exactly 15 days ago
|
|
96
|
+
fifteen_days_ago = (datetime.datetime.now() - datetime.timedelta(days=15)).isoformat()
|
|
97
|
+
result = is_report_expired(fifteen_days_ago, max_age_days=15)
|
|
98
|
+
self.assertTrue(result)
|
|
99
|
+
|
|
100
|
+
def test_is_report_expired_invalid_date(self):
|
|
101
|
+
"""Test with invalid date format"""
|
|
102
|
+
result = is_report_expired("invalid-date", max_age_days=15)
|
|
103
|
+
self.assertTrue(result)
|
|
104
|
+
|
|
105
|
+
def test_is_report_expired_none_date(self):
|
|
106
|
+
"""Test with None date"""
|
|
107
|
+
result = is_report_expired(None, max_age_days=15)
|
|
108
|
+
self.assertTrue(result)
|
|
109
|
+
|
|
110
|
+
def test_is_report_expired_empty_string(self):
|
|
111
|
+
"""Test with empty string"""
|
|
112
|
+
result = is_report_expired("", max_age_days=15)
|
|
113
|
+
self.assertTrue(result)
|
|
114
|
+
|
|
115
|
+
def test_convert_first_seen_to_days_valid_date(self):
|
|
116
|
+
"""Test converting a valid first seen date to days"""
|
|
117
|
+
# Date from 10 days ago
|
|
118
|
+
ten_days_ago = (datetime.datetime.now() - datetime.timedelta(days=10)).isoformat()
|
|
119
|
+
result = convert_first_seen_to_days(ten_days_ago)
|
|
120
|
+
self.assertEqual(result, 10)
|
|
121
|
+
|
|
122
|
+
def test_convert_first_seen_to_days_today(self):
|
|
123
|
+
"""Test converting today's date to days"""
|
|
124
|
+
today = datetime.datetime.now().isoformat()
|
|
125
|
+
result = convert_first_seen_to_days(today)
|
|
126
|
+
self.assertEqual(result, 0)
|
|
127
|
+
|
|
128
|
+
def test_convert_first_seen_to_days_invalid_date(self):
|
|
129
|
+
"""Test with invalid date format"""
|
|
130
|
+
result = convert_first_seen_to_days("invalid-date")
|
|
131
|
+
self.assertEqual(result, 0)
|
|
132
|
+
|
|
133
|
+
def test_convert_first_seen_to_days_none(self):
|
|
134
|
+
"""Test with None"""
|
|
135
|
+
result = convert_first_seen_to_days(None)
|
|
136
|
+
self.assertEqual(result, 0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ==================== Property and Entity Tests ====================
|
|
140
|
+
class TestPropertiesAndEntities(unittest.TestCase):
|
|
141
|
+
"""Test property extraction and entity handling functions"""
|
|
142
|
+
|
|
143
|
+
def test_get_notes_from_wiz_props_all_properties(self):
|
|
144
|
+
"""Test getting notes with all properties present"""
|
|
145
|
+
wiz_properties = {
|
|
146
|
+
"cloudPlatform": "AWS",
|
|
147
|
+
"providerUniqueId": "i-1234567890",
|
|
148
|
+
"cloudProviderURL": "https://console.aws.amazon.com/ec2/v2/home",
|
|
149
|
+
"_vertexID": "vertex-123",
|
|
150
|
+
"severity_name": "High",
|
|
151
|
+
"severity_description": "Critical vulnerability detected",
|
|
152
|
+
}
|
|
153
|
+
external_id = "ext-123"
|
|
154
|
+
|
|
155
|
+
result = get_notes_from_wiz_props(wiz_properties, external_id)
|
|
156
|
+
|
|
157
|
+
self.assertIn("External ID: ext-123", result)
|
|
158
|
+
self.assertIn("Cloud Platform: AWS", result)
|
|
159
|
+
self.assertIn("Provider Unique ID: i-1234567890", result)
|
|
160
|
+
self.assertIn("cloudProviderURL:", result)
|
|
161
|
+
self.assertIn('target="_blank"', result)
|
|
162
|
+
self.assertIn("Vertex ID: vertex-123", result)
|
|
163
|
+
self.assertIn("Severity Name: High", result)
|
|
164
|
+
self.assertIn("Severity Description: Critical vulnerability detected", result)
|
|
165
|
+
self.assertIn("<br>", result)
|
|
166
|
+
|
|
167
|
+
def test_get_notes_from_wiz_props_minimal_properties(self):
|
|
168
|
+
"""Test getting notes with minimal properties"""
|
|
169
|
+
wiz_properties = {}
|
|
170
|
+
external_id = "ext-456"
|
|
171
|
+
|
|
172
|
+
result = get_notes_from_wiz_props(wiz_properties, external_id)
|
|
173
|
+
|
|
174
|
+
self.assertIn("External ID: ext-456", result)
|
|
175
|
+
self.assertNotIn("Cloud Platform:", result)
|
|
176
|
+
self.assertNotIn("Provider Unique ID:", result)
|
|
177
|
+
|
|
178
|
+
def test_get_notes_from_wiz_props_with_url(self):
|
|
179
|
+
"""Test URL formatting in notes"""
|
|
180
|
+
wiz_properties = {"cloudProviderURL": "https://example.com/resource"}
|
|
181
|
+
external_id = "ext-789"
|
|
182
|
+
|
|
183
|
+
result = get_notes_from_wiz_props(wiz_properties, external_id)
|
|
184
|
+
|
|
185
|
+
self.assertIn('<a href="https://example.com/resource" target="_blank">', result)
|
|
186
|
+
|
|
187
|
+
def test_handle_management_type_managed(self):
|
|
188
|
+
"""Test management type for managed resources"""
|
|
189
|
+
wiz_properties = {"isManaged": True}
|
|
190
|
+
result = handle_management_type(wiz_properties)
|
|
191
|
+
self.assertEqual(result, "External/Third Party Managed")
|
|
192
|
+
|
|
193
|
+
def test_handle_management_type_internally_managed(self):
|
|
194
|
+
"""Test management type for internally managed resources"""
|
|
195
|
+
wiz_properties = {"isManaged": False}
|
|
196
|
+
result = handle_management_type(wiz_properties)
|
|
197
|
+
self.assertEqual(result, "Internally Managed")
|
|
198
|
+
|
|
199
|
+
def test_handle_management_type_missing_key(self):
|
|
200
|
+
"""Test management type when isManaged key is missing"""
|
|
201
|
+
wiz_properties = {}
|
|
202
|
+
result = handle_management_type(wiz_properties)
|
|
203
|
+
self.assertEqual(result, "Internally Managed")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ==================== Asset Category Mapping Tests ====================
|
|
207
|
+
class TestAssetCategoryMapping(unittest.TestCase):
|
|
208
|
+
"""Test asset category mapping functions"""
|
|
209
|
+
|
|
210
|
+
@patch(f"{PATH}._get_category_from_cpe")
|
|
211
|
+
def test_map_category_from_cpe(self, mock_cpe):
|
|
212
|
+
"""Test mapping category from CPE"""
|
|
213
|
+
mock_cpe.return_value = regscale_models.AssetCategory.Hardware
|
|
214
|
+
node = {"type": "VM", "graphEntity": {}}
|
|
215
|
+
|
|
216
|
+
result = map_category(node)
|
|
217
|
+
|
|
218
|
+
self.assertEqual(result, regscale_models.AssetCategory.Hardware)
|
|
219
|
+
mock_cpe.assert_called_once()
|
|
220
|
+
|
|
221
|
+
@patch(f"{PATH}._get_category_from_cpe")
|
|
222
|
+
@patch(f"{PATH}._get_category_from_hardware_types")
|
|
223
|
+
def test_map_category_from_hardware_types(self, mock_hardware, mock_cpe):
|
|
224
|
+
"""Test mapping category from hardware types"""
|
|
225
|
+
mock_cpe.return_value = None
|
|
226
|
+
mock_hardware.return_value = regscale_models.AssetCategory.Hardware
|
|
227
|
+
|
|
228
|
+
node = {"type": "VM", "graphEntity": {}}
|
|
229
|
+
|
|
230
|
+
result = map_category(node)
|
|
231
|
+
|
|
232
|
+
self.assertEqual(result, regscale_models.AssetCategory.Hardware)
|
|
233
|
+
mock_hardware.assert_called_once()
|
|
234
|
+
|
|
235
|
+
@patch(f"{PATH}._get_category_from_cpe")
|
|
236
|
+
@patch(f"{PATH}._get_category_from_hardware_types")
|
|
237
|
+
@patch(f"{PATH}._get_category_from_asset_type")
|
|
238
|
+
def test_map_category_from_asset_type(self, mock_asset_type, mock_hardware, mock_cpe):
|
|
239
|
+
"""Test mapping category from asset type"""
|
|
240
|
+
mock_cpe.return_value = None
|
|
241
|
+
mock_hardware.return_value = None
|
|
242
|
+
mock_asset_type.return_value = regscale_models.AssetCategory.Software
|
|
243
|
+
|
|
244
|
+
node = {"type": "Application", "graphEntity": {}}
|
|
245
|
+
|
|
246
|
+
result = map_category(node)
|
|
247
|
+
|
|
248
|
+
self.assertEqual(result, regscale_models.AssetCategory.Software)
|
|
249
|
+
mock_asset_type.assert_called_once()
|
|
250
|
+
|
|
251
|
+
@patch(f"{PATH}._get_category_from_cpe")
|
|
252
|
+
@patch(f"{PATH}._get_category_from_hardware_types")
|
|
253
|
+
@patch(f"{PATH}._get_category_from_asset_type")
|
|
254
|
+
def test_map_category_default_to_software(self, mock_asset_type, mock_hardware, mock_cpe):
|
|
255
|
+
"""Test default category is Software"""
|
|
256
|
+
mock_cpe.return_value = None
|
|
257
|
+
mock_hardware.return_value = None
|
|
258
|
+
mock_asset_type.return_value = None
|
|
259
|
+
|
|
260
|
+
node = {"type": "Unknown", "graphEntity": {}}
|
|
261
|
+
|
|
262
|
+
result = map_category(node)
|
|
263
|
+
|
|
264
|
+
self.assertEqual(result, regscale_models.AssetCategory.Software)
|
|
265
|
+
|
|
266
|
+
@patch(f"{PATH}.extract_product_name_and_version")
|
|
267
|
+
def test_get_category_from_cpe_valid(self, mock_extract):
|
|
268
|
+
"""Test getting category from valid CPE"""
|
|
269
|
+
mock_extract.return_value = {"part": "h"}
|
|
270
|
+
node = {"graphEntity": {"properties": {"cpe": "cpe:2.3:h:vendor:product:*"}}}
|
|
271
|
+
|
|
272
|
+
result = _get_category_from_cpe(node)
|
|
273
|
+
|
|
274
|
+
self.assertEqual(result, regscale_models.AssetCategory.Hardware)
|
|
275
|
+
|
|
276
|
+
@patch(f"{PATH}.extract_product_name_and_version")
|
|
277
|
+
def test_get_category_from_cpe_no_cpe(self, mock_extract):
|
|
278
|
+
"""Test getting category when no CPE present"""
|
|
279
|
+
node = {"graphEntity": {"properties": {}}}
|
|
280
|
+
|
|
281
|
+
result = _get_category_from_cpe(node)
|
|
282
|
+
|
|
283
|
+
self.assertIsNone(result)
|
|
284
|
+
|
|
285
|
+
@patch(f"{PATH}.WizVariables")
|
|
286
|
+
def test_get_category_from_hardware_types_matching_type(self, mock_vars):
|
|
287
|
+
"""Test getting hardware category from matching type"""
|
|
288
|
+
mock_vars.useWizHardwareAssetTypes = True
|
|
289
|
+
mock_vars.wizHardwareAssetTypes = ["VIRTUAL_MACHINE", "CONTAINER"]
|
|
290
|
+
|
|
291
|
+
node = {"type": "VIRTUAL_MACHINE", "graphEntity": {}}
|
|
292
|
+
|
|
293
|
+
result = _get_category_from_hardware_types(node, "VIRTUAL_MACHINE")
|
|
294
|
+
|
|
295
|
+
self.assertEqual(result, regscale_models.AssetCategory.Hardware)
|
|
296
|
+
|
|
297
|
+
@patch(f"{PATH}.WizVariables")
|
|
298
|
+
def test_get_category_from_hardware_types_feature_disabled(self, mock_vars):
|
|
299
|
+
"""Test when useWizHardwareAssetTypes is disabled"""
|
|
300
|
+
mock_vars.useWizHardwareAssetTypes = False
|
|
301
|
+
|
|
302
|
+
node = {"type": "VIRTUAL_MACHINE", "graphEntity": {}}
|
|
303
|
+
|
|
304
|
+
result = _get_category_from_hardware_types(node, "VIRTUAL_MACHINE")
|
|
305
|
+
|
|
306
|
+
self.assertIsNone(result)
|
|
307
|
+
|
|
308
|
+
def test_get_category_from_asset_type_valid_attribute(self):
|
|
309
|
+
"""Test getting category from valid asset type attribute"""
|
|
310
|
+
node = {"type": "Software"}
|
|
311
|
+
|
|
312
|
+
result = _get_category_from_asset_type("Software", node)
|
|
313
|
+
|
|
314
|
+
self.assertEqual(result, regscale_models.AssetCategory.Software)
|
|
315
|
+
|
|
316
|
+
def test_get_category_from_asset_type_invalid_attribute(self):
|
|
317
|
+
"""Test getting category from invalid asset type"""
|
|
318
|
+
node = {"type": "Unknown"}
|
|
319
|
+
|
|
320
|
+
result = _get_category_from_asset_type("UnknownType", node)
|
|
321
|
+
|
|
322
|
+
self.assertIsNone(result)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ==================== Asset Type Creation Tests ====================
|
|
326
|
+
class TestAssetTypeCreation(unittest.TestCase):
|
|
327
|
+
"""Test asset type creation and formatting"""
|
|
328
|
+
|
|
329
|
+
@patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
|
|
330
|
+
@patch(f"{PATH}.regscale_models.Metadata")
|
|
331
|
+
def test_create_asset_type_new_type(self, mock_metadata_class, mock_get_metadata):
|
|
332
|
+
"""Test creating a new asset type"""
|
|
333
|
+
mock_get_metadata.return_value = []
|
|
334
|
+
mock_metadata_instance = MagicMock()
|
|
335
|
+
mock_metadata_class.return_value = mock_metadata_instance
|
|
336
|
+
|
|
337
|
+
result = create_asset_type("VIRTUAL_MACHINE")
|
|
338
|
+
|
|
339
|
+
self.assertEqual(result, "Virtual Machine")
|
|
340
|
+
mock_metadata_instance.create.assert_called_once()
|
|
341
|
+
|
|
342
|
+
@patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
|
|
343
|
+
def test_create_asset_type_existing_type(self, mock_get_metadata):
|
|
344
|
+
"""Test with existing asset type"""
|
|
345
|
+
mock_metadata = MagicMock()
|
|
346
|
+
mock_metadata.value = "Virtual Machine"
|
|
347
|
+
mock_get_metadata.return_value = [mock_metadata]
|
|
348
|
+
|
|
349
|
+
result = create_asset_type("virtual_machine")
|
|
350
|
+
|
|
351
|
+
self.assertEqual(result, "Virtual Machine")
|
|
352
|
+
|
|
353
|
+
@patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
|
|
354
|
+
@patch(f"{PATH}.regscale_models.Metadata")
|
|
355
|
+
def test_create_asset_type_formatting(self, mock_metadata_class, mock_get_metadata):
|
|
356
|
+
"""Test asset type string formatting"""
|
|
357
|
+
mock_get_metadata.return_value = []
|
|
358
|
+
mock_metadata_instance = MagicMock()
|
|
359
|
+
mock_metadata_class.return_value = mock_metadata_instance
|
|
360
|
+
|
|
361
|
+
result = create_asset_type("test_asset_type")
|
|
362
|
+
|
|
363
|
+
self.assertEqual(result, "Test Asset Type")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ==================== Framework and Report Tests ====================
|
|
367
|
+
class TestFrameworkAndReports(unittest.TestCase):
|
|
368
|
+
"""Test framework and report handling functions"""
|
|
369
|
+
|
|
370
|
+
def test_get_framework_names_single(self):
|
|
371
|
+
"""Test getting framework names with single framework"""
|
|
372
|
+
wiz_frameworks = [{"name": "NIST SP 800-53 Revision 5"}]
|
|
373
|
+
|
|
374
|
+
result = get_framework_names(wiz_frameworks)
|
|
375
|
+
|
|
376
|
+
self.assertEqual(result, ["NIST_SP_800-53_Revision_5"])
|
|
377
|
+
|
|
378
|
+
def test_get_framework_names_multiple(self):
|
|
379
|
+
"""Test getting framework names with multiple frameworks"""
|
|
380
|
+
wiz_frameworks = [{"name": "NIST SP 800-53 Revision 5"}, {"name": "NIST CSF v1.1"}, {"name": "ISO 27001"}]
|
|
381
|
+
|
|
382
|
+
result = get_framework_names(wiz_frameworks)
|
|
383
|
+
|
|
384
|
+
self.assertEqual(result, ["NIST_SP_800-53_Revision_5", "NIST_CSF_v1.1", "ISO_27001"])
|
|
385
|
+
|
|
386
|
+
def test_get_framework_names_empty(self):
|
|
387
|
+
"""Test getting framework names with empty list"""
|
|
388
|
+
result = get_framework_names([])
|
|
389
|
+
self.assertEqual(result, [])
|
|
390
|
+
|
|
391
|
+
def test_check_reports_for_frameworks_found(self):
|
|
392
|
+
"""Test checking reports when framework is found"""
|
|
393
|
+
reports = [
|
|
394
|
+
{"name": "NIST_SP_800-53_Revision_5_project_123"},
|
|
395
|
+
{"name": "ISO_27001_project_456"},
|
|
396
|
+
]
|
|
397
|
+
frames = ["NIST_SP_800-53_Revision_5"]
|
|
398
|
+
|
|
399
|
+
result = check_reports_for_frameworks(reports, frames)
|
|
400
|
+
|
|
401
|
+
self.assertTrue(result)
|
|
402
|
+
|
|
403
|
+
def test_check_reports_for_frameworks_not_found(self):
|
|
404
|
+
"""Test checking reports when framework is not found"""
|
|
405
|
+
reports = [
|
|
406
|
+
{"name": "ISO_27001_project_456"},
|
|
407
|
+
]
|
|
408
|
+
frames = ["NIST_SP_800-53_Revision_5"]
|
|
409
|
+
|
|
410
|
+
result = check_reports_for_frameworks(reports, frames)
|
|
411
|
+
|
|
412
|
+
self.assertFalse(result)
|
|
413
|
+
|
|
414
|
+
def test_check_reports_for_frameworks_empty_reports(self):
|
|
415
|
+
"""Test checking empty reports"""
|
|
416
|
+
result = check_reports_for_frameworks([], ["NIST_SP_800-53_Revision_5"])
|
|
417
|
+
self.assertFalse(result)
|
|
418
|
+
|
|
419
|
+
@patch(f"{PATH}.create_compliance_report")
|
|
420
|
+
def test_create_report_if_needed_creates_new(self, mock_create_report):
|
|
421
|
+
"""Test creating a new report when needed"""
|
|
422
|
+
mock_create_report.return_value = "new-report-123"
|
|
423
|
+
|
|
424
|
+
wiz_project_id = "project-456"
|
|
425
|
+
frames = ["NIST_SP_800-53_Revision_5"]
|
|
426
|
+
wiz_frameworks = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
|
|
427
|
+
reports = []
|
|
428
|
+
snake_framework = "NIST_SP_800-53_Revision_5"
|
|
429
|
+
|
|
430
|
+
result = create_report_if_needed(wiz_project_id, frames, wiz_frameworks, reports, snake_framework)
|
|
431
|
+
|
|
432
|
+
self.assertEqual(result, ["new-report-123"])
|
|
433
|
+
mock_create_report.assert_called_once()
|
|
434
|
+
|
|
435
|
+
def test_create_report_if_needed_existing_reports(self):
|
|
436
|
+
"""Test when reports already exist"""
|
|
437
|
+
wiz_project_id = "project-456"
|
|
438
|
+
frames = ["NIST_SP_800-53_Revision_5"]
|
|
439
|
+
wiz_frameworks = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
|
|
440
|
+
reports = [
|
|
441
|
+
{"id": "existing-report-1", "name": "NIST_SP_800-53_Revision_5_project_123"},
|
|
442
|
+
{"id": "existing-report-2", "name": "other_framework_project_123"},
|
|
443
|
+
]
|
|
444
|
+
snake_framework = "NIST_SP_800-53_Revision_5"
|
|
445
|
+
|
|
446
|
+
result = create_report_if_needed(wiz_project_id, frames, wiz_frameworks, reports, snake_framework)
|
|
447
|
+
|
|
448
|
+
self.assertEqual(result, ["existing-report-1"])
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ==================== Report Download and Processing Tests ====================
|
|
452
|
+
class TestReportDownloadAndProcessing(unittest.TestCase):
|
|
453
|
+
"""Test report download and processing functions"""
|
|
454
|
+
|
|
455
|
+
@patch(f"{PATH}.get_report_url_and_status")
|
|
456
|
+
@patch(f"{PATH}.requests.get")
|
|
457
|
+
@patch(f"{PATH}.csv.DictReader")
|
|
458
|
+
def test_fetch_and_process_report_data(self, mock_dict_reader, mock_requests_get, mock_get_url):
|
|
459
|
+
"""Test fetching and processing report data"""
|
|
460
|
+
mock_get_url.return_value = "https://example.com/report.csv"
|
|
461
|
+
|
|
462
|
+
# Mock response
|
|
463
|
+
mock_response = MagicMock()
|
|
464
|
+
mock_response.iter_lines.return_value = [
|
|
465
|
+
b"col1,col2",
|
|
466
|
+
b"val1,val2",
|
|
467
|
+
b"val3,val4",
|
|
468
|
+
]
|
|
469
|
+
mock_requests_get.return_value.__enter__.return_value = mock_response
|
|
470
|
+
|
|
471
|
+
# Mock CSV reader
|
|
472
|
+
mock_dict_reader.return_value = [
|
|
473
|
+
{"col1": "val1", "col2": "val2"},
|
|
474
|
+
{"col1": "val3", "col2": "val4"},
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
wiz_report_ids = ["report-1"]
|
|
478
|
+
result = fetch_and_process_report_data(wiz_report_ids)
|
|
479
|
+
|
|
480
|
+
self.assertEqual(len(result), 2)
|
|
481
|
+
mock_get_url.assert_called_once_with("report-1")
|
|
482
|
+
|
|
483
|
+
@patch(f"{PATH}.check_file_path")
|
|
484
|
+
@patch(f"{PATH}.requests.get")
|
|
485
|
+
@patch("builtins.open", new_callable=mock_open)
|
|
486
|
+
def test_download_file_success(self, mock_file, mock_requests_get, mock_check_path):
|
|
487
|
+
"""Test successful file download"""
|
|
488
|
+
mock_response = MagicMock()
|
|
489
|
+
mock_response.raise_for_status.return_value = None
|
|
490
|
+
mock_response.iter_content.return_value = [b"chunk1", b"chunk2"]
|
|
491
|
+
mock_requests_get.return_value.__enter__.return_value = mock_response
|
|
492
|
+
|
|
493
|
+
download_file("https://example.com/file.csv", "artifacts/test.csv")
|
|
494
|
+
|
|
495
|
+
mock_check_path.assert_called_once_with("artifacts")
|
|
496
|
+
mock_requests_get.assert_called_once()
|
|
497
|
+
mock_file.assert_called_once()
|
|
498
|
+
|
|
499
|
+
@patch(f"{PATH}.check_file_path")
|
|
500
|
+
@patch(f"{PATH}.requests.get")
|
|
501
|
+
def test_download_file_http_error(self, mock_requests_get, mock_check_path):
|
|
502
|
+
"""Test file download with HTTP error"""
|
|
503
|
+
mock_response = MagicMock()
|
|
504
|
+
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
|
|
505
|
+
mock_requests_get.return_value.__enter__.return_value = mock_response
|
|
506
|
+
|
|
507
|
+
with self.assertRaises(requests.HTTPError):
|
|
508
|
+
download_file("https://example.com/file.csv", "artifacts/test.csv")
|
|
509
|
+
|
|
510
|
+
@patch(f"{PATH}.WizVariables")
|
|
511
|
+
@patch(f"{PATH}.PaginatedGraphQLClient")
|
|
512
|
+
@patch(f"{PATH}.error_and_exit")
|
|
513
|
+
def test_fetch_report_by_id_no_token(self, mock_error_exit, mock_client, mock_vars):
|
|
514
|
+
"""Test fetch_report_by_id with missing token"""
|
|
515
|
+
mock_vars.wizAccessToken = None
|
|
516
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
517
|
+
|
|
518
|
+
with pytest.raises(SystemExit):
|
|
519
|
+
fetch_report_by_id("report-123", 456)
|
|
520
|
+
|
|
521
|
+
mock_error_exit.assert_called_once()
|
|
522
|
+
|
|
523
|
+
@patch(f"{PATH}.WizVariables")
|
|
524
|
+
@patch(f"{PATH}.PaginatedGraphQLClient")
|
|
525
|
+
@patch(f"{PATH}.download_file")
|
|
526
|
+
@patch(f"{PATH}.Api")
|
|
527
|
+
@patch(f"{PATH}.File")
|
|
528
|
+
def test_fetch_report_by_id_success(self, mock_file, mock_api, mock_download, mock_client, mock_vars):
|
|
529
|
+
"""Test successful report fetch"""
|
|
530
|
+
mock_vars.wizAccessToken = "test-token"
|
|
531
|
+
mock_vars.wizUrl = "https://api.wiz.io/graphql"
|
|
532
|
+
|
|
533
|
+
mock_client_instance = MagicMock()
|
|
534
|
+
mock_client.return_value = mock_client_instance
|
|
535
|
+
mock_client_instance.fetch_results.return_value = {
|
|
536
|
+
"report": {"lastRun": {"url": "https://example.com/report.csv"}}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
mock_api_instance = MagicMock()
|
|
540
|
+
mock_api.return_value = mock_api_instance
|
|
541
|
+
|
|
542
|
+
fetch_report_by_id("report-123", 456)
|
|
543
|
+
|
|
544
|
+
mock_download.assert_called_once()
|
|
545
|
+
mock_file.upload_file_to_regscale.assert_called_once()
|
|
546
|
+
|
|
547
|
+
@patch(f"{PATH}.WizVariables")
|
|
548
|
+
@patch(f"{PATH}.PaginatedGraphQLClient")
|
|
549
|
+
def test_fetch_report_by_id_with_errors(self, mock_client, mock_vars):
|
|
550
|
+
"""Test fetch_report_by_id with API errors"""
|
|
551
|
+
mock_vars.wizAccessToken = "test-token"
|
|
552
|
+
mock_vars.wizUrl = "https://api.wiz.io/graphql"
|
|
553
|
+
|
|
554
|
+
mock_client_instance = MagicMock()
|
|
555
|
+
mock_client.return_value = mock_client_instance
|
|
556
|
+
mock_client_instance.fetch_results.return_value = {"errors": [{"message": "API Error"}]}
|
|
557
|
+
|
|
558
|
+
# Should not raise, just log error
|
|
559
|
+
fetch_report_by_id("report-123", 456)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# ==================== SBOM Report Tests ====================
|
|
563
|
+
class TestSBOMReports(unittest.TestCase):
|
|
564
|
+
"""Test SBOM report fetching and processing"""
|
|
565
|
+
|
|
566
|
+
@patch(f"{PATH}.WizVariables")
|
|
567
|
+
@patch(f"{PATH}.error_and_exit")
|
|
568
|
+
def test_fetch_sbom_report_no_token(self, mock_error_exit, mock_vars):
|
|
569
|
+
"""Test fetch_sbom_report with missing token"""
|
|
570
|
+
mock_vars.wizAccessToken = None
|
|
571
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
572
|
+
|
|
573
|
+
with pytest.raises(SystemExit):
|
|
574
|
+
fetch_sbom_report("report-123", "456")
|
|
575
|
+
|
|
576
|
+
mock_error_exit.assert_called_once()
|
|
577
|
+
|
|
578
|
+
@patch(f"{PATH}.WizVariables")
|
|
579
|
+
@patch(f"{PATH}.PaginatedGraphQLClient")
|
|
580
|
+
@patch(f"{PATH}.download_file")
|
|
581
|
+
@patch(f"{PATH}.ZipFile")
|
|
582
|
+
@patch(f"{PATH}.Sbom")
|
|
583
|
+
def test_fetch_sbom_report_success(self, mock_sbom, mock_zipfile, mock_download, mock_client, mock_vars):
|
|
584
|
+
"""Test successful SBOM report fetch"""
|
|
585
|
+
mock_vars.wizAccessToken = "test-token"
|
|
586
|
+
mock_vars.wizUrl = "https://api.wiz.io/graphql"
|
|
587
|
+
|
|
588
|
+
# Mock client response
|
|
589
|
+
mock_client_instance = MagicMock()
|
|
590
|
+
mock_client.return_value = mock_client_instance
|
|
591
|
+
mock_client_instance.fetch_results.return_value = {
|
|
592
|
+
"report": {"lastRun": {"url": "https://example.com/sbom.zip"}}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Mock zip file
|
|
596
|
+
mock_zip_instance = MagicMock()
|
|
597
|
+
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
|
|
598
|
+
mock_zip_instance.namelist.return_value = ["sbom.json"]
|
|
599
|
+
|
|
600
|
+
# Mock JSON file inside zip
|
|
601
|
+
mock_json_file = MagicMock()
|
|
602
|
+
mock_json_file.__enter__.return_value = mock_json_file
|
|
603
|
+
sbom_data = {"bomFormat": "CycloneDX", "specVersion": "1.5", "components": []}
|
|
604
|
+
mock_json_file.read.return_value = json.dumps(sbom_data).encode()
|
|
605
|
+
mock_zip_instance.open.return_value = mock_json_file
|
|
606
|
+
|
|
607
|
+
# Mock Sbom model
|
|
608
|
+
mock_sbom_instance = MagicMock()
|
|
609
|
+
mock_sbom.return_value = mock_sbom_instance
|
|
610
|
+
|
|
611
|
+
fetch_sbom_report("report-123", "456")
|
|
612
|
+
|
|
613
|
+
mock_download.assert_called_once()
|
|
614
|
+
mock_sbom.assert_called_once()
|
|
615
|
+
mock_sbom_instance.create_or_update.assert_called_once()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# ==================== GraphQL Request Tests ====================
|
|
619
|
+
class TestGraphQLRequests(unittest.TestCase):
|
|
620
|
+
"""Test GraphQL request functions"""
|
|
621
|
+
|
|
622
|
+
@patch(f"{PATH}.WizVariables")
|
|
623
|
+
@patch(f"{PATH}.Api")
|
|
624
|
+
def test_send_request_success(self, mock_api, mock_vars):
|
|
625
|
+
"""Test successful send_request"""
|
|
626
|
+
mock_vars.wizUrl = "https://api.wiz.io/graphql"
|
|
627
|
+
mock_vars.wizAccessToken = "test-token"
|
|
628
|
+
|
|
629
|
+
mock_api_instance = MagicMock()
|
|
630
|
+
mock_api.return_value = mock_api_instance
|
|
631
|
+
|
|
632
|
+
mock_response = MagicMock()
|
|
633
|
+
mock_api_instance.post.return_value = mock_response
|
|
634
|
+
|
|
635
|
+
query = "query { test }"
|
|
636
|
+
variables = {"var1": "value1"}
|
|
637
|
+
|
|
638
|
+
result = send_request(query, variables)
|
|
639
|
+
|
|
640
|
+
self.assertEqual(result, mock_response)
|
|
641
|
+
mock_api_instance.post.assert_called_once()
|
|
642
|
+
|
|
643
|
+
@patch(f"{PATH}.WizVariables")
|
|
644
|
+
@patch(f"{PATH}.Api")
|
|
645
|
+
def test_send_request_no_token(self, mock_api, mock_vars):
|
|
646
|
+
"""Test send_request with missing token"""
|
|
647
|
+
mock_vars.wizAccessToken = None
|
|
648
|
+
|
|
649
|
+
query = "query { test }"
|
|
650
|
+
variables = {"var1": "value1"}
|
|
651
|
+
|
|
652
|
+
with self.assertRaises(ValueError) as context:
|
|
653
|
+
send_request(query, variables)
|
|
654
|
+
|
|
655
|
+
self.assertIn("access token is missing", str(context.exception))
|
|
656
|
+
|
|
657
|
+
@patch(f"{PATH}.send_request")
|
|
658
|
+
def test_fetch_frameworks_success(self, mock_send_request):
|
|
659
|
+
"""Test successful fetch_frameworks"""
|
|
660
|
+
mock_response = MagicMock()
|
|
661
|
+
mock_response.ok = True
|
|
662
|
+
mock_response.json.return_value = {
|
|
663
|
+
"data": {
|
|
664
|
+
"securityFrameworks": {
|
|
665
|
+
"nodes": [
|
|
666
|
+
{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"},
|
|
667
|
+
{"id": "framework-2", "name": "ISO 27001"},
|
|
668
|
+
]
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
mock_send_request.return_value = mock_response
|
|
673
|
+
|
|
674
|
+
result = fetch_frameworks()
|
|
675
|
+
|
|
676
|
+
self.assertEqual(len(result), 2)
|
|
677
|
+
self.assertEqual(result[0]["name"], "NIST SP 800-53 Revision 5")
|
|
678
|
+
|
|
679
|
+
@patch(f"{PATH}.send_request")
|
|
680
|
+
@patch(f"{PATH}.error_and_exit")
|
|
681
|
+
def test_fetch_frameworks_error(self, mock_error_exit, mock_send_request):
|
|
682
|
+
"""Test fetch_frameworks with API error"""
|
|
683
|
+
mock_response = MagicMock()
|
|
684
|
+
mock_response.ok = False
|
|
685
|
+
mock_response.status_code = 500
|
|
686
|
+
mock_response.text = "Internal Server Error"
|
|
687
|
+
mock_send_request.return_value = mock_response
|
|
688
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
689
|
+
|
|
690
|
+
with pytest.raises(SystemExit):
|
|
691
|
+
fetch_frameworks()
|
|
692
|
+
|
|
693
|
+
mock_error_exit.assert_called_once()
|
|
694
|
+
|
|
695
|
+
@patch(f"{PATH}.send_request")
|
|
696
|
+
def test_query_reports_success(self, mock_send_request):
|
|
697
|
+
"""Test successful query_reports"""
|
|
698
|
+
mock_response = MagicMock()
|
|
699
|
+
mock_response.json.return_value = {
|
|
700
|
+
"data": {
|
|
701
|
+
"reports": {
|
|
702
|
+
"nodes": [
|
|
703
|
+
{"id": "report-1", "name": "Test Report 1"},
|
|
704
|
+
{"id": "report-2", "name": "Test Report 2"},
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
mock_send_request.return_value = mock_response
|
|
710
|
+
|
|
711
|
+
result = query_reports("project-123")
|
|
712
|
+
|
|
713
|
+
self.assertEqual(len(result), 2)
|
|
714
|
+
self.assertEqual(result[0]["id"], "report-1")
|
|
715
|
+
|
|
716
|
+
@patch(f"{PATH}.send_request")
|
|
717
|
+
@patch(f"{PATH}.error_and_exit")
|
|
718
|
+
def test_query_reports_with_errors(self, mock_error_exit, mock_send_request):
|
|
719
|
+
"""Test query_reports with API errors"""
|
|
720
|
+
mock_response = MagicMock()
|
|
721
|
+
mock_response.json.return_value = {"errors": [{"message": "API Error"}]}
|
|
722
|
+
mock_send_request.return_value = mock_response
|
|
723
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
724
|
+
|
|
725
|
+
with pytest.raises(SystemExit):
|
|
726
|
+
query_reports("project-123")
|
|
727
|
+
|
|
728
|
+
mock_error_exit.assert_called_once()
|
|
729
|
+
|
|
730
|
+
@patch(f"{PATH}.send_request")
|
|
731
|
+
@patch(f"{PATH}.error_and_exit")
|
|
732
|
+
def test_query_reports_json_decode_error(self, mock_error_exit, mock_send_request):
|
|
733
|
+
"""Test query_reports with JSON decode error"""
|
|
734
|
+
mock_response = MagicMock()
|
|
735
|
+
mock_response.json.side_effect = requests.JSONDecodeError("msg", "doc", 0)
|
|
736
|
+
mock_response.status_code = 500
|
|
737
|
+
mock_response.reason = "Internal Server Error"
|
|
738
|
+
mock_send_request.return_value = mock_response
|
|
739
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
740
|
+
|
|
741
|
+
with pytest.raises(SystemExit):
|
|
742
|
+
query_reports("project-123")
|
|
743
|
+
|
|
744
|
+
mock_error_exit.assert_called_once()
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# ==================== Report Creation and Download Tests ====================
|
|
748
|
+
class TestReportCreationAndDownload(unittest.TestCase):
|
|
749
|
+
"""Test report creation and download functions"""
|
|
750
|
+
|
|
751
|
+
@patch(f"{PATH}.fetch_report_id")
|
|
752
|
+
def test_create_compliance_report(self, mock_fetch_report_id):
|
|
753
|
+
"""Test creating compliance report"""
|
|
754
|
+
mock_fetch_report_id.return_value = "new-report-789"
|
|
755
|
+
|
|
756
|
+
result = create_compliance_report(
|
|
757
|
+
report_name="Test_Report", wiz_project_id="project-123", framework_id="framework-456"
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
self.assertEqual(result, "new-report-789")
|
|
761
|
+
mock_fetch_report_id.assert_called_once()
|
|
762
|
+
|
|
763
|
+
@patch(f"{PATH}.send_request")
|
|
764
|
+
def test_download_report(self, mock_send_request):
|
|
765
|
+
"""Test download_report"""
|
|
766
|
+
mock_response = MagicMock()
|
|
767
|
+
mock_send_request.return_value = mock_response
|
|
768
|
+
|
|
769
|
+
variables = {"reportId": "report-123"}
|
|
770
|
+
result = download_report(variables)
|
|
771
|
+
|
|
772
|
+
self.assertEqual(result, mock_response)
|
|
773
|
+
mock_send_request.assert_called_once()
|
|
774
|
+
|
|
775
|
+
@patch(f"{PATH}.send_request")
|
|
776
|
+
def test_rerun_expired_report(self, mock_send_request):
|
|
777
|
+
"""Test rerun_expired_report"""
|
|
778
|
+
mock_response = MagicMock()
|
|
779
|
+
mock_send_request.return_value = mock_response
|
|
780
|
+
|
|
781
|
+
variables = {"reportId": "report-123"}
|
|
782
|
+
result = rerun_expired_report(variables)
|
|
783
|
+
|
|
784
|
+
self.assertEqual(result, mock_response)
|
|
785
|
+
mock_send_request.assert_called_once()
|
|
786
|
+
|
|
787
|
+
@patch(f"{PATH}.time.sleep")
|
|
788
|
+
@patch(f"{PATH}.download_report")
|
|
789
|
+
def test_get_report_url_and_status_completed(self, mock_download_report, mock_sleep):
|
|
790
|
+
"""Test get_report_url_and_status with completed status"""
|
|
791
|
+
mock_response = MagicMock()
|
|
792
|
+
mock_response.ok = True
|
|
793
|
+
mock_response.json.return_value = {
|
|
794
|
+
"data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
|
|
795
|
+
}
|
|
796
|
+
mock_download_report.return_value = mock_response
|
|
797
|
+
|
|
798
|
+
result = get_report_url_and_status("report-123")
|
|
799
|
+
|
|
800
|
+
self.assertEqual(result, "https://example.com/report.csv")
|
|
801
|
+
mock_download_report.assert_called_once()
|
|
802
|
+
|
|
803
|
+
@patch(f"{PATH}.time.sleep")
|
|
804
|
+
@patch(f"{PATH}.download_report")
|
|
805
|
+
def test_get_report_url_and_status_failed_response(self, mock_download_report, mock_sleep):
|
|
806
|
+
"""Test get_report_url_and_status with failed response"""
|
|
807
|
+
mock_response = MagicMock()
|
|
808
|
+
mock_response.ok = False
|
|
809
|
+
mock_download_report.return_value = mock_response
|
|
810
|
+
|
|
811
|
+
with self.assertRaises(requests.RequestException) as context:
|
|
812
|
+
get_report_url_and_status("report-123")
|
|
813
|
+
|
|
814
|
+
self.assertIn("Failed to download report", str(context.exception))
|
|
815
|
+
|
|
816
|
+
def test_handle_rate_limit_error_with_rate_limit(self):
|
|
817
|
+
"""Test handling rate limit error"""
|
|
818
|
+
errors = [{"message": "Rate limit exceeded", "extensions": {"retryAfter": 0.001}}]
|
|
819
|
+
|
|
820
|
+
with patch(f"{PATH}.time.sleep") as mock_sleep:
|
|
821
|
+
result = _handle_rate_limit_error(errors)
|
|
822
|
+
|
|
823
|
+
self.assertTrue(result)
|
|
824
|
+
mock_sleep.assert_called_once_with(0.001)
|
|
825
|
+
|
|
826
|
+
def test_handle_rate_limit_error_without_rate_limit(self):
|
|
827
|
+
"""Test handling non-rate-limit error"""
|
|
828
|
+
errors = [{"message": "Some other error"}]
|
|
829
|
+
|
|
830
|
+
result = _handle_rate_limit_error(errors)
|
|
831
|
+
|
|
832
|
+
self.assertFalse(result)
|
|
833
|
+
|
|
834
|
+
def test_handle_report_response_completed(self):
|
|
835
|
+
"""Test handling completed report response"""
|
|
836
|
+
response_json = {"data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report"}}}}
|
|
837
|
+
|
|
838
|
+
result = _handle_report_response(response_json, "report-123")
|
|
839
|
+
|
|
840
|
+
self.assertEqual(result, "https://example.com/report")
|
|
841
|
+
|
|
842
|
+
@patch(f"{PATH}.rerun_expired_report")
|
|
843
|
+
@patch(f"{PATH}.get_report_url_and_status")
|
|
844
|
+
def test_handle_report_response_expired(self, mock_get_url, mock_rerun):
|
|
845
|
+
"""Test handling expired report response"""
|
|
846
|
+
response_json = {"data": {"report": {"lastRun": {"status": "EXPIRED"}}}}
|
|
847
|
+
mock_get_url.return_value = "https://example.com/new-report"
|
|
848
|
+
|
|
849
|
+
result = _handle_report_response(response_json, "report-123")
|
|
850
|
+
|
|
851
|
+
self.assertEqual(result, "https://example.com/new-report")
|
|
852
|
+
mock_rerun.assert_called_once()
|
|
853
|
+
|
|
854
|
+
def test_handle_report_response_with_errors(self):
|
|
855
|
+
"""Test handling response with errors"""
|
|
856
|
+
response_json = {"errors": [{"message": "API Error"}]}
|
|
857
|
+
|
|
858
|
+
result = _handle_report_response(response_json, "report-123")
|
|
859
|
+
|
|
860
|
+
self.assertIsNone(result)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
# ==================== Compliance Check Tests ====================
|
|
864
|
+
class TestComplianceChecks(unittest.TestCase):
|
|
865
|
+
"""Test compliance checking and assessment creation"""
|
|
866
|
+
|
|
867
|
+
def test_check_compliance_passing_control(self):
|
|
868
|
+
"""Test checking compliance for passing control"""
|
|
869
|
+
cr_dict = {
|
|
870
|
+
"Framework": "NIST SP 800-53 Revision 5",
|
|
871
|
+
"Compliance Check Name (Wiz Subcategory)": "ac-1 access control policy",
|
|
872
|
+
"Result": "Pass",
|
|
873
|
+
"Resource Name": "test-resource",
|
|
874
|
+
"Resource ID": "res-123",
|
|
875
|
+
"Cloud Provider": "AWS",
|
|
876
|
+
"Cloud Provider ID": "cloud-123",
|
|
877
|
+
"Object Type": "VM",
|
|
878
|
+
"Native Type": "Virtual Machine",
|
|
879
|
+
"Subscription": "sub-123",
|
|
880
|
+
"Policy ID": "policy-123",
|
|
881
|
+
"Policy Short Name": "AC-1",
|
|
882
|
+
"Severity": "Medium",
|
|
883
|
+
"Assessed At": "2023-07-15T14:37:55.450532Z",
|
|
884
|
+
}
|
|
885
|
+
cr = ComplianceReport(**cr_dict)
|
|
886
|
+
|
|
887
|
+
controls = [{"controlId": "AC-1", "id": 1}]
|
|
888
|
+
passing = {}
|
|
889
|
+
failing = {}
|
|
890
|
+
controls_to_reports = {}
|
|
891
|
+
|
|
892
|
+
check_compliance(cr, controls, passing, failing, controls_to_reports)
|
|
893
|
+
|
|
894
|
+
self.assertIn("ac-1", passing)
|
|
895
|
+
self.assertIn("ac-1", controls_to_reports)
|
|
896
|
+
self.assertEqual(len(controls_to_reports["ac-1"]), 1)
|
|
897
|
+
|
|
898
|
+
def test_check_compliance_failing_control(self):
|
|
899
|
+
"""Test checking compliance for failing control"""
|
|
900
|
+
cr_dict = {
|
|
901
|
+
"Framework": "NIST SP 800-53 Revision 5",
|
|
902
|
+
"Compliance Check Name (Wiz Subcategory)": "ac-2 account management",
|
|
903
|
+
"Result": "Fail",
|
|
904
|
+
"Resource Name": "test-resource",
|
|
905
|
+
"Resource ID": "res-456",
|
|
906
|
+
"Cloud Provider": "Azure",
|
|
907
|
+
"Cloud Provider ID": "cloud-456",
|
|
908
|
+
"Object Type": "VM",
|
|
909
|
+
"Native Type": "Virtual Machine",
|
|
910
|
+
"Subscription": "sub-456",
|
|
911
|
+
"Policy ID": "policy-456",
|
|
912
|
+
"Policy Short Name": "AC-2",
|
|
913
|
+
"Severity": "High",
|
|
914
|
+
"Assessed At": "2023-07-15T14:37:55.450532Z",
|
|
915
|
+
}
|
|
916
|
+
cr = ComplianceReport(**cr_dict)
|
|
917
|
+
|
|
918
|
+
controls = [{"controlId": "AC-2", "id": 2}]
|
|
919
|
+
passing = {}
|
|
920
|
+
failing = {}
|
|
921
|
+
controls_to_reports = {}
|
|
922
|
+
|
|
923
|
+
check_compliance(cr, controls, passing, failing, controls_to_reports)
|
|
924
|
+
|
|
925
|
+
self.assertIn("ac-2", failing)
|
|
926
|
+
self.assertNotIn("ac-2", passing)
|
|
927
|
+
|
|
928
|
+
def test_add_controls_to_controls_to_report_dict_new(self):
|
|
929
|
+
"""Test adding control to report dict for first time"""
|
|
930
|
+
control = {"controlId": "AC-1", "id": 1}
|
|
931
|
+
controls_to_reports = {}
|
|
932
|
+
cr = MagicMock()
|
|
933
|
+
|
|
934
|
+
_add_controls_to_controls_to_report_dict(control, controls_to_reports, cr)
|
|
935
|
+
|
|
936
|
+
self.assertIn("ac-1", controls_to_reports)
|
|
937
|
+
self.assertEqual(len(controls_to_reports["ac-1"]), 1)
|
|
938
|
+
|
|
939
|
+
def test_add_controls_to_controls_to_report_dict_existing(self):
|
|
940
|
+
"""Test adding control to report dict when already exists"""
|
|
941
|
+
control = {"controlId": "AC-1", "id": 1}
|
|
942
|
+
cr1 = MagicMock()
|
|
943
|
+
controls_to_reports = {"ac-1": [cr1]}
|
|
944
|
+
cr2 = MagicMock()
|
|
945
|
+
|
|
946
|
+
_add_controls_to_controls_to_report_dict(control, controls_to_reports, cr2)
|
|
947
|
+
|
|
948
|
+
self.assertEqual(len(controls_to_reports["ac-1"]), 2)
|
|
949
|
+
|
|
950
|
+
def test_clean_passing_list(self):
|
|
951
|
+
"""Test cleaning passing list removes failing controls"""
|
|
952
|
+
passing = {"ac-1": {"controlId": "AC-1"}, "ac-2": {"controlId": "AC-2"}}
|
|
953
|
+
failing = {"ac-2": {"controlId": "AC-2"}}
|
|
954
|
+
|
|
955
|
+
_clean_passing_list(passing, failing)
|
|
956
|
+
|
|
957
|
+
self.assertIn("ac-1", passing)
|
|
958
|
+
self.assertNotIn("ac-2", passing)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
# ==================== Assessment Creation Tests ====================
|
|
962
|
+
class TestAssessmentCreation(unittest.TestCase):
|
|
963
|
+
"""Test assessment creation from compliance reports"""
|
|
964
|
+
|
|
965
|
+
def test_create_aggregated_assessment_report_with_failures(self):
|
|
966
|
+
"""Test creating aggregated assessment report with failures"""
|
|
967
|
+
asset_details = [
|
|
968
|
+
{
|
|
969
|
+
"resource_name": "resource1",
|
|
970
|
+
"resource_id": "id1",
|
|
971
|
+
"cloud_provider": "AWS",
|
|
972
|
+
"subscription": "sub1",
|
|
973
|
+
"result": "Fail",
|
|
974
|
+
"policy_short_name": "AC-1",
|
|
975
|
+
"compliance_check": "access control",
|
|
976
|
+
"severity": "High",
|
|
977
|
+
"assessed_at": "2023-07-15",
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
"resource_name": "resource2",
|
|
981
|
+
"resource_id": "id2",
|
|
982
|
+
"cloud_provider": "Azure",
|
|
983
|
+
"subscription": "sub2",
|
|
984
|
+
"result": "Pass",
|
|
985
|
+
"policy_short_name": "AC-2",
|
|
986
|
+
"compliance_check": "account management",
|
|
987
|
+
"severity": "Medium",
|
|
988
|
+
"assessed_at": "2023-07-15",
|
|
989
|
+
},
|
|
990
|
+
]
|
|
991
|
+
|
|
992
|
+
result = _create_aggregated_assessment_report(
|
|
993
|
+
control_id="AC-1",
|
|
994
|
+
overall_result="Fail",
|
|
995
|
+
pass_count=1,
|
|
996
|
+
fail_count=1,
|
|
997
|
+
asset_details=asset_details,
|
|
998
|
+
total_assets=2,
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
self.assertIn("AC-1", result)
|
|
1002
|
+
self.assertIn("Fail", result)
|
|
1003
|
+
self.assertIn("resource1", result)
|
|
1004
|
+
self.assertIn("resource2", result)
|
|
1005
|
+
self.assertIn("Total Assets Assessed:", result)
|
|
1006
|
+
|
|
1007
|
+
def test_create_aggregated_assessment_report_all_pass(self):
|
|
1008
|
+
"""Test creating aggregated assessment report with all passing"""
|
|
1009
|
+
asset_details = [
|
|
1010
|
+
{
|
|
1011
|
+
"resource_name": "resource1",
|
|
1012
|
+
"resource_id": "id1",
|
|
1013
|
+
"cloud_provider": "AWS",
|
|
1014
|
+
"subscription": "sub1",
|
|
1015
|
+
"result": "Pass",
|
|
1016
|
+
"policy_short_name": "AC-1",
|
|
1017
|
+
"compliance_check": "access control",
|
|
1018
|
+
"severity": "Low",
|
|
1019
|
+
"assessed_at": "2023-07-15",
|
|
1020
|
+
}
|
|
1021
|
+
]
|
|
1022
|
+
|
|
1023
|
+
result = _create_aggregated_assessment_report(
|
|
1024
|
+
control_id="AC-1",
|
|
1025
|
+
overall_result="Pass",
|
|
1026
|
+
pass_count=1,
|
|
1027
|
+
fail_count=0,
|
|
1028
|
+
asset_details=asset_details,
|
|
1029
|
+
total_assets=1,
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
self.assertIn("AC-1", result)
|
|
1033
|
+
self.assertIn("Pass", result)
|
|
1034
|
+
self.assertIn("#2e7d32", result) # Green color for pass
|
|
1035
|
+
|
|
1036
|
+
@patch(f"{PATH}.Assessment")
|
|
1037
|
+
@patch(f"{PATH}.update_implementation_status")
|
|
1038
|
+
def test_create_report_assessment_with_failures(self, mock_update_status, mock_assessment):
|
|
1039
|
+
"""Test creating report assessment with failures"""
|
|
1040
|
+
implementation = MagicMock()
|
|
1041
|
+
implementation.id = 123
|
|
1042
|
+
implementation.createdById = 456
|
|
1043
|
+
|
|
1044
|
+
reports = [
|
|
1045
|
+
MagicMock(
|
|
1046
|
+
result="Fail",
|
|
1047
|
+
resource_name="res1",
|
|
1048
|
+
resource_id="id1",
|
|
1049
|
+
cloud_provider="AWS",
|
|
1050
|
+
subscription="sub1",
|
|
1051
|
+
policy_short_name="AC-1",
|
|
1052
|
+
compliance_check="access control",
|
|
1053
|
+
severity="High",
|
|
1054
|
+
assessed_at="2023-07-15",
|
|
1055
|
+
)
|
|
1056
|
+
]
|
|
1057
|
+
|
|
1058
|
+
mock_assessment_instance = MagicMock()
|
|
1059
|
+
mock_assessment_instance.id = 789
|
|
1060
|
+
mock_assessment.return_value = mock_assessment_instance
|
|
1061
|
+
mock_assessment_instance.create.return_value = mock_assessment_instance
|
|
1062
|
+
|
|
1063
|
+
create_report_assessment([implementation], reports, "AC-1", update_control_status=True)
|
|
1064
|
+
|
|
1065
|
+
mock_assessment.assert_called_once()
|
|
1066
|
+
mock_assessment_instance.create.assert_called_once()
|
|
1067
|
+
mock_update_status.assert_called_once()
|
|
1068
|
+
|
|
1069
|
+
@patch(f"{PATH}.Assessment")
|
|
1070
|
+
def test_create_report_assessment_no_implementation(self, mock_assessment):
|
|
1071
|
+
"""Test creating report assessment with no implementation"""
|
|
1072
|
+
create_report_assessment([], [], "AC-1")
|
|
1073
|
+
|
|
1074
|
+
mock_assessment.assert_not_called()
|
|
1075
|
+
|
|
1076
|
+
@patch(f"{PATH}.Assessment")
|
|
1077
|
+
@patch(f"{PATH}.update_implementation_status")
|
|
1078
|
+
def test_create_report_assessment_update_status_disabled(self, mock_update_status, mock_assessment):
|
|
1079
|
+
"""Test creating report assessment with status update disabled"""
|
|
1080
|
+
implementation = MagicMock()
|
|
1081
|
+
implementation.id = 123
|
|
1082
|
+
implementation.createdById = 456
|
|
1083
|
+
|
|
1084
|
+
reports = [
|
|
1085
|
+
MagicMock(
|
|
1086
|
+
result="Pass",
|
|
1087
|
+
resource_name="res1",
|
|
1088
|
+
resource_id="id1",
|
|
1089
|
+
cloud_provider="AWS",
|
|
1090
|
+
subscription="sub1",
|
|
1091
|
+
policy_short_name="AC-1",
|
|
1092
|
+
compliance_check="access control",
|
|
1093
|
+
severity="Low",
|
|
1094
|
+
assessed_at="2023-07-15",
|
|
1095
|
+
)
|
|
1096
|
+
]
|
|
1097
|
+
|
|
1098
|
+
mock_assessment_instance = MagicMock()
|
|
1099
|
+
mock_assessment.return_value = mock_assessment_instance
|
|
1100
|
+
mock_assessment_instance.create.return_value = mock_assessment_instance
|
|
1101
|
+
|
|
1102
|
+
create_report_assessment([implementation], reports, "AC-1", update_control_status=False)
|
|
1103
|
+
|
|
1104
|
+
mock_update_status.assert_not_called()
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
# ==================== Status Mapping Tests ====================
|
|
1108
|
+
class TestStatusMapping(unittest.TestCase):
|
|
1109
|
+
"""Test compliance status to implementation status mapping"""
|
|
1110
|
+
|
|
1111
|
+
def test_get_default_status_mapping_pass(self):
|
|
1112
|
+
"""Test default status mapping for Pass"""
|
|
1113
|
+
result = _get_default_status_mapping(ComplianceCheckStatus.PASS.value)
|
|
1114
|
+
self.assertEqual(result, "Implemented")
|
|
1115
|
+
|
|
1116
|
+
def test_get_default_status_mapping_fail(self):
|
|
1117
|
+
"""Test default status mapping for Fail"""
|
|
1118
|
+
result = _get_default_status_mapping(ComplianceCheckStatus.FAIL.value)
|
|
1119
|
+
self.assertEqual(result, "In Remediation")
|
|
1120
|
+
|
|
1121
|
+
def test_get_default_status_mapping_other(self):
|
|
1122
|
+
"""Test default status mapping for other status"""
|
|
1123
|
+
result = _get_default_status_mapping("Unknown")
|
|
1124
|
+
self.assertEqual(result, "Not Implemented")
|
|
1125
|
+
|
|
1126
|
+
def test_match_label_to_result_pass(self):
|
|
1127
|
+
"""Test matching label to Pass result"""
|
|
1128
|
+
result = _match_label_to_result("Implemented", ComplianceCheckStatus.PASS.value.lower())
|
|
1129
|
+
self.assertEqual(result, "Implemented")
|
|
1130
|
+
|
|
1131
|
+
result = _match_label_to_result("Complete", ComplianceCheckStatus.PASS.value.lower())
|
|
1132
|
+
self.assertEqual(result, "Complete")
|
|
1133
|
+
|
|
1134
|
+
def test_match_label_to_result_fail(self):
|
|
1135
|
+
"""Test matching label to Fail result"""
|
|
1136
|
+
result = _match_label_to_result("InRemediation", ComplianceCheckStatus.FAIL.value.lower())
|
|
1137
|
+
self.assertEqual(result, "InRemediation")
|
|
1138
|
+
|
|
1139
|
+
result = _match_label_to_result("Failed", ComplianceCheckStatus.FAIL.value.lower())
|
|
1140
|
+
self.assertEqual(result, "Failed")
|
|
1141
|
+
|
|
1142
|
+
def test_match_label_to_result_no_match(self):
|
|
1143
|
+
"""Test matching label with no match"""
|
|
1144
|
+
result = _match_label_to_result("SomeOtherLabel", ComplianceCheckStatus.PASS.value.lower())
|
|
1145
|
+
self.assertIsNone(result)
|
|
1146
|
+
|
|
1147
|
+
@patch(f"{PATH}.get_wiz_compliance_settings")
|
|
1148
|
+
def test_report_result_to_implementation_status_with_settings(self, mock_get_settings):
|
|
1149
|
+
"""Test converting report result with compliance settings"""
|
|
1150
|
+
mock_settings = MagicMock()
|
|
1151
|
+
mock_settings.get_field_labels.return_value = ["Implemented", "InRemediation", "NotImplemented"]
|
|
1152
|
+
mock_get_settings.return_value = mock_settings
|
|
1153
|
+
|
|
1154
|
+
result = report_result_to_implementation_status("Pass")
|
|
1155
|
+
|
|
1156
|
+
self.assertEqual(result, "Implemented")
|
|
1157
|
+
|
|
1158
|
+
@patch(f"{PATH}.get_wiz_compliance_settings")
|
|
1159
|
+
def test_report_result_to_implementation_status_no_settings(self, mock_get_settings):
|
|
1160
|
+
"""Test converting report result without compliance settings"""
|
|
1161
|
+
mock_get_settings.return_value = None
|
|
1162
|
+
|
|
1163
|
+
result = report_result_to_implementation_status("Pass")
|
|
1164
|
+
|
|
1165
|
+
self.assertEqual(result, "Implemented")
|
|
1166
|
+
|
|
1167
|
+
@patch(f"{PATH}.ComplianceSettings")
|
|
1168
|
+
def test_get_wiz_compliance_settings_found(self, mock_compliance_settings):
|
|
1169
|
+
"""Test getting Wiz compliance settings when found"""
|
|
1170
|
+
mock_setting = MagicMock()
|
|
1171
|
+
mock_setting.title = "Wiz Compliance Setting"
|
|
1172
|
+
mock_compliance_settings.get_by_current_tenant.return_value = [mock_setting]
|
|
1173
|
+
|
|
1174
|
+
result = get_wiz_compliance_settings()
|
|
1175
|
+
|
|
1176
|
+
self.assertEqual(result, mock_setting)
|
|
1177
|
+
|
|
1178
|
+
@patch(f"{PATH}.ComplianceSettings")
|
|
1179
|
+
def test_get_wiz_compliance_settings_not_found(self, mock_compliance_settings):
|
|
1180
|
+
"""Test getting Wiz compliance settings when not found"""
|
|
1181
|
+
mock_other_setting = MagicMock()
|
|
1182
|
+
mock_other_setting.title = "Other Setting"
|
|
1183
|
+
mock_compliance_settings.get_by_current_tenant.return_value = [mock_other_setting]
|
|
1184
|
+
|
|
1185
|
+
result = get_wiz_compliance_settings()
|
|
1186
|
+
|
|
1187
|
+
self.assertIsNone(result)
|
|
1188
|
+
|
|
1189
|
+
@patch(f"{PATH}.ComplianceSettings")
|
|
1190
|
+
def test_get_wiz_compliance_settings_exception(self, mock_compliance_settings):
|
|
1191
|
+
"""Test getting Wiz compliance settings with exception"""
|
|
1192
|
+
mock_compliance_settings.get_by_current_tenant.side_effect = Exception("API Error")
|
|
1193
|
+
|
|
1194
|
+
result = get_wiz_compliance_settings()
|
|
1195
|
+
|
|
1196
|
+
self.assertIsNone(result)
|
|
1197
|
+
|
|
1198
|
+
@patch(f"{PATH}.ImplementationObjective")
|
|
1199
|
+
def test_update_implementation_status_with_objectives(self, mock_objective):
|
|
1200
|
+
"""Test updating implementation status with objectives"""
|
|
1201
|
+
implementation = MagicMock()
|
|
1202
|
+
implementation.id = 123
|
|
1203
|
+
implementation.get_module_slug.return_value = "controls"
|
|
1204
|
+
|
|
1205
|
+
objective = MagicMock()
|
|
1206
|
+
objective.id = 456
|
|
1207
|
+
mock_objective.get_all_by_parent.return_value = [objective]
|
|
1208
|
+
|
|
1209
|
+
update_implementation_status(implementation, "Pass")
|
|
1210
|
+
|
|
1211
|
+
objective.save.assert_called_once()
|
|
1212
|
+
implementation.save.assert_called_once()
|
|
1213
|
+
|
|
1214
|
+
@patch(f"{PATH}.ImplementationObjective")
|
|
1215
|
+
def test_update_implementation_status_no_objectives(self, mock_objective):
|
|
1216
|
+
"""Test updating implementation status without objectives"""
|
|
1217
|
+
implementation = MagicMock()
|
|
1218
|
+
implementation.id = 123
|
|
1219
|
+
implementation.get_module_slug.return_value = "controls"
|
|
1220
|
+
|
|
1221
|
+
mock_objective.get_all_by_parent.return_value = []
|
|
1222
|
+
|
|
1223
|
+
update_implementation_status(implementation, "Pass")
|
|
1224
|
+
|
|
1225
|
+
self.assertEqual(implementation.objectives, [])
|
|
1226
|
+
implementation.save.assert_called_once()
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
# ==================== Vulnerability Creation Tests ====================
|
|
1230
|
+
class TestVulnerabilityCreation(unittest.TestCase):
|
|
1231
|
+
"""Test vulnerability creation from Wiz findings"""
|
|
1232
|
+
|
|
1233
|
+
@patch(f"{PATH}.WizVariables")
|
|
1234
|
+
@patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
|
|
1235
|
+
def test_create_vulnerabilities_from_wiz_findings_success(self, mock_wiz_integration_class, mock_vars):
|
|
1236
|
+
"""Test successful vulnerability creation"""
|
|
1237
|
+
mock_vars.wizAccessToken = "test-token"
|
|
1238
|
+
|
|
1239
|
+
mock_integration = MagicMock()
|
|
1240
|
+
mock_wiz_integration_class.return_value = mock_integration
|
|
1241
|
+
mock_wiz_integration_class.sync_findings.return_value = 10
|
|
1242
|
+
|
|
1243
|
+
result = create_vulnerabilities_from_wiz_findings(
|
|
1244
|
+
wiz_project_id="project-123", regscale_plan_id=456, client_id="client-id", client_secret="client-secret"
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
self.assertEqual(result, 10)
|
|
1248
|
+
mock_integration.authenticate.assert_called_once_with(client_id="client-id", client_secret="client-secret")
|
|
1249
|
+
|
|
1250
|
+
@patch(f"{PATH}.WizVariables")
|
|
1251
|
+
@patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
|
|
1252
|
+
def test_create_vulnerabilities_from_wiz_findings_with_filter(self, mock_wiz_integration_class, mock_vars):
|
|
1253
|
+
"""Test vulnerability creation with filter override"""
|
|
1254
|
+
mock_vars.wizAccessToken = "test-token"
|
|
1255
|
+
|
|
1256
|
+
mock_integration = MagicMock()
|
|
1257
|
+
mock_wiz_integration_class.return_value = mock_integration
|
|
1258
|
+
mock_wiz_integration_class.sync_findings.return_value = 5
|
|
1259
|
+
|
|
1260
|
+
filter_override = '{"severity": "HIGH"}'
|
|
1261
|
+
result = create_vulnerabilities_from_wiz_findings(
|
|
1262
|
+
wiz_project_id="project-123", regscale_plan_id=456, filter_by_override=filter_override
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
self.assertEqual(result, 5)
|
|
1266
|
+
|
|
1267
|
+
@patch(f"{PATH}.WizVariables")
|
|
1268
|
+
@patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
|
|
1269
|
+
def test_create_vulnerabilities_from_wiz_findings_error(self, mock_wiz_integration_class, mock_vars):
|
|
1270
|
+
"""Test vulnerability creation with error"""
|
|
1271
|
+
mock_vars.wizAccessToken = "test-token"
|
|
1272
|
+
|
|
1273
|
+
mock_wiz_integration_class.side_effect = Exception("Integration Error")
|
|
1274
|
+
|
|
1275
|
+
with self.assertRaises(Exception):
|
|
1276
|
+
create_vulnerabilities_from_wiz_findings(wiz_project_id="project-123", regscale_plan_id=456)
|
|
1277
|
+
|
|
1278
|
+
@patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
|
|
1279
|
+
def test_create_single_vulnerability_from_wiz_data_success(self, mock_wiz_integration_class):
|
|
1280
|
+
"""Test creating single vulnerability successfully"""
|
|
1281
|
+
mock_integration = MagicMock()
|
|
1282
|
+
mock_wiz_integration_class.return_value = mock_integration
|
|
1283
|
+
|
|
1284
|
+
# Mock scan history with RegScaleModel-like behavior
|
|
1285
|
+
mock_scan_history_instance = MagicMock()
|
|
1286
|
+
mock_scan_history_instance.id = 456
|
|
1287
|
+
|
|
1288
|
+
# Create a mock ScanHistory class that returns the instance from get_by_id
|
|
1289
|
+
with patch(f"{PATH}.regscale_models.ScanHistory") as mock_scan_history_class:
|
|
1290
|
+
mock_scan_history_class.get_by_id.return_value = mock_scan_history_instance
|
|
1291
|
+
|
|
1292
|
+
mock_finding = MagicMock()
|
|
1293
|
+
mock_integration.parse_finding.return_value = mock_finding
|
|
1294
|
+
|
|
1295
|
+
mock_asset = MagicMock()
|
|
1296
|
+
mock_integration.get_asset_by_identifier.return_value = mock_asset
|
|
1297
|
+
|
|
1298
|
+
mock_integration.handle_vulnerability.return_value = 789
|
|
1299
|
+
|
|
1300
|
+
# Mock Vulnerability class with get_by_id
|
|
1301
|
+
mock_vuln = MagicMock()
|
|
1302
|
+
mock_vuln.id = 789
|
|
1303
|
+
with patch(f"{PATH}.regscale_models.Vulnerability") as mock_vuln_class:
|
|
1304
|
+
mock_vuln_class.get_by_id.return_value = mock_vuln
|
|
1305
|
+
|
|
1306
|
+
wiz_finding_data = {"id": "finding-123", "severity": "HIGH"}
|
|
1307
|
+
result = create_single_vulnerability_from_wiz_data(
|
|
1308
|
+
wiz_finding_data=wiz_finding_data, asset_id="asset-456", regscale_plan_id=123, scan_history_id=456
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
self.assertIsNotNone(result)
|
|
1312
|
+
# The mock returns a MagicMock with id = 789
|
|
1313
|
+
self.assertEqual(result, mock_vuln)
|
|
1314
|
+
|
|
1315
|
+
@patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
|
|
1316
|
+
def test_create_single_vulnerability_from_wiz_data_no_scan_history(self, mock_wiz_integration_class):
|
|
1317
|
+
"""Test creating single vulnerability without scan history"""
|
|
1318
|
+
mock_integration = MagicMock()
|
|
1319
|
+
mock_wiz_integration_class.return_value = mock_integration
|
|
1320
|
+
|
|
1321
|
+
mock_scan_history = MagicMock()
|
|
1322
|
+
mock_integration.create_scan_history.return_value = mock_scan_history
|
|
1323
|
+
|
|
1324
|
+
mock_finding = MagicMock()
|
|
1325
|
+
mock_integration.parse_finding.return_value = mock_finding
|
|
1326
|
+
|
|
1327
|
+
mock_asset = MagicMock()
|
|
1328
|
+
mock_integration.get_asset_by_identifier.return_value = mock_asset
|
|
1329
|
+
|
|
1330
|
+
mock_integration.handle_vulnerability.return_value = 789
|
|
1331
|
+
|
|
1332
|
+
mock_vuln = MagicMock()
|
|
1333
|
+
mock_vuln.id = 789
|
|
1334
|
+
with patch(f"{PATH}.regscale_models.Vulnerability") as mock_vuln_class:
|
|
1335
|
+
mock_vuln_class.get_by_id.return_value = mock_vuln
|
|
1336
|
+
|
|
1337
|
+
wiz_finding_data = {"id": "finding-123", "severity": "HIGH"}
|
|
1338
|
+
result = create_single_vulnerability_from_wiz_data(
|
|
1339
|
+
wiz_finding_data=wiz_finding_data, asset_id="asset-456", regscale_plan_id=123
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
self.assertIsNotNone(result)
|
|
1343
|
+
mock_integration.create_scan_history.assert_called_once()
|
|
1344
|
+
|
|
1345
|
+
@patch("regscale.integrations.commercial.wizv2.scanner.WizVulnerabilityIntegration")
|
|
1346
|
+
def test_create_single_vulnerability_from_wiz_data_parse_failure(self, mock_wiz_integration_class):
|
|
1347
|
+
"""Test creating single vulnerability with parse failure"""
|
|
1348
|
+
mock_integration = MagicMock()
|
|
1349
|
+
mock_wiz_integration_class.return_value = mock_integration
|
|
1350
|
+
|
|
1351
|
+
mock_scan_history = MagicMock()
|
|
1352
|
+
mock_integration.create_scan_history.return_value = mock_scan_history
|
|
1353
|
+
|
|
1354
|
+
mock_integration.parse_finding.return_value = None
|
|
1355
|
+
|
|
1356
|
+
wiz_finding_data = {"id": "finding-123"}
|
|
1357
|
+
result = create_single_vulnerability_from_wiz_data(
|
|
1358
|
+
wiz_finding_data=wiz_finding_data, asset_id="asset-456", regscale_plan_id=123
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
self.assertIsNone(result)
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
# ==================== Deprecated and Legacy Function Tests ====================
|
|
1365
|
+
class TestDeprecatedFunctions(unittest.TestCase):
|
|
1366
|
+
"""Test deprecated and legacy functions"""
|
|
1367
|
+
|
|
1368
|
+
@patch(f"{PATH}.send_request")
|
|
1369
|
+
def test_fetch_report_id_success(self, mock_send_request):
|
|
1370
|
+
"""Test deprecated fetch_report_id success"""
|
|
1371
|
+
mock_response = MagicMock()
|
|
1372
|
+
mock_response.json.return_value = {"data": {"createReport": {"report": {"id": "report-123"}}}}
|
|
1373
|
+
mock_send_request.return_value = mock_response
|
|
1374
|
+
|
|
1375
|
+
result = fetch_report_id(query="query { test }", variables={}, url="https://api.wiz.io/graphql")
|
|
1376
|
+
|
|
1377
|
+
self.assertEqual(result, "report-123")
|
|
1378
|
+
|
|
1379
|
+
@patch(f"{PATH}.send_request")
|
|
1380
|
+
@patch(f"{PATH}.error_and_exit")
|
|
1381
|
+
def test_fetch_report_id_with_error(self, mock_error_exit, mock_send_request):
|
|
1382
|
+
"""Test deprecated fetch_report_id with error"""
|
|
1383
|
+
mock_response = MagicMock()
|
|
1384
|
+
mock_response.json.return_value = {"error": "API Error"}
|
|
1385
|
+
mock_send_request.return_value = mock_response
|
|
1386
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
1387
|
+
|
|
1388
|
+
with pytest.raises(SystemExit):
|
|
1389
|
+
fetch_report_id(query="query { test }", variables={}, url="https://api.wiz.io/graphql")
|
|
1390
|
+
|
|
1391
|
+
mock_error_exit.assert_called_once()
|
|
1392
|
+
|
|
1393
|
+
@patch(f"{PATH}.send_request")
|
|
1394
|
+
def test_fetch_report_id_request_exception(self, mock_send_request):
|
|
1395
|
+
"""Test deprecated fetch_report_id with request exception"""
|
|
1396
|
+
mock_send_request.side_effect = requests.RequestException("Connection Error")
|
|
1397
|
+
|
|
1398
|
+
result = fetch_report_id(query="query { test }", variables={}, url="https://api.wiz.io/graphql")
|
|
1399
|
+
|
|
1400
|
+
self.assertEqual(result, "")
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
# ==================== Integration Tests ====================
|
|
1404
|
+
class TestReportProcessingIntegration(unittest.TestCase):
|
|
1405
|
+
"""Integration tests for report processing workflow"""
|
|
1406
|
+
|
|
1407
|
+
@patch(f"{PATH}.get_or_create_report_id")
|
|
1408
|
+
@patch(f"{PATH}.fetch_report_data")
|
|
1409
|
+
def test_process_single_report_workflow(self, mock_fetch_data, mock_get_report_id):
|
|
1410
|
+
"""Test complete single report processing workflow"""
|
|
1411
|
+
mock_get_report_id.return_value = "report-123"
|
|
1412
|
+
mock_fetch_data.return_value = [{"col1": "val1"}, {"col1": "val2"}]
|
|
1413
|
+
|
|
1414
|
+
project_id = "project-456"
|
|
1415
|
+
frameworks = ["NIST_SP_800-53_Revision_5"]
|
|
1416
|
+
wiz_frameworks = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
|
|
1417
|
+
existing_reports = []
|
|
1418
|
+
target_framework = "NIST_SP_800-53_Revision_5"
|
|
1419
|
+
|
|
1420
|
+
result = process_single_report(project_id, frameworks, wiz_frameworks, existing_reports, target_framework)
|
|
1421
|
+
|
|
1422
|
+
self.assertEqual(len(result), 2)
|
|
1423
|
+
mock_get_report_id.assert_called_once()
|
|
1424
|
+
mock_fetch_data.assert_called_once_with("report-123")
|
|
1425
|
+
|
|
1426
|
+
@patch(f"{PATH}.get_report_url_and_status")
|
|
1427
|
+
@patch(f"{PATH}.requests.get")
|
|
1428
|
+
@patch(f"{PATH}.error_and_exit")
|
|
1429
|
+
def test_fetch_report_data_success(self, mock_error_exit, mock_requests_get, mock_get_url):
|
|
1430
|
+
"""Test successful report data fetching"""
|
|
1431
|
+
mock_get_url.return_value = "https://example.com/report.csv"
|
|
1432
|
+
|
|
1433
|
+
mock_response = MagicMock()
|
|
1434
|
+
mock_response.raise_for_status.return_value = None
|
|
1435
|
+
mock_response.iter_lines.return_value = iter([b"col1,col2", b"val1,val2", b"val3,val4"])
|
|
1436
|
+
mock_requests_get.return_value.__enter__.return_value = mock_response
|
|
1437
|
+
|
|
1438
|
+
result = fetch_report_data("report-123")
|
|
1439
|
+
|
|
1440
|
+
self.assertIsNotNone(result)
|
|
1441
|
+
mock_get_url.assert_called_once_with("report-123")
|
|
1442
|
+
|
|
1443
|
+
@patch(f"{PATH}.get_report_url_and_status")
|
|
1444
|
+
@patch(f"{PATH}.requests.get")
|
|
1445
|
+
@patch(f"{PATH}.error_and_exit")
|
|
1446
|
+
def test_fetch_report_data_request_error(self, mock_error_exit, mock_requests_get, mock_get_url):
|
|
1447
|
+
"""Test report data fetching with request error"""
|
|
1448
|
+
mock_get_url.return_value = "https://example.com/report.csv"
|
|
1449
|
+
mock_requests_get.side_effect = requests.RequestException("Connection Error")
|
|
1450
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
1451
|
+
|
|
1452
|
+
with pytest.raises(SystemExit):
|
|
1453
|
+
fetch_report_data("report-123")
|
|
1454
|
+
|
|
1455
|
+
mock_error_exit.assert_called_once()
|
|
1456
|
+
|
|
1457
|
+
@patch(f"{PATH}.fetch_frameworks")
|
|
1458
|
+
@patch(f"{PATH}.get_framework_names")
|
|
1459
|
+
@patch(f"{PATH}.query_reports")
|
|
1460
|
+
@patch(f"{PATH}.process_single_report")
|
|
1461
|
+
def test_fetch_framework_report_workflow(
|
|
1462
|
+
self, mock_process_report, mock_query_reports, mock_get_names, mock_fetch_frameworks
|
|
1463
|
+
):
|
|
1464
|
+
"""Test complete framework report fetching workflow"""
|
|
1465
|
+
mock_fetch_frameworks.return_value = [{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"}]
|
|
1466
|
+
mock_get_names.return_value = ["NIST_SP_800-53_Revision_5"]
|
|
1467
|
+
mock_query_reports.return_value = []
|
|
1468
|
+
mock_process_report.return_value = [{"data": "report_data"}]
|
|
1469
|
+
|
|
1470
|
+
result = fetch_framework_report("project-123", "NIST_SP_800-53_Revision_5")
|
|
1471
|
+
|
|
1472
|
+
self.assertEqual(len(result), 1)
|
|
1473
|
+
mock_fetch_frameworks.assert_called_once()
|
|
1474
|
+
mock_query_reports.assert_called_once_with("project-123")
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
# ==================== Edge Cases and Error Handling Tests ====================
|
|
1478
|
+
class TestEdgeCasesAndErrorHandling(unittest.TestCase):
|
|
1479
|
+
"""Test edge cases and error handling"""
|
|
1480
|
+
|
|
1481
|
+
def test_get_notes_from_wiz_props_empty_values(self):
|
|
1482
|
+
"""Test getting notes with empty string values"""
|
|
1483
|
+
wiz_properties = {"cloudPlatform": "", "providerUniqueId": ""}
|
|
1484
|
+
external_id = ""
|
|
1485
|
+
|
|
1486
|
+
result = get_notes_from_wiz_props(wiz_properties, external_id)
|
|
1487
|
+
|
|
1488
|
+
# With all empty values, result should be empty string
|
|
1489
|
+
# The function only includes values if they are truthy
|
|
1490
|
+
self.assertEqual(result, "")
|
|
1491
|
+
|
|
1492
|
+
@patch(f"{PATH}.regscale_models.Metadata.get_metadata_by_module_field")
|
|
1493
|
+
@patch(f"{PATH}.regscale_models.Metadata")
|
|
1494
|
+
def test_create_asset_type_with_special_characters(self, mock_metadata_class, mock_get_metadata):
|
|
1495
|
+
"""Test creating asset type with special characters"""
|
|
1496
|
+
mock_get_metadata.return_value = []
|
|
1497
|
+
mock_metadata_instance = MagicMock()
|
|
1498
|
+
mock_metadata_class.return_value = mock_metadata_instance
|
|
1499
|
+
|
|
1500
|
+
result = create_asset_type("test-asset_TYPE_123")
|
|
1501
|
+
|
|
1502
|
+
# Should handle title case and underscore replacement
|
|
1503
|
+
self.assertIn("Test-Asset", result)
|
|
1504
|
+
self.assertIn("Type", result)
|
|
1505
|
+
|
|
1506
|
+
@patch(f"{PATH}.time.sleep")
|
|
1507
|
+
@patch(f"{PATH}.download_report")
|
|
1508
|
+
@patch(f"{PATH}.MAX_RETRIES", 2)
|
|
1509
|
+
def test_get_report_url_and_status_max_retries_exceeded(self, mock_download_report, mock_sleep):
|
|
1510
|
+
"""Test exceeding max retries for report download"""
|
|
1511
|
+
mock_response = MagicMock()
|
|
1512
|
+
mock_response.ok = True
|
|
1513
|
+
mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
|
|
1514
|
+
mock_download_report.return_value = mock_response
|
|
1515
|
+
|
|
1516
|
+
with self.assertRaises(requests.RequestException) as context:
|
|
1517
|
+
get_report_url_and_status("report-123")
|
|
1518
|
+
|
|
1519
|
+
self.assertIn("exceeding the maximum number of retries", str(context.exception))
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
if __name__ == "__main__":
|
|
1523
|
+
unittest.main()
|