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,519 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Unit tests for Wiz V2 utility functions."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
|
|
9
|
+
from regscale.integrations.commercial.wizv2.utils import get_report_url_and_status, get_or_create_report_id
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("regscale")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestWizUtils(unittest.TestCase):
|
|
15
|
+
"""Test cases for Wiz utility functions."""
|
|
16
|
+
|
|
17
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
18
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
19
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
20
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
21
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
|
|
22
|
+
def test_get_report_url_and_status_completed(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
23
|
+
"""Test get_report_url_and_status with COMPLETED status."""
|
|
24
|
+
# Mock response for completed report
|
|
25
|
+
mock_response = MagicMock()
|
|
26
|
+
mock_response.ok = True
|
|
27
|
+
mock_response.json.return_value = {
|
|
28
|
+
"data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
|
|
29
|
+
}
|
|
30
|
+
mock_download_report.return_value = mock_response
|
|
31
|
+
|
|
32
|
+
# Call the function
|
|
33
|
+
result = get_report_url_and_status("test-report-id")
|
|
34
|
+
|
|
35
|
+
# Verify the result
|
|
36
|
+
self.assertEqual(result, "https://example.com/report.csv")
|
|
37
|
+
mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
|
|
38
|
+
mock_rerun_report.assert_not_called()
|
|
39
|
+
mock_sleep.assert_not_called() # Should not sleep on first success
|
|
40
|
+
|
|
41
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
42
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
43
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
44
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.get_report_url_and_status")
|
|
45
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
46
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
|
|
47
|
+
def test_get_report_url_and_status_expired(
|
|
48
|
+
self, mock_recursive_call, mock_rerun_report, mock_download_report, mock_sleep
|
|
49
|
+
):
|
|
50
|
+
"""Test get_report_url_and_status with EXPIRED status."""
|
|
51
|
+
# Mock response for expired report
|
|
52
|
+
mock_response = MagicMock()
|
|
53
|
+
mock_response.ok = True
|
|
54
|
+
mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "EXPIRED"}}}}
|
|
55
|
+
mock_download_report.return_value = mock_response
|
|
56
|
+
|
|
57
|
+
# Mock rerun response
|
|
58
|
+
mock_rerun_response = MagicMock()
|
|
59
|
+
mock_rerun_response.ok = True
|
|
60
|
+
mock_rerun_report.return_value = mock_rerun_response
|
|
61
|
+
|
|
62
|
+
# Mock recursive call to return final URL
|
|
63
|
+
mock_recursive_call.return_value = "https://example.com/new-report.csv"
|
|
64
|
+
|
|
65
|
+
# Call the function
|
|
66
|
+
result = get_report_url_and_status("test-report-id")
|
|
67
|
+
|
|
68
|
+
# Verify the result
|
|
69
|
+
self.assertEqual(result, "https://example.com/new-report.csv")
|
|
70
|
+
mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
|
|
71
|
+
mock_rerun_report.assert_called_once_with({"reportId": "test-report-id"})
|
|
72
|
+
mock_recursive_call.assert_called_once_with("test-report-id")
|
|
73
|
+
mock_sleep.assert_not_called() # Should not sleep on first call before recursion
|
|
74
|
+
|
|
75
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
76
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
77
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
78
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
79
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 5)
|
|
80
|
+
def test_get_report_url_and_status_rate_limit(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
81
|
+
"""Test get_report_url_and_status with rate limit error."""
|
|
82
|
+
from regscale.integrations.commercial.wizv2.core.constants import RATE_LIMIT_MSG
|
|
83
|
+
|
|
84
|
+
# Mock response with rate limit error
|
|
85
|
+
mock_response = MagicMock()
|
|
86
|
+
mock_response.ok = True
|
|
87
|
+
mock_response.json.return_value = {
|
|
88
|
+
"errors": [
|
|
89
|
+
{"message": "Rate limit exceeded", "extensions": {"retryAfter": 0.001}} # Reduced to milliseconds
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
mock_download_report.return_value = mock_response
|
|
93
|
+
|
|
94
|
+
# Mock second response for successful completion
|
|
95
|
+
mock_response2 = MagicMock()
|
|
96
|
+
mock_response2.ok = True
|
|
97
|
+
mock_response2.json.return_value = {
|
|
98
|
+
"data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Configure mock to return different responses on subsequent calls
|
|
102
|
+
mock_download_report.side_effect = [mock_response, mock_response2]
|
|
103
|
+
|
|
104
|
+
# Call the function
|
|
105
|
+
result = get_report_url_and_status("test-report-id")
|
|
106
|
+
|
|
107
|
+
# Verify the result
|
|
108
|
+
self.assertEqual(result, "https://example.com/report.csv")
|
|
109
|
+
self.assertEqual(mock_download_report.call_count, 2)
|
|
110
|
+
mock_rerun_report.assert_not_called()
|
|
111
|
+
# Sleep is called twice: once for rate limit, once for retry interval
|
|
112
|
+
self.assertEqual(mock_sleep.call_count, 2)
|
|
113
|
+
|
|
114
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
115
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
116
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
117
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
118
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
|
|
119
|
+
def test_get_report_url_and_status_failed_response(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
120
|
+
"""Test get_report_url_and_status with failed response."""
|
|
121
|
+
# Mock failed response
|
|
122
|
+
mock_response = MagicMock()
|
|
123
|
+
mock_response.ok = False
|
|
124
|
+
mock_download_report.return_value = mock_response
|
|
125
|
+
|
|
126
|
+
# Call the function and expect exception
|
|
127
|
+
with self.assertRaises(Exception) as context:
|
|
128
|
+
get_report_url_and_status("test-report-id")
|
|
129
|
+
|
|
130
|
+
self.assertIn("Failed to download report", str(context.exception))
|
|
131
|
+
mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
|
|
132
|
+
mock_rerun_report.assert_not_called()
|
|
133
|
+
|
|
134
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
135
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
136
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
137
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
138
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
|
|
139
|
+
def test_get_report_url_and_status_none_response(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
140
|
+
"""Test get_report_url_and_status with None response."""
|
|
141
|
+
# Mock None response
|
|
142
|
+
mock_download_report.return_value = None
|
|
143
|
+
|
|
144
|
+
# Call the function and expect exception
|
|
145
|
+
with self.assertRaises(Exception) as context:
|
|
146
|
+
get_report_url_and_status("test-report-id")
|
|
147
|
+
|
|
148
|
+
self.assertIn("Failed to download report", str(context.exception))
|
|
149
|
+
mock_download_report.assert_called_once_with({"reportId": "test-report-id"})
|
|
150
|
+
mock_rerun_report.assert_not_called()
|
|
151
|
+
|
|
152
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
153
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
154
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
155
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
156
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3) # Reduce retries for faster testing
|
|
157
|
+
def test_get_report_url_and_status_other_error(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
158
|
+
"""Test get_report_url_and_status with other error in response."""
|
|
159
|
+
# Mock response with other error
|
|
160
|
+
mock_response = MagicMock()
|
|
161
|
+
mock_response.ok = True
|
|
162
|
+
mock_response.json.return_value = {"errors": [{"message": "Some other error occurred"}]}
|
|
163
|
+
mock_download_report.return_value = mock_response
|
|
164
|
+
|
|
165
|
+
# Call the function and expect exception after max retries
|
|
166
|
+
with self.assertRaises(Exception) as context:
|
|
167
|
+
get_report_url_and_status("test-report-id")
|
|
168
|
+
|
|
169
|
+
self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
|
|
170
|
+
# Should be called MAX_RETRIES times (now 3)
|
|
171
|
+
self.assertEqual(mock_download_report.call_count, 3)
|
|
172
|
+
mock_rerun_report.assert_not_called()
|
|
173
|
+
|
|
174
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
175
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
176
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
177
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
178
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3) # Reduce retries for faster testing
|
|
179
|
+
def test_get_report_url_and_status_unknown_status(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
180
|
+
"""Test get_report_url_and_status with unknown status."""
|
|
181
|
+
# Mock response with unknown status
|
|
182
|
+
mock_response = MagicMock()
|
|
183
|
+
mock_response.ok = True
|
|
184
|
+
mock_response.json.return_value = {"data": {"report": {"lastRun": {"status": "UNKNOWN_STATUS"}}}}
|
|
185
|
+
mock_download_report.return_value = mock_response
|
|
186
|
+
|
|
187
|
+
# Call the function and expect exception after max retries
|
|
188
|
+
with self.assertRaises(Exception) as context:
|
|
189
|
+
get_report_url_and_status("test-report-id")
|
|
190
|
+
|
|
191
|
+
self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
|
|
192
|
+
# Should be called MAX_RETRIES times (now 3)
|
|
193
|
+
self.assertEqual(mock_download_report.call_count, 3)
|
|
194
|
+
mock_rerun_report.assert_not_called()
|
|
195
|
+
|
|
196
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
197
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
198
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
199
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
200
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3) # Reduce retries for faster testing
|
|
201
|
+
def test_get_report_url_and_status_missing_status(self, mock_rerun_report, mock_download_report, mock_sleep):
|
|
202
|
+
"""Test get_report_url_and_status with missing status in response."""
|
|
203
|
+
# Mock response with missing status
|
|
204
|
+
mock_response = MagicMock()
|
|
205
|
+
mock_response.ok = True
|
|
206
|
+
mock_response.json.return_value = {"data": {"report": {"lastRun": {}}}}
|
|
207
|
+
mock_download_report.return_value = mock_response
|
|
208
|
+
|
|
209
|
+
# Call the function and expect exception after max retries
|
|
210
|
+
with self.assertRaises(Exception) as context:
|
|
211
|
+
get_report_url_and_status("test-report-id")
|
|
212
|
+
|
|
213
|
+
self.assertIn("Download failed, exceeding the maximum number of retries", str(context.exception))
|
|
214
|
+
# Should be called MAX_RETRIES times (now 3)
|
|
215
|
+
self.assertEqual(mock_download_report.call_count, 3)
|
|
216
|
+
mock_rerun_report.assert_not_called()
|
|
217
|
+
|
|
218
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.time.sleep")
|
|
219
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.download_report")
|
|
220
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.rerun_expired_report")
|
|
221
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.CHECK_INTERVAL_FOR_DOWNLOAD_REPORT", 0.001)
|
|
222
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.MAX_RETRIES", 3)
|
|
223
|
+
def test_get_report_url_and_status_multiple_attempts_before_completion(
|
|
224
|
+
self, mock_rerun_report, mock_download_report, mock_sleep
|
|
225
|
+
):
|
|
226
|
+
"""Test get_report_url_and_status with multiple attempts before completion."""
|
|
227
|
+
# Mock responses: first two with unknown status, third with completed
|
|
228
|
+
mock_response1 = MagicMock()
|
|
229
|
+
mock_response1.ok = True
|
|
230
|
+
mock_response1.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
|
|
231
|
+
|
|
232
|
+
mock_response2 = MagicMock()
|
|
233
|
+
mock_response2.ok = True
|
|
234
|
+
mock_response2.json.return_value = {"data": {"report": {"lastRun": {"status": "PROCESSING"}}}}
|
|
235
|
+
|
|
236
|
+
mock_response3 = MagicMock()
|
|
237
|
+
mock_response3.ok = True
|
|
238
|
+
mock_response3.json.return_value = {
|
|
239
|
+
"data": {"report": {"lastRun": {"status": "COMPLETED", "url": "https://example.com/report.csv"}}}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Configure mock to return different responses on subsequent calls
|
|
243
|
+
mock_download_report.side_effect = [mock_response1, mock_response2, mock_response3]
|
|
244
|
+
|
|
245
|
+
# Call the function
|
|
246
|
+
result = get_report_url_and_status("test-report-id")
|
|
247
|
+
|
|
248
|
+
# Verify the result
|
|
249
|
+
self.assertEqual(result, "https://example.com/report.csv")
|
|
250
|
+
self.assertEqual(mock_download_report.call_count, 3)
|
|
251
|
+
# Should sleep twice (after first and second attempts)
|
|
252
|
+
self.assertEqual(mock_sleep.call_count, 2)
|
|
253
|
+
mock_rerun_report.assert_not_called()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class TestGetOrCreateReportId(unittest.TestCase):
|
|
257
|
+
"""Test cases for get_or_create_report_id function."""
|
|
258
|
+
|
|
259
|
+
def setUp(self):
|
|
260
|
+
"""Set up test fixtures."""
|
|
261
|
+
self.project_id = "test-project-123"
|
|
262
|
+
self.frameworks = ["NIST_SP_800-53_Revision_5", "NIST_CSF_v1.1"]
|
|
263
|
+
self.wiz_frameworks = [
|
|
264
|
+
{"id": "framework-1", "name": "NIST SP 800-53 Revision 5"},
|
|
265
|
+
{"id": "framework-2", "name": "NIST CSF v1.1"},
|
|
266
|
+
]
|
|
267
|
+
self.target_framework = "NIST_SP_800-53_Revision_5"
|
|
268
|
+
|
|
269
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
270
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
|
|
271
|
+
def test_get_or_create_report_id_existing_valid_report(self, mock_is_expired, mock_application):
|
|
272
|
+
"""Test returning existing report ID when report is valid (not expired)."""
|
|
273
|
+
# Mock Application
|
|
274
|
+
mock_app = MagicMock()
|
|
275
|
+
mock_app.config.get.return_value = 15
|
|
276
|
+
mock_application.return_value = mock_app
|
|
277
|
+
|
|
278
|
+
# Mock is_report_expired to return False (not expired)
|
|
279
|
+
mock_is_expired.return_value = False
|
|
280
|
+
|
|
281
|
+
# Mock existing reports with a valid report
|
|
282
|
+
existing_reports = [
|
|
283
|
+
{
|
|
284
|
+
"id": "existing-report-123",
|
|
285
|
+
"name": "NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
286
|
+
"lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
result = get_or_create_report_id(
|
|
291
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
self.assertEqual(result, "existing-report-123")
|
|
295
|
+
mock_app.config.get.assert_called_once_with("wizReportAge", 15)
|
|
296
|
+
mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 15)
|
|
297
|
+
|
|
298
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
299
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
|
|
300
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
|
|
301
|
+
def test_get_or_create_report_id_existing_expired_report(
|
|
302
|
+
self, mock_create_report, mock_is_expired, mock_application
|
|
303
|
+
):
|
|
304
|
+
"""Test creating new report when existing report is expired."""
|
|
305
|
+
# Mock Application
|
|
306
|
+
mock_app = MagicMock()
|
|
307
|
+
mock_app.config.get.return_value = 15
|
|
308
|
+
mock_application.return_value = mock_app
|
|
309
|
+
|
|
310
|
+
# Mock is_report_expired to return True (expired)
|
|
311
|
+
mock_is_expired.return_value = True
|
|
312
|
+
|
|
313
|
+
# Mock create_compliance_report to return new report ID
|
|
314
|
+
mock_create_report.return_value = "new-report-456"
|
|
315
|
+
|
|
316
|
+
# Mock existing reports with an expired report
|
|
317
|
+
existing_reports = [
|
|
318
|
+
{
|
|
319
|
+
"id": "existing-report-123",
|
|
320
|
+
"name": "NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
321
|
+
"lastRun": {"runAt": "2023-06-01T14:37:55.450532Z"}, # Old date
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
result = get_or_create_report_id(
|
|
326
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
self.assertEqual(result, "new-report-456")
|
|
330
|
+
mock_app.config.get.assert_called_once_with("wizReportAge", 15)
|
|
331
|
+
mock_is_expired.assert_called_once_with("2023-06-01T14:37:55.450532Z", 15)
|
|
332
|
+
mock_create_report.assert_called_once_with(
|
|
333
|
+
wiz_project_id=self.project_id,
|
|
334
|
+
report_name="NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
335
|
+
framework_id="framework-1",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
339
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
|
|
340
|
+
def test_get_or_create_report_id_no_existing_report(self, mock_create_report, mock_application):
|
|
341
|
+
"""Test creating new report when no existing report is found."""
|
|
342
|
+
# Mock Application
|
|
343
|
+
mock_app = MagicMock()
|
|
344
|
+
mock_app.config.get.return_value = 15
|
|
345
|
+
mock_application.return_value = mock_app
|
|
346
|
+
|
|
347
|
+
# Mock create_compliance_report to return new report ID
|
|
348
|
+
mock_create_report.return_value = "new-report-789"
|
|
349
|
+
|
|
350
|
+
# Empty existing reports list
|
|
351
|
+
existing_reports = []
|
|
352
|
+
|
|
353
|
+
result = get_or_create_report_id(
|
|
354
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
self.assertEqual(result, "new-report-789")
|
|
358
|
+
mock_app.config.get.assert_called_once_with("wizReportAge", 15)
|
|
359
|
+
mock_create_report.assert_called_once_with(
|
|
360
|
+
wiz_project_id=self.project_id,
|
|
361
|
+
report_name="NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
362
|
+
framework_id="framework-1",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
366
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
|
|
367
|
+
def test_get_or_create_report_id_missing_run_at(self, mock_is_expired, mock_application):
|
|
368
|
+
"""Test behavior when existing report has no runAt timestamp."""
|
|
369
|
+
# Mock Application
|
|
370
|
+
mock_app = MagicMock()
|
|
371
|
+
mock_app.config.get.return_value = 15
|
|
372
|
+
mock_application.return_value = mock_app
|
|
373
|
+
|
|
374
|
+
# Mock existing reports with missing runAt
|
|
375
|
+
existing_reports = [
|
|
376
|
+
{
|
|
377
|
+
"id": "existing-report-123",
|
|
378
|
+
"name": "NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
379
|
+
"lastRun": {}, # No runAt timestamp
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
result = get_or_create_report_id(
|
|
384
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# When runAt is missing, the method returns the existing report
|
|
388
|
+
# because the condition `if run_at and is_report_expired(run_at, report_age_days):`
|
|
389
|
+
# is False when run_at is None/missing
|
|
390
|
+
self.assertEqual(result, "existing-report-123")
|
|
391
|
+
mock_app.config.get.assert_called_once_with("wizReportAge", 15)
|
|
392
|
+
# is_report_expired should not be called when runAt is missing
|
|
393
|
+
mock_is_expired.assert_not_called()
|
|
394
|
+
|
|
395
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
396
|
+
def test_get_or_create_report_id_framework_not_found(self, mock_application):
|
|
397
|
+
"""Test ValueError when target framework is not in frameworks list."""
|
|
398
|
+
# Mock Application
|
|
399
|
+
mock_app = MagicMock()
|
|
400
|
+
mock_app.config.get.return_value = 15
|
|
401
|
+
mock_application.return_value = mock_app
|
|
402
|
+
|
|
403
|
+
# Use a framework not in the frameworks list
|
|
404
|
+
invalid_framework = "INVALID_FRAMEWORK"
|
|
405
|
+
|
|
406
|
+
existing_reports = []
|
|
407
|
+
|
|
408
|
+
with self.assertRaises(ValueError) as context:
|
|
409
|
+
get_or_create_report_id(
|
|
410
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, invalid_framework
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# The actual error message from list.index() is different
|
|
414
|
+
self.assertIn("is not in list", str(context.exception))
|
|
415
|
+
|
|
416
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
417
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
|
|
418
|
+
def test_get_or_create_report_id_custom_report_age(self, mock_is_expired, mock_application):
|
|
419
|
+
"""Test using custom wizReportAge configuration."""
|
|
420
|
+
# Mock Application with custom age
|
|
421
|
+
mock_app = MagicMock()
|
|
422
|
+
mock_app.config.get.return_value = 30 # 30 days instead of default 15
|
|
423
|
+
mock_application.return_value = mock_app
|
|
424
|
+
|
|
425
|
+
# Mock is_report_expired to return False
|
|
426
|
+
mock_is_expired.return_value = False
|
|
427
|
+
|
|
428
|
+
# Mock existing reports with a valid report
|
|
429
|
+
existing_reports = [
|
|
430
|
+
{
|
|
431
|
+
"id": "existing-report-123",
|
|
432
|
+
"name": "NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
433
|
+
"lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
result = get_or_create_report_id(
|
|
438
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
self.assertEqual(result, "existing-report-123")
|
|
442
|
+
mock_app.config.get.assert_called_once_with("wizReportAge", 15)
|
|
443
|
+
mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 30)
|
|
444
|
+
|
|
445
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
446
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
|
|
447
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
|
|
448
|
+
def test_get_or_create_report_id_multiple_reports_first_match(
|
|
449
|
+
self, mock_create_report, mock_is_expired, mock_application
|
|
450
|
+
):
|
|
451
|
+
"""Test behavior when multiple reports exist, should use first matching report."""
|
|
452
|
+
# Mock Application
|
|
453
|
+
mock_app = MagicMock()
|
|
454
|
+
mock_app.config.get.return_value = 15
|
|
455
|
+
mock_application.return_value = mock_app
|
|
456
|
+
|
|
457
|
+
# Mock is_report_expired to return False for first call
|
|
458
|
+
mock_is_expired.return_value = False
|
|
459
|
+
|
|
460
|
+
# Mock existing reports with multiple matching reports
|
|
461
|
+
existing_reports = [
|
|
462
|
+
{
|
|
463
|
+
"id": "first-report-123",
|
|
464
|
+
"name": "NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
465
|
+
"lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"id": "second-report-456",
|
|
469
|
+
"name": "NIST_SP_800-53_Revision_5_project_test-project-123",
|
|
470
|
+
"lastRun": {"runAt": "2023-07-16T14:37:55.450532Z"},
|
|
471
|
+
},
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
result = get_or_create_report_id(
|
|
475
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Should return the first matching report
|
|
479
|
+
self.assertEqual(result, "first-report-123")
|
|
480
|
+
mock_is_expired.assert_called_once_with("2023-07-15T14:37:55.450532Z", 15)
|
|
481
|
+
mock_create_report.assert_not_called()
|
|
482
|
+
|
|
483
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.Application")
|
|
484
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.is_report_expired")
|
|
485
|
+
@patch("regscale.integrations.commercial.wizv2.utils.main.create_compliance_report")
|
|
486
|
+
def test_get_or_create_report_id_different_report_names(
|
|
487
|
+
self, mock_create_report, mock_is_expired, mock_application
|
|
488
|
+
):
|
|
489
|
+
"""Test creating new report when existing reports have different names."""
|
|
490
|
+
# Mock Application
|
|
491
|
+
mock_app = MagicMock()
|
|
492
|
+
mock_app.config.get.return_value = 15
|
|
493
|
+
mock_application.return_value = mock_app
|
|
494
|
+
|
|
495
|
+
# Mock create_compliance_report to return new report ID
|
|
496
|
+
mock_create_report.return_value = "new-report-999"
|
|
497
|
+
|
|
498
|
+
# Mock existing reports with different names
|
|
499
|
+
existing_reports = [
|
|
500
|
+
{
|
|
501
|
+
"id": "other-report-123",
|
|
502
|
+
"name": "OTHER_FRAMEWORK_project_test-project-123",
|
|
503
|
+
"lastRun": {"runAt": "2023-07-15T14:37:55.450532Z"},
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
result = get_or_create_report_id(
|
|
508
|
+
self.project_id, self.frameworks, self.wiz_frameworks, existing_reports, self.target_framework
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
self.assertEqual(result, "new-report-999")
|
|
512
|
+
mock_app.config.get.assert_called_once_with("wizReportAge", 15)
|
|
513
|
+
# is_report_expired should not be called since no matching report name
|
|
514
|
+
mock_is_expired.assert_not_called()
|
|
515
|
+
mock_create_report.assert_called_once()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
if __name__ == "__main__":
|
|
519
|
+
unittest.main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for Wiz V2 Utils"""
|