regscale-cli 6.25.0.1__py3-none-any.whl → 6.26.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/constants.py +20 -71
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/due_date_handler.py +118 -6
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +199 -130
- regscale/models/integration_models/cisa_kev_data.json +199 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Unit tests for FedRAMP Rev 5 POAM Export functionality
|
|
5
|
+
|
|
6
|
+
This module tests the FedRAMP Rev 5 POAM Excel export functionality, including:
|
|
7
|
+
- Dynamic POAM ID generation based on source file path properties
|
|
8
|
+
- KEV date determination from CISA KEV catalog
|
|
9
|
+
- Deviation status mapping (Approved/Pending/Rejected)
|
|
10
|
+
- Custom milestone and comment generation
|
|
11
|
+
- Excel formatting and column operations
|
|
12
|
+
- Date rounding for closed POAMs (25th of month)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from unittest.mock import MagicMock, Mock, patch, call
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
from openpyxl import Workbook
|
|
21
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
22
|
+
|
|
23
|
+
from regscale.integrations.public.fedramp.poam_export_v5 import (
|
|
24
|
+
convert_to_list,
|
|
25
|
+
determine_kev_date,
|
|
26
|
+
determine_poam_comment,
|
|
27
|
+
determine_poam_id,
|
|
28
|
+
determine_poam_service_name,
|
|
29
|
+
get_cached_cisa_kev,
|
|
30
|
+
lookup_scan_date,
|
|
31
|
+
set_end_columns,
|
|
32
|
+
set_milestones,
|
|
33
|
+
set_risk_info,
|
|
34
|
+
set_short_date,
|
|
35
|
+
set_status,
|
|
36
|
+
set_vendor_info,
|
|
37
|
+
strip_html,
|
|
38
|
+
update_column_widths,
|
|
39
|
+
align_column,
|
|
40
|
+
update_header,
|
|
41
|
+
get_all_poams,
|
|
42
|
+
gen_links,
|
|
43
|
+
gen_files,
|
|
44
|
+
gen_milestones,
|
|
45
|
+
process_row,
|
|
46
|
+
process_worksheet,
|
|
47
|
+
export_poam_v5,
|
|
48
|
+
map_weakness_detector_and_id_for_rev5_issues,
|
|
49
|
+
_generate_closed_poam_comment,
|
|
50
|
+
_generate_open_poam_comment,
|
|
51
|
+
)
|
|
52
|
+
from regscale.models.regscale_models import (
|
|
53
|
+
Asset,
|
|
54
|
+
Deviation,
|
|
55
|
+
Issue,
|
|
56
|
+
IssueSeverity,
|
|
57
|
+
IssueStatus,
|
|
58
|
+
Property,
|
|
59
|
+
ScanHistory,
|
|
60
|
+
SecurityPlan,
|
|
61
|
+
VulnerabilityMapping,
|
|
62
|
+
)
|
|
63
|
+
from tests import CLITestFixture
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestPOAMExportV5(CLITestFixture):
|
|
67
|
+
"""Test class for FedRAMP Rev 5 POAM export functionality"""
|
|
68
|
+
|
|
69
|
+
def test_set_short_date_valid(self):
|
|
70
|
+
"""Test set_short_date with valid date string"""
|
|
71
|
+
date_str = "2025-03-15T10:30:00"
|
|
72
|
+
result = set_short_date(date_str)
|
|
73
|
+
assert result == "03/15/25"
|
|
74
|
+
|
|
75
|
+
def test_set_short_date_different_formats(self):
|
|
76
|
+
"""Test set_short_date with different date formats"""
|
|
77
|
+
# ISO format
|
|
78
|
+
assert set_short_date("2025-01-01T00:00:00") == "01/01/25"
|
|
79
|
+
# Date only
|
|
80
|
+
assert set_short_date("2025-12-31") == "12/31/25"
|
|
81
|
+
|
|
82
|
+
def test_strip_html_with_tags(self):
|
|
83
|
+
"""Test strip_html removes HTML tags"""
|
|
84
|
+
html_str = "<p>Test <strong>content</strong> here</p>"
|
|
85
|
+
result = strip_html(html_str)
|
|
86
|
+
assert result == "Test content here"
|
|
87
|
+
|
|
88
|
+
def test_strip_html_with_entities(self):
|
|
89
|
+
"""Test strip_html handles HTML entities"""
|
|
90
|
+
html_str = "<div>Test & content</div>"
|
|
91
|
+
result = strip_html(html_str)
|
|
92
|
+
assert result == "<div>Test & content</div>"
|
|
93
|
+
|
|
94
|
+
def test_strip_html_empty_string(self):
|
|
95
|
+
"""Test strip_html with empty string"""
|
|
96
|
+
result = strip_html("")
|
|
97
|
+
assert result == ""
|
|
98
|
+
|
|
99
|
+
def test_strip_html_none(self):
|
|
100
|
+
"""Test strip_html with None"""
|
|
101
|
+
result = strip_html(None)
|
|
102
|
+
assert result == ""
|
|
103
|
+
|
|
104
|
+
def test_strip_html_nested_tags(self):
|
|
105
|
+
"""Test strip_html with nested HTML tags"""
|
|
106
|
+
html_str = "<div><p><span>Nested</span> content</p></div>"
|
|
107
|
+
result = strip_html(html_str)
|
|
108
|
+
assert result == "Nested content"
|
|
109
|
+
|
|
110
|
+
def test_convert_to_list_paragraph_tags(self):
|
|
111
|
+
"""Test convert_to_list with <p> tag delimiters"""
|
|
112
|
+
asset_str = "<p>Asset1</p><p>Asset2</p><p>Asset3</p>"
|
|
113
|
+
result = convert_to_list(asset_str)
|
|
114
|
+
assert result == ["Asset1", "Asset2", "Asset3"]
|
|
115
|
+
|
|
116
|
+
def test_convert_to_list_tabs(self):
|
|
117
|
+
"""Test convert_to_list with tab delimiters"""
|
|
118
|
+
asset_str = "Asset1\tAsset2\tAsset3"
|
|
119
|
+
result = convert_to_list(asset_str)
|
|
120
|
+
assert result == ["Asset1", "Asset2", "Asset3"]
|
|
121
|
+
|
|
122
|
+
def test_convert_to_list_newlines(self):
|
|
123
|
+
"""Test convert_to_list with newline delimiters"""
|
|
124
|
+
asset_str = "Asset1\nAsset2\nAsset3"
|
|
125
|
+
result = convert_to_list(asset_str)
|
|
126
|
+
assert result == ["Asset1", "Asset2", "Asset3"]
|
|
127
|
+
|
|
128
|
+
def test_convert_to_list_empty_string(self):
|
|
129
|
+
"""Test convert_to_list with empty string"""
|
|
130
|
+
result = convert_to_list("")
|
|
131
|
+
assert result == []
|
|
132
|
+
|
|
133
|
+
def test_convert_to_list_none(self):
|
|
134
|
+
"""Test convert_to_list with None"""
|
|
135
|
+
result = convert_to_list(None)
|
|
136
|
+
assert result == []
|
|
137
|
+
|
|
138
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
|
|
139
|
+
def test_determine_kev_date_match_found(self, mock_get_kev):
|
|
140
|
+
"""Test determine_kev_date with matching CVE"""
|
|
141
|
+
mock_get_kev.return_value = {
|
|
142
|
+
"vulnerabilities": [
|
|
143
|
+
{"cveID": "CVE-2025-1234", "dueDate": "2025-06-15T00:00:00"},
|
|
144
|
+
{"cveID": "CVE-2025-5678", "dueDate": "2025-07-20T00:00:00"},
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
result = determine_kev_date("CVE-2025-1234")
|
|
148
|
+
assert result == "06/15/25"
|
|
149
|
+
|
|
150
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
|
|
151
|
+
def test_determine_kev_date_case_insensitive(self, mock_get_kev):
|
|
152
|
+
"""Test determine_kev_date is case insensitive"""
|
|
153
|
+
mock_get_kev.return_value = {"vulnerabilities": [{"cveID": "CVE-2025-1234", "dueDate": "2025-06-15T00:00:00"}]}
|
|
154
|
+
result = determine_kev_date("cve-2025-1234")
|
|
155
|
+
assert result == "06/15/25"
|
|
156
|
+
|
|
157
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
|
|
158
|
+
def test_determine_kev_date_no_match(self, mock_get_kev):
|
|
159
|
+
"""Test determine_kev_date with no matching CVE"""
|
|
160
|
+
mock_get_kev.return_value = {"vulnerabilities": [{"cveID": "CVE-2025-9999", "dueDate": "2025-08-01"}]}
|
|
161
|
+
result = determine_kev_date("CVE-2025-1234")
|
|
162
|
+
assert result == "N/A"
|
|
163
|
+
|
|
164
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
|
|
165
|
+
def test_determine_kev_date_empty_cve(self, mock_get_kev):
|
|
166
|
+
"""Test determine_kev_date with empty CVE"""
|
|
167
|
+
result = determine_kev_date("")
|
|
168
|
+
assert result == "N/A"
|
|
169
|
+
mock_get_kev.assert_not_called()
|
|
170
|
+
|
|
171
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_cached_cisa_kev")
|
|
172
|
+
def test_determine_kev_date_none_cve(self, mock_get_kev):
|
|
173
|
+
"""Test determine_kev_date with None CVE"""
|
|
174
|
+
result = determine_kev_date(None)
|
|
175
|
+
assert result == "N/A"
|
|
176
|
+
mock_get_kev.assert_not_called()
|
|
177
|
+
|
|
178
|
+
def test_determine_poam_id_pdf_path(self):
|
|
179
|
+
"""Test determine_poam_id with pdf in file path"""
|
|
180
|
+
poam = Mock(id=12345)
|
|
181
|
+
props = [Mock(key="source_file_path", value="/path/to/pdf/file.xml")]
|
|
182
|
+
result = determine_poam_id(poam, props)
|
|
183
|
+
assert result == "DC-12345"
|
|
184
|
+
|
|
185
|
+
def test_determine_poam_id_signatures_path(self):
|
|
186
|
+
"""Test determine_poam_id with signatures in file path"""
|
|
187
|
+
poam = Mock(id=12345)
|
|
188
|
+
props = [Mock(key="source_file_path", value="/path/to/signatures/file.xml")]
|
|
189
|
+
result = determine_poam_id(poam, props)
|
|
190
|
+
assert result == "CPT-12345"
|
|
191
|
+
|
|
192
|
+
def test_determine_poam_id_campaign_path(self):
|
|
193
|
+
"""Test determine_poam_id with campaign in file path"""
|
|
194
|
+
poam = Mock(id=12345)
|
|
195
|
+
props = [Mock(key="source_file_path", value="/path/to/campaign/file.xml")]
|
|
196
|
+
result = determine_poam_id(poam, props)
|
|
197
|
+
assert result == "ALM-12345"
|
|
198
|
+
|
|
199
|
+
def test_determine_poam_id_learning_manager_path(self):
|
|
200
|
+
"""Test determine_poam_id with learning manager in file path"""
|
|
201
|
+
poam = Mock(id=12345)
|
|
202
|
+
props = [Mock(key="source_file_path", value="/path/to/learning manager/file.xml")]
|
|
203
|
+
result = determine_poam_id(poam, props)
|
|
204
|
+
assert result == "CCD-12345"
|
|
205
|
+
|
|
206
|
+
def test_determine_poam_id_cce_path(self):
|
|
207
|
+
"""Test determine_poam_id with cce in file path"""
|
|
208
|
+
poam = Mock(id=12345)
|
|
209
|
+
props = [Mock(key="source_file_path", value="/path/to/cce/file.xml")]
|
|
210
|
+
result = determine_poam_id(poam, props)
|
|
211
|
+
assert result == "CCE-12345"
|
|
212
|
+
|
|
213
|
+
def test_determine_poam_id_unknown_path(self):
|
|
214
|
+
"""Test determine_poam_id with unknown file path"""
|
|
215
|
+
poam = Mock(id=12345)
|
|
216
|
+
props = [Mock(key="source_file_path", value="/path/to/unknown/file.xml")]
|
|
217
|
+
result = determine_poam_id(poam, props)
|
|
218
|
+
assert result == "UNK-12345"
|
|
219
|
+
|
|
220
|
+
def test_determine_poam_id_no_source_path_property(self):
|
|
221
|
+
"""Test determine_poam_id with no source_file_path property"""
|
|
222
|
+
poam = Mock(id=12345)
|
|
223
|
+
props = [Mock(key="other_property", value="value")]
|
|
224
|
+
result = determine_poam_id(poam, props)
|
|
225
|
+
assert result == "UNK-12345"
|
|
226
|
+
|
|
227
|
+
def test_determine_poam_id_empty_properties(self):
|
|
228
|
+
"""Test determine_poam_id with empty properties list"""
|
|
229
|
+
poam = Mock(id=12345)
|
|
230
|
+
props = []
|
|
231
|
+
result = determine_poam_id(poam, props)
|
|
232
|
+
assert result == "UNK-12345"
|
|
233
|
+
|
|
234
|
+
def test_determine_poam_id_case_insensitive(self):
|
|
235
|
+
"""Test determine_poam_id is case insensitive"""
|
|
236
|
+
poam = Mock(id=12345)
|
|
237
|
+
props = [Mock(key="source_file_path", value="/path/to/PDF/FILE.XML")]
|
|
238
|
+
result = determine_poam_id(poam, props)
|
|
239
|
+
assert result == "DC-12345"
|
|
240
|
+
|
|
241
|
+
def test_determine_poam_service_name_pdf(self):
|
|
242
|
+
"""Test determine_poam_service_name with pdf in path"""
|
|
243
|
+
poam = Mock(id=12345)
|
|
244
|
+
props = [Mock(key="source_file_path", value="/path/to/pdf/file.xml")]
|
|
245
|
+
result = determine_poam_service_name(poam, props)
|
|
246
|
+
assert result == "PDF Services"
|
|
247
|
+
|
|
248
|
+
def test_determine_poam_service_name_signatures(self):
|
|
249
|
+
"""Test determine_poam_service_name with signatures in path"""
|
|
250
|
+
poam = Mock(id=12345)
|
|
251
|
+
props = [Mock(key="source_file_path", value="/path/to/signatures/file.xml")]
|
|
252
|
+
result = determine_poam_service_name(poam, props)
|
|
253
|
+
assert result == "Signatures"
|
|
254
|
+
|
|
255
|
+
def test_determine_poam_service_name_unknown(self):
|
|
256
|
+
"""Test determine_poam_service_name with unknown path"""
|
|
257
|
+
poam = Mock(id=12345)
|
|
258
|
+
props = [Mock(key="source_file_path", value="/path/to/unknown/file.xml")]
|
|
259
|
+
result = determine_poam_service_name(poam, props)
|
|
260
|
+
assert result == "UNKNOWN"
|
|
261
|
+
|
|
262
|
+
def test_determine_poam_service_name_no_property(self):
|
|
263
|
+
"""Test determine_poam_service_name with no source_file_path property"""
|
|
264
|
+
poam = Mock(id=12345)
|
|
265
|
+
props = [Mock(key="other_property", value="value")]
|
|
266
|
+
result = determine_poam_service_name(poam, props)
|
|
267
|
+
assert result == "UNKNOWN"
|
|
268
|
+
|
|
269
|
+
def test_lookup_scan_date_with_matching_asset(self):
|
|
270
|
+
"""Test lookup_scan_date finds scan date from vulnerability mapping"""
|
|
271
|
+
# Test the function without excessive mocking, just verify it returns a date
|
|
272
|
+
poam = Mock(assetIdentifier="Asset1", dateLastUpdated="2025-03-15T10:00:00")
|
|
273
|
+
asset = Mock(id=100, name="Asset1")
|
|
274
|
+
assets = [asset]
|
|
275
|
+
|
|
276
|
+
# Since this function makes real API calls, test that it falls back to dateLastUpdated
|
|
277
|
+
result = lookup_scan_date(poam, assets)
|
|
278
|
+
# Should return a formatted date
|
|
279
|
+
assert result == "03/15/25"
|
|
280
|
+
|
|
281
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.VulnerabilityMapping")
|
|
282
|
+
def test_lookup_scan_date_no_matching_asset(self, mock_vuln_mapping):
|
|
283
|
+
"""Test lookup_scan_date with no matching asset"""
|
|
284
|
+
poam = Mock(assetIdentifier="Asset1", dateLastUpdated="2025-03-15T10:00:00")
|
|
285
|
+
asset = Mock(id=100, name="Asset2")
|
|
286
|
+
assets = [asset]
|
|
287
|
+
|
|
288
|
+
result = lookup_scan_date(poam, assets)
|
|
289
|
+
assert result == "03/15/25"
|
|
290
|
+
mock_vuln_mapping.find_by_asset.assert_not_called()
|
|
291
|
+
|
|
292
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.VulnerabilityMapping")
|
|
293
|
+
def test_lookup_scan_date_no_vulnerabilities(self, mock_vuln_mapping):
|
|
294
|
+
"""Test lookup_scan_date when no vulnerabilities found"""
|
|
295
|
+
poam = Mock(assetIdentifier="Asset1", dateLastUpdated="2025-03-15T10:00:00")
|
|
296
|
+
asset = Mock(id=100, name="Asset1")
|
|
297
|
+
assets = [asset]
|
|
298
|
+
|
|
299
|
+
mock_vuln_mapping.find_by_asset.return_value = []
|
|
300
|
+
|
|
301
|
+
result = lookup_scan_date(poam, assets)
|
|
302
|
+
assert result == "03/15/25"
|
|
303
|
+
|
|
304
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_short_date")
|
|
305
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.convert_to_list")
|
|
306
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.VulnerabilityMapping")
|
|
307
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.ScanHistory")
|
|
308
|
+
def test_lookup_scan_date_multiple_assets(
|
|
309
|
+
self, mock_scan_history, mock_vuln_mapping, mock_convert, mock_short_date
|
|
310
|
+
):
|
|
311
|
+
"""Test lookup_scan_date with multiple assets in identifier"""
|
|
312
|
+
poam = Mock(assetIdentifier="Asset1\nAsset2", dateLastUpdated="2025-03-15T10:00:00")
|
|
313
|
+
asset1 = Mock(id=100, name="Asset1")
|
|
314
|
+
asset2 = Mock(id=101, name="Asset2")
|
|
315
|
+
assets = [asset1, asset2]
|
|
316
|
+
|
|
317
|
+
mock_convert.return_value = ["Asset1", "Asset2"]
|
|
318
|
+
mock_vuln = Mock(scanId=500)
|
|
319
|
+
mock_vuln_mapping.find_by_asset.return_value = [mock_vuln]
|
|
320
|
+
|
|
321
|
+
mock_scan = Mock(scanDate="2025-03-10T08:00:00")
|
|
322
|
+
mock_scan_history.get_object.return_value = mock_scan
|
|
323
|
+
mock_short_date.return_value = "03/10/25"
|
|
324
|
+
|
|
325
|
+
result = lookup_scan_date(poam, assets)
|
|
326
|
+
assert result == "03/10/25"
|
|
327
|
+
|
|
328
|
+
def test_determine_poam_comment_closed_poam_with_date(self):
|
|
329
|
+
"""Test determine_poam_comment for closed POAM"""
|
|
330
|
+
poam = Mock(
|
|
331
|
+
id=123,
|
|
332
|
+
dateFirstDetected="2025-03-01T10:00:00",
|
|
333
|
+
dateCompleted="2025-03-15T10:00:00",
|
|
334
|
+
dateCreated="2025-03-01T10:00:00",
|
|
335
|
+
poamComments="",
|
|
336
|
+
)
|
|
337
|
+
poam.save = Mock()
|
|
338
|
+
assets = []
|
|
339
|
+
|
|
340
|
+
result = determine_poam_comment(poam, assets)
|
|
341
|
+
assert "Per review of the latest scan report" in result
|
|
342
|
+
assert "This POAM will be submitted for closure" in result
|
|
343
|
+
assert "03/01/25: POAM entry added" in result
|
|
344
|
+
poam.save.assert_called_once()
|
|
345
|
+
|
|
346
|
+
def test_determine_poam_comment_open_poam_new_comment(self):
|
|
347
|
+
"""Test determine_poam_comment for open POAM with no existing comment"""
|
|
348
|
+
poam = Mock(
|
|
349
|
+
id=123, dateFirstDetected="2025-03-01T10:00:00", dateCompleted=None, poamComments="", dateCreated=None
|
|
350
|
+
)
|
|
351
|
+
poam.save = Mock()
|
|
352
|
+
assets = []
|
|
353
|
+
|
|
354
|
+
result = determine_poam_comment(poam, assets)
|
|
355
|
+
assert "03/01/25: POAM entry added" in result
|
|
356
|
+
poam.save.assert_called_once()
|
|
357
|
+
|
|
358
|
+
def test_determine_poam_comment_open_poam_existing_comment(self):
|
|
359
|
+
"""Test determine_poam_comment for open POAM with existing comment"""
|
|
360
|
+
existing_comment = "03/01/25: POAM entry added"
|
|
361
|
+
poam = Mock(
|
|
362
|
+
id=123,
|
|
363
|
+
dateFirstDetected="2025-03-01T10:00:00",
|
|
364
|
+
dateCompleted=None,
|
|
365
|
+
poamComments=existing_comment,
|
|
366
|
+
dateCreated=None,
|
|
367
|
+
)
|
|
368
|
+
poam.save = Mock()
|
|
369
|
+
assets = []
|
|
370
|
+
|
|
371
|
+
result = determine_poam_comment(poam, assets)
|
|
372
|
+
assert result == existing_comment
|
|
373
|
+
poam.save.assert_not_called()
|
|
374
|
+
|
|
375
|
+
def test_determine_poam_comment_no_detection_date(self):
|
|
376
|
+
"""Test determine_poam_comment with no detection date"""
|
|
377
|
+
poam = Mock(id=123, dateFirstDetected=None, poamComments="")
|
|
378
|
+
poam.save = Mock()
|
|
379
|
+
assets = []
|
|
380
|
+
|
|
381
|
+
result = determine_poam_comment(poam, assets)
|
|
382
|
+
assert result == "N/A"
|
|
383
|
+
poam.save.assert_not_called()
|
|
384
|
+
|
|
385
|
+
def test_generate_closed_poam_comment_new(self):
|
|
386
|
+
"""Test _generate_closed_poam_comment creates new closed comment"""
|
|
387
|
+
poam = Mock(dateCompleted="2025-03-15T10:00:00", dateCreated="2025-03-01T10:00:00")
|
|
388
|
+
current_comment = ""
|
|
389
|
+
template = "Per review of the latest scan report on %s, (TGRC) can confirm that this issue no longer persists. This POAM will be submitted for closure."
|
|
390
|
+
open_template = "POAM entry added"
|
|
391
|
+
|
|
392
|
+
result = _generate_closed_poam_comment(poam, current_comment, template, open_template)
|
|
393
|
+
assert "Per review of the latest scan report on 03/15/25" in result
|
|
394
|
+
assert "This POAM will be submitted for closure" in result
|
|
395
|
+
assert "03/01/25: POAM entry added" in result
|
|
396
|
+
|
|
397
|
+
def test_generate_closed_poam_comment_already_has_closed_blurb(self):
|
|
398
|
+
"""Test _generate_closed_poam_comment when comment already has closed blurb"""
|
|
399
|
+
poam = Mock(dateCompleted="2025-03-15T10:00:00", dateCreated="2025-03-01T10:00:00")
|
|
400
|
+
# Include "POAM entry added" to pass the first check, and the closed blurb to pass the second
|
|
401
|
+
current_comment = "This POAM will be submitted for closure\n03/01/25: POAM entry added"
|
|
402
|
+
template = "Template %s"
|
|
403
|
+
open_template = "POAM entry added"
|
|
404
|
+
|
|
405
|
+
result = _generate_closed_poam_comment(poam, current_comment, template, open_template)
|
|
406
|
+
# Should return unchanged because it already has the closure blurb
|
|
407
|
+
assert result == current_comment
|
|
408
|
+
assert "This POAM will be submitted for closure" in result
|
|
409
|
+
|
|
410
|
+
def test_generate_open_poam_comment_new(self):
|
|
411
|
+
"""Test _generate_open_poam_comment creates new comment"""
|
|
412
|
+
current_comment = ""
|
|
413
|
+
detection_date = "03/01/25"
|
|
414
|
+
template = "POAM entry added"
|
|
415
|
+
|
|
416
|
+
result = _generate_open_poam_comment(current_comment, detection_date, template)
|
|
417
|
+
assert result == "03/01/25: POAM entry added"
|
|
418
|
+
|
|
419
|
+
def test_generate_open_poam_comment_already_has_entry_added(self):
|
|
420
|
+
"""Test _generate_open_poam_comment when comment already has entry added"""
|
|
421
|
+
current_comment = "03/01/25: POAM entry added"
|
|
422
|
+
detection_date = "03/01/25"
|
|
423
|
+
template = "POAM entry added"
|
|
424
|
+
|
|
425
|
+
result = _generate_open_poam_comment(current_comment, detection_date, template)
|
|
426
|
+
assert result == current_comment
|
|
427
|
+
|
|
428
|
+
def test_generate_open_poam_comment_prepends_to_existing(self):
|
|
429
|
+
"""Test _generate_open_poam_comment prepends to existing comment"""
|
|
430
|
+
current_comment = "Some existing text"
|
|
431
|
+
detection_date = "03/01/25"
|
|
432
|
+
template = "POAM entry added"
|
|
433
|
+
|
|
434
|
+
result = _generate_open_poam_comment(current_comment, detection_date, template)
|
|
435
|
+
assert result == "03/01/25: POAM entry added\nSome existing text"
|
|
436
|
+
|
|
437
|
+
def test_set_milestones_with_milestones(self):
|
|
438
|
+
"""Test set_milestones populates worksheet with milestone data"""
|
|
439
|
+
poam = Mock(id=123)
|
|
440
|
+
sheet = MagicMock()
|
|
441
|
+
column_l_date = "04/01/25"
|
|
442
|
+
all_milestones = [
|
|
443
|
+
{"parent_id": 123, "MilestoneDate": "2025-03-15T10:00:00"},
|
|
444
|
+
{"parent_id": 123, "MilestoneDate": "2025-03-20T10:00:00"},
|
|
445
|
+
{"parent_id": 456, "MilestoneDate": "2025-03-25T10:00:00"},
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
set_milestones(poam, 10, sheet, column_l_date, all_milestones)
|
|
449
|
+
|
|
450
|
+
# Should contain milestone dates
|
|
451
|
+
assert sheet.__getitem__.called
|
|
452
|
+
|
|
453
|
+
def test_set_milestones_no_milestones(self):
|
|
454
|
+
"""Test set_milestones with no milestones for POAM"""
|
|
455
|
+
poam = Mock(id=123)
|
|
456
|
+
sheet = MagicMock()
|
|
457
|
+
column_l_date = "04/01/25"
|
|
458
|
+
all_milestones = [{"parent_id": 456, "MilestoneDate": "2025-03-25T10:00:00"}]
|
|
459
|
+
|
|
460
|
+
set_milestones(poam, 10, sheet, column_l_date, all_milestones)
|
|
461
|
+
|
|
462
|
+
# Should set default milestone text
|
|
463
|
+
assert sheet.__getitem__.called
|
|
464
|
+
|
|
465
|
+
def test_set_status_closed_before_25th(self):
|
|
466
|
+
"""Test set_status for closed POAM completed before 25th of month"""
|
|
467
|
+
poam = Mock(status="Closed", dateCompleted="2025-03-10T10:00:00")
|
|
468
|
+
sheet = MagicMock()
|
|
469
|
+
|
|
470
|
+
set_status(poam, 10, sheet)
|
|
471
|
+
|
|
472
|
+
# Should round to 25th of same month
|
|
473
|
+
sheet.__getitem__.assert_called_with("O10")
|
|
474
|
+
sheet.__getitem__.return_value.value = "03/25/25"
|
|
475
|
+
|
|
476
|
+
def test_set_status_closed_on_25th(self):
|
|
477
|
+
"""Test set_status for closed POAM completed on 25th of month"""
|
|
478
|
+
poam = Mock(status="Closed", dateCompleted="2025-03-25T10:00:00")
|
|
479
|
+
sheet = MagicMock()
|
|
480
|
+
|
|
481
|
+
set_status(poam, 10, sheet)
|
|
482
|
+
|
|
483
|
+
# Should stay on 25th of same month
|
|
484
|
+
sheet.__getitem__.assert_called()
|
|
485
|
+
|
|
486
|
+
def test_set_status_closed_after_25th(self):
|
|
487
|
+
"""Test set_status for closed POAM completed after 25th of month"""
|
|
488
|
+
poam = Mock(status="Closed", dateCompleted="2025-03-26T10:00:00")
|
|
489
|
+
sheet = MagicMock()
|
|
490
|
+
|
|
491
|
+
set_status(poam, 10, sheet)
|
|
492
|
+
|
|
493
|
+
# Should round to 25th of next month
|
|
494
|
+
sheet.__getitem__.assert_called()
|
|
495
|
+
|
|
496
|
+
def test_set_status_closed_no_completion_date(self):
|
|
497
|
+
"""Test set_status for closed POAM with no completion date"""
|
|
498
|
+
poam = Mock(status="Closed", dateCompleted=None)
|
|
499
|
+
sheet = MagicMock()
|
|
500
|
+
|
|
501
|
+
set_status(poam, 10, sheet)
|
|
502
|
+
|
|
503
|
+
sheet.__getitem__.assert_called_with("O10")
|
|
504
|
+
|
|
505
|
+
def test_set_status_open_with_last_updated(self):
|
|
506
|
+
"""Test set_status for open POAM with last updated date"""
|
|
507
|
+
poam = Mock(status="Open", dateLastUpdated="2025-03-15T10:00:00", dateCompleted=None)
|
|
508
|
+
sheet = MagicMock()
|
|
509
|
+
|
|
510
|
+
set_status(poam, 10, sheet)
|
|
511
|
+
|
|
512
|
+
sheet.__getitem__.assert_called()
|
|
513
|
+
|
|
514
|
+
def test_set_status_open_no_last_updated(self):
|
|
515
|
+
"""Test set_status for open POAM with no last updated date"""
|
|
516
|
+
poam = Mock(status="Open", dateLastUpdated=None, dateCompleted=None)
|
|
517
|
+
sheet = MagicMock()
|
|
518
|
+
|
|
519
|
+
set_status(poam, 10, sheet)
|
|
520
|
+
|
|
521
|
+
sheet.__getitem__.assert_called_with("O10")
|
|
522
|
+
|
|
523
|
+
def test_set_vendor_info_with_dependency(self):
|
|
524
|
+
"""Test set_vendor_info with vendor dependency"""
|
|
525
|
+
poam = Mock(vendorDependency="Yes", vendorName="Test Vendor", vendorLastUpdate="2025-03-15T10:00:00")
|
|
526
|
+
sheet = MagicMock()
|
|
527
|
+
|
|
528
|
+
set_vendor_info(poam, 10, sheet)
|
|
529
|
+
|
|
530
|
+
assert sheet.__getitem__.call_count >= 3
|
|
531
|
+
|
|
532
|
+
def test_set_vendor_info_no_dependency(self):
|
|
533
|
+
"""Test set_vendor_info without vendor dependency"""
|
|
534
|
+
poam = Mock(vendorDependency="No", vendorName=None, vendorLastUpdate=None)
|
|
535
|
+
sheet = MagicMock()
|
|
536
|
+
|
|
537
|
+
set_vendor_info(poam, 10, sheet)
|
|
538
|
+
|
|
539
|
+
assert sheet.__getitem__.called
|
|
540
|
+
|
|
541
|
+
def test_set_vendor_info_no_vendor_name(self):
|
|
542
|
+
"""Test set_vendor_info with no vendor name"""
|
|
543
|
+
poam = Mock(vendorDependency="Yes", vendorName=None, vendorLastUpdate="2025-03-15T10:00:00")
|
|
544
|
+
sheet = MagicMock()
|
|
545
|
+
|
|
546
|
+
set_vendor_info(poam, 10, sheet)
|
|
547
|
+
|
|
548
|
+
assert sheet.__getitem__.called
|
|
549
|
+
|
|
550
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
|
|
551
|
+
def test_set_risk_info_approved_deviation(self, mock_deviation_class):
|
|
552
|
+
"""Test set_risk_info with approved deviation"""
|
|
553
|
+
mock_deviation = Mock(deviationStatus="Approved")
|
|
554
|
+
mock_deviation_class.get_by_issue.return_value = mock_deviation
|
|
555
|
+
|
|
556
|
+
poam = Mock(
|
|
557
|
+
id=123,
|
|
558
|
+
originalRiskRating="High",
|
|
559
|
+
adjustedRiskRating="Medium",
|
|
560
|
+
riskAdjustment="Yes",
|
|
561
|
+
falsePositive="No",
|
|
562
|
+
operationalRequirement="No",
|
|
563
|
+
deviationRationale="<p>Test rationale</p>",
|
|
564
|
+
severityLevel=1,
|
|
565
|
+
)
|
|
566
|
+
sheet = MagicMock()
|
|
567
|
+
|
|
568
|
+
set_risk_info(poam, 10, sheet)
|
|
569
|
+
|
|
570
|
+
mock_deviation_class.get_by_issue.assert_called_once_with(123)
|
|
571
|
+
assert sheet.__getitem__.call_count >= 6
|
|
572
|
+
|
|
573
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.IssueSeverity")
|
|
574
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
|
|
575
|
+
def test_set_risk_info_pending_deviation(self, mock_deviation_class, mock_severity):
|
|
576
|
+
"""Test set_risk_info with pending deviation"""
|
|
577
|
+
mock_deviation = Mock(deviationStatus="Pending")
|
|
578
|
+
mock_deviation_class.get_by_issue.return_value = mock_deviation
|
|
579
|
+
mock_severity.return_value.name = "Moderate"
|
|
580
|
+
|
|
581
|
+
poam = Mock(
|
|
582
|
+
id=123,
|
|
583
|
+
originalRiskRating=None,
|
|
584
|
+
adjustedRiskRating=None,
|
|
585
|
+
riskAdjustment="Pending",
|
|
586
|
+
falsePositive="No",
|
|
587
|
+
operationalRequirement="No",
|
|
588
|
+
deviationRationale="",
|
|
589
|
+
severityLevel=2,
|
|
590
|
+
)
|
|
591
|
+
sheet = MagicMock()
|
|
592
|
+
|
|
593
|
+
set_risk_info(poam, 10, sheet)
|
|
594
|
+
|
|
595
|
+
assert sheet.__getitem__.called
|
|
596
|
+
|
|
597
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
|
|
598
|
+
def test_set_risk_info_rejected_deviation(self, mock_deviation_class):
|
|
599
|
+
"""Test set_risk_info with rejected deviation"""
|
|
600
|
+
mock_deviation = Mock(deviationStatus="Rejected")
|
|
601
|
+
mock_deviation_class.get_by_issue.return_value = mock_deviation
|
|
602
|
+
|
|
603
|
+
poam = Mock(
|
|
604
|
+
id=123,
|
|
605
|
+
originalRiskRating="Critical",
|
|
606
|
+
adjustedRiskRating=None,
|
|
607
|
+
riskAdjustment="Yes",
|
|
608
|
+
falsePositive="No",
|
|
609
|
+
operationalRequirement="No",
|
|
610
|
+
deviationRationale="Test",
|
|
611
|
+
severityLevel=0,
|
|
612
|
+
)
|
|
613
|
+
sheet = MagicMock()
|
|
614
|
+
|
|
615
|
+
set_risk_info(poam, 10, sheet)
|
|
616
|
+
|
|
617
|
+
assert sheet.__getitem__.called
|
|
618
|
+
|
|
619
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
|
|
620
|
+
def test_set_risk_info_false_positive(self, mock_deviation_class):
|
|
621
|
+
"""Test set_risk_info with false positive"""
|
|
622
|
+
mock_deviation = Mock(deviationStatus="Approved")
|
|
623
|
+
mock_deviation_class.get_by_issue.return_value = mock_deviation
|
|
624
|
+
|
|
625
|
+
poam = Mock(
|
|
626
|
+
id=123,
|
|
627
|
+
originalRiskRating="High",
|
|
628
|
+
adjustedRiskRating="N/A",
|
|
629
|
+
riskAdjustment="No",
|
|
630
|
+
falsePositive="Yes",
|
|
631
|
+
operationalRequirement="No",
|
|
632
|
+
deviationRationale="False positive",
|
|
633
|
+
severityLevel=1,
|
|
634
|
+
)
|
|
635
|
+
sheet = MagicMock()
|
|
636
|
+
|
|
637
|
+
set_risk_info(poam, 10, sheet)
|
|
638
|
+
|
|
639
|
+
assert sheet.__getitem__.called
|
|
640
|
+
|
|
641
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
|
|
642
|
+
def test_set_risk_info_operational_requirement(self, mock_deviation_class):
|
|
643
|
+
"""Test set_risk_info with operational requirement"""
|
|
644
|
+
mock_deviation = Mock(deviationStatus="Approved")
|
|
645
|
+
mock_deviation_class.get_by_issue.return_value = mock_deviation
|
|
646
|
+
|
|
647
|
+
poam = Mock(
|
|
648
|
+
id=123,
|
|
649
|
+
originalRiskRating="High",
|
|
650
|
+
adjustedRiskRating="N/A",
|
|
651
|
+
riskAdjustment="No",
|
|
652
|
+
falsePositive="No",
|
|
653
|
+
operationalRequirement="Yes",
|
|
654
|
+
deviationRationale="Operational need",
|
|
655
|
+
severityLevel=1,
|
|
656
|
+
)
|
|
657
|
+
sheet = MagicMock()
|
|
658
|
+
|
|
659
|
+
set_risk_info(poam, 10, sheet)
|
|
660
|
+
|
|
661
|
+
assert sheet.__getitem__.called
|
|
662
|
+
|
|
663
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Deviation")
|
|
664
|
+
def test_set_risk_info_operational_requirement_pending(self, mock_deviation_class):
|
|
665
|
+
"""Test set_risk_info with pending operational requirement"""
|
|
666
|
+
mock_deviation = Mock(deviationStatus="Pending")
|
|
667
|
+
mock_deviation_class.get_by_issue.return_value = mock_deviation
|
|
668
|
+
|
|
669
|
+
poam = Mock(
|
|
670
|
+
id=123,
|
|
671
|
+
originalRiskRating="High",
|
|
672
|
+
adjustedRiskRisk="N/A",
|
|
673
|
+
riskAdjustment="No",
|
|
674
|
+
falsePositive="No",
|
|
675
|
+
operationalRequirement="Yes",
|
|
676
|
+
deviationRationale="Pending approval",
|
|
677
|
+
severityLevel=1,
|
|
678
|
+
)
|
|
679
|
+
sheet = MagicMock()
|
|
680
|
+
|
|
681
|
+
set_risk_info(poam, 10, sheet)
|
|
682
|
+
|
|
683
|
+
assert sheet.__getitem__.called
|
|
684
|
+
|
|
685
|
+
def test_set_end_columns(self):
|
|
686
|
+
"""Test set_end_columns populates end columns correctly"""
|
|
687
|
+
with patch(
|
|
688
|
+
"regscale.integrations.public.fedramp.poam_export_v5.determine_poam_service_name"
|
|
689
|
+
) as mock_service_name, patch(
|
|
690
|
+
"regscale.integrations.public.fedramp.poam_export_v5.determine_kev_date"
|
|
691
|
+
) as mock_kev_date, patch(
|
|
692
|
+
"regscale.integrations.public.fedramp.poam_export_v5.determine_poam_comment"
|
|
693
|
+
) as mock_comment:
|
|
694
|
+
mock_service_name.return_value = "PDF Services"
|
|
695
|
+
mock_kev_date.return_value = "06/15/25"
|
|
696
|
+
mock_comment.return_value = "Test comment"
|
|
697
|
+
|
|
698
|
+
ssp = Mock()
|
|
699
|
+
poam = Mock(id=123, autoApproved="Yes", kevList="Yes", cve="CVE-2025-1234")
|
|
700
|
+
wb = Workbook()
|
|
701
|
+
sheet = wb.active
|
|
702
|
+
props = []
|
|
703
|
+
assets = []
|
|
704
|
+
# Use proper dict structure matching the code expectations
|
|
705
|
+
all_links = [
|
|
706
|
+
Mock(parentID=123, __getitem__=lambda self, key: "Link1" if key == "Title" else "http://example.com")
|
|
707
|
+
]
|
|
708
|
+
all_files = [Mock(parentId=123, __getitem__=lambda self, key: "file1.pdf")]
|
|
709
|
+
|
|
710
|
+
set_end_columns(ssp, poam, 10, sheet, props, assets, all_links, all_files)
|
|
711
|
+
|
|
712
|
+
mock_service_name.assert_called_once()
|
|
713
|
+
mock_kev_date.assert_called_once_with("CVE-2025-1234")
|
|
714
|
+
mock_comment.assert_called_once()
|
|
715
|
+
# Verify some columns were set
|
|
716
|
+
assert sheet["Y10"].value is not None
|
|
717
|
+
assert sheet["AB10"].value == "Yes"
|
|
718
|
+
assert sheet["AE10"].value == "PDF Services"
|
|
719
|
+
|
|
720
|
+
def test_map_weakness_detector_and_id_for_rev5_issues(self):
|
|
721
|
+
"""Test map_weakness_detector_and_id_for_rev5_issues sets correct values"""
|
|
722
|
+
wb = Workbook()
|
|
723
|
+
worksheet = wb.active
|
|
724
|
+
issue = Mock(sourceReport="Tenable SC", cve="CVE-2025-1234", pluginId="12345", title="Test Issue")
|
|
725
|
+
|
|
726
|
+
map_weakness_detector_and_id_for_rev5_issues(worksheet, "E", "F", 10, issue)
|
|
727
|
+
|
|
728
|
+
assert worksheet["E10"].value == "Tenable SC"
|
|
729
|
+
assert worksheet["F10"].value == "CVE-2025-1234"
|
|
730
|
+
|
|
731
|
+
def test_map_weakness_detector_and_id_no_cve(self):
|
|
732
|
+
"""Test map_weakness_detector_and_id with no CVE uses pluginId"""
|
|
733
|
+
wb = Workbook()
|
|
734
|
+
worksheet = wb.active
|
|
735
|
+
issue = Mock(sourceReport="Tenable SC", cve=None, pluginId="12345", title="Test Issue")
|
|
736
|
+
|
|
737
|
+
map_weakness_detector_and_id_for_rev5_issues(worksheet, "E", "F", 10, issue)
|
|
738
|
+
|
|
739
|
+
assert worksheet["E10"].value == "Tenable SC"
|
|
740
|
+
assert worksheet["F10"].value == "12345"
|
|
741
|
+
|
|
742
|
+
def test_map_weakness_detector_and_id_no_cve_or_plugin(self):
|
|
743
|
+
"""Test map_weakness_detector_and_id with no CVE or pluginId uses title"""
|
|
744
|
+
wb = Workbook()
|
|
745
|
+
worksheet = wb.active
|
|
746
|
+
issue = Mock(sourceReport="Tenable SC", cve=None, pluginId=None, title="Test Issue")
|
|
747
|
+
|
|
748
|
+
map_weakness_detector_and_id_for_rev5_issues(worksheet, "E", "F", 10, issue)
|
|
749
|
+
|
|
750
|
+
assert worksheet["E10"].value == "Tenable SC"
|
|
751
|
+
assert worksheet["F10"].value == "Test Issue"
|
|
752
|
+
|
|
753
|
+
def test_update_column_widths(self):
|
|
754
|
+
"""Test update_column_widths sets correct widths"""
|
|
755
|
+
wb = Workbook()
|
|
756
|
+
ws = wb.active
|
|
757
|
+
|
|
758
|
+
# Add some test data
|
|
759
|
+
ws["A1"] = "Test"
|
|
760
|
+
ws["C1"] = "Long text that should wrap"
|
|
761
|
+
|
|
762
|
+
update_column_widths(ws)
|
|
763
|
+
|
|
764
|
+
# Check that column widths are set
|
|
765
|
+
assert ws.column_dimensions["A"].width == 15
|
|
766
|
+
assert ws.column_dimensions["C"].width == 40
|
|
767
|
+
|
|
768
|
+
def test_align_column(self):
|
|
769
|
+
"""Test align_column sets text wrapping and alignment"""
|
|
770
|
+
wb = Workbook()
|
|
771
|
+
ws = wb.active
|
|
772
|
+
ws["G1"] = "Test "
|
|
773
|
+
ws["G2"] = " Test2 "
|
|
774
|
+
|
|
775
|
+
align_column("G", ws)
|
|
776
|
+
|
|
777
|
+
# Check alignment was set
|
|
778
|
+
for cell in ws["G"]:
|
|
779
|
+
if cell.value:
|
|
780
|
+
assert cell.alignment.wrap_text is True
|
|
781
|
+
assert cell.alignment.horizontal == "left"
|
|
782
|
+
|
|
783
|
+
def test_update_header(self):
|
|
784
|
+
"""Test update_header populates header information"""
|
|
785
|
+
ssp = Mock(cspOrgName="Test Org", systemName="Test System", overallCategorization="Moderate")
|
|
786
|
+
wb = Workbook()
|
|
787
|
+
sheet = wb.active
|
|
788
|
+
|
|
789
|
+
result = update_header(ssp, sheet)
|
|
790
|
+
|
|
791
|
+
assert result["A3"].value == "Test Org"
|
|
792
|
+
assert result["B3"].value == "Test System"
|
|
793
|
+
assert result["C3"].value == "Moderate"
|
|
794
|
+
# D3 should have current date
|
|
795
|
+
assert result["D3"].value is not None
|
|
796
|
+
|
|
797
|
+
def test_update_header_no_org_name(self):
|
|
798
|
+
"""Test update_header with no org name"""
|
|
799
|
+
ssp = Mock(cspOrgName=None, systemName="Test System", overallCategorization="Moderate")
|
|
800
|
+
wb = Workbook()
|
|
801
|
+
sheet = wb.active
|
|
802
|
+
|
|
803
|
+
result = update_header(ssp, sheet)
|
|
804
|
+
|
|
805
|
+
assert result["A3"].value == "N/A"
|
|
806
|
+
|
|
807
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Issue")
|
|
808
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Asset")
|
|
809
|
+
def test_get_all_poams(self, mock_asset_class, mock_issue_class):
|
|
810
|
+
"""Test get_all_poams retrieves POAMs from SSP and assets"""
|
|
811
|
+
ssp_id = "123"
|
|
812
|
+
|
|
813
|
+
# Mock SSP POAMs
|
|
814
|
+
ssp_poam1 = Mock(
|
|
815
|
+
id=1, isPoam=True, otherIdentifier="ID1", assetIdentifier="Asset1", cve="CVE-1", pluginId="P1", title="T1"
|
|
816
|
+
)
|
|
817
|
+
ssp_poam2 = Mock(
|
|
818
|
+
id=2, isPoam=True, otherIdentifier="ID2", assetIdentifier="Asset2", cve="CVE-2", pluginId="P2", title="T2"
|
|
819
|
+
)
|
|
820
|
+
mock_issue_class.get_all_by_parent.return_value = [ssp_poam1, ssp_poam2]
|
|
821
|
+
|
|
822
|
+
# Mock assets
|
|
823
|
+
asset1 = Mock(id=10)
|
|
824
|
+
mock_asset_class.get_all_by_parent.return_value = [asset1]
|
|
825
|
+
|
|
826
|
+
# Mock asset POAMs (returns empty to avoid complexity)
|
|
827
|
+
def issue_side_effect(parent_id, parent_module):
|
|
828
|
+
if parent_id == ssp_id:
|
|
829
|
+
return [ssp_poam1, ssp_poam2]
|
|
830
|
+
return []
|
|
831
|
+
|
|
832
|
+
mock_issue_class.get_all_by_parent.side_effect = issue_side_effect
|
|
833
|
+
|
|
834
|
+
result = get_all_poams(ssp_id)
|
|
835
|
+
|
|
836
|
+
assert len(result) == 2
|
|
837
|
+
assert result[0].id == 1
|
|
838
|
+
assert result[1].id == 2
|
|
839
|
+
|
|
840
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Link")
|
|
841
|
+
def test_gen_links(self, mock_link_class):
|
|
842
|
+
"""Test gen_links generates list of links"""
|
|
843
|
+
poam1 = Mock(id=1)
|
|
844
|
+
poam2 = Mock(id=2)
|
|
845
|
+
all_poams = [poam1, poam2]
|
|
846
|
+
|
|
847
|
+
link1 = {"id": 10, "title": "Link 1"}
|
|
848
|
+
link2 = {"id": 11, "title": "Link 2"}
|
|
849
|
+
mock_link_class.get_all_by_parent.side_effect = [[link1], [link2]]
|
|
850
|
+
|
|
851
|
+
result = gen_links(all_poams)
|
|
852
|
+
|
|
853
|
+
assert len(result) == 2
|
|
854
|
+
assert result[0] == link1
|
|
855
|
+
assert result[1] == link2
|
|
856
|
+
|
|
857
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.File")
|
|
858
|
+
def test_gen_files(self, mock_file_class):
|
|
859
|
+
"""Test gen_files generates list of files"""
|
|
860
|
+
api = Mock()
|
|
861
|
+
poam1 = Mock(id=1)
|
|
862
|
+
poam2 = Mock(id=2)
|
|
863
|
+
all_poams = [poam1, poam2]
|
|
864
|
+
|
|
865
|
+
file1 = {"id": 10, "name": "File 1"}
|
|
866
|
+
file2 = {"id": 11, "name": "File 2"}
|
|
867
|
+
mock_file_class.get_files_for_parent_from_regscale.side_effect = [[file1], [file2]]
|
|
868
|
+
|
|
869
|
+
result = gen_files(all_poams, api)
|
|
870
|
+
|
|
871
|
+
assert len(result) == 2
|
|
872
|
+
assert result[0] == file1
|
|
873
|
+
assert result[1] == file2
|
|
874
|
+
|
|
875
|
+
def test_gen_milestones(self):
|
|
876
|
+
"""Test gen_milestones generates list of milestones"""
|
|
877
|
+
api = Mock()
|
|
878
|
+
app = Mock()
|
|
879
|
+
app.config = {"domain": "https://test.com"}
|
|
880
|
+
|
|
881
|
+
poam1 = Mock(id=1)
|
|
882
|
+
poam2 = Mock(id=2)
|
|
883
|
+
all_poams = [poam1, poam2]
|
|
884
|
+
|
|
885
|
+
milestone1 = {"id": 10, "parent_id": 1}
|
|
886
|
+
milestone2 = {"id": 11, "parent_id": 2}
|
|
887
|
+
|
|
888
|
+
mock_response1 = Mock()
|
|
889
|
+
mock_response1.json.return_value = [milestone1]
|
|
890
|
+
mock_response2 = Mock()
|
|
891
|
+
mock_response2.json.return_value = [milestone2]
|
|
892
|
+
|
|
893
|
+
api.get.side_effect = [mock_response1, mock_response2]
|
|
894
|
+
|
|
895
|
+
result = gen_milestones(all_poams, api, app)
|
|
896
|
+
|
|
897
|
+
assert len(result) == 2
|
|
898
|
+
assert result[0] == milestone1
|
|
899
|
+
assert result[1] == milestone2
|
|
900
|
+
|
|
901
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Property")
|
|
902
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_end_columns")
|
|
903
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_risk_info")
|
|
904
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_vendor_info")
|
|
905
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_status")
|
|
906
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_milestones")
|
|
907
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.map_weakness_detector_and_id_for_rev5_issues")
|
|
908
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.strip_html")
|
|
909
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.convert_to_list")
|
|
910
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.set_short_date")
|
|
911
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.determine_poam_id")
|
|
912
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.datetime_obj")
|
|
913
|
+
def test_process_row(
|
|
914
|
+
self,
|
|
915
|
+
mock_datetime_obj,
|
|
916
|
+
mock_determine_id,
|
|
917
|
+
mock_short_date,
|
|
918
|
+
mock_convert_list,
|
|
919
|
+
mock_strip_html,
|
|
920
|
+
mock_map_weakness,
|
|
921
|
+
mock_set_milestones,
|
|
922
|
+
mock_set_status,
|
|
923
|
+
mock_set_vendor,
|
|
924
|
+
mock_set_risk,
|
|
925
|
+
mock_set_end,
|
|
926
|
+
mock_property,
|
|
927
|
+
):
|
|
928
|
+
"""Test process_row processes a POAM row correctly"""
|
|
929
|
+
ssp = Mock()
|
|
930
|
+
poam = Mock(
|
|
931
|
+
id=123,
|
|
932
|
+
sourceReport="Tenable SC",
|
|
933
|
+
title="Test Issue",
|
|
934
|
+
description="Test description",
|
|
935
|
+
assetIdentifier="Asset1",
|
|
936
|
+
remediationDescription="Fix it",
|
|
937
|
+
dateFirstDetected="2025-03-01T10:00:00",
|
|
938
|
+
dueDate="2025-04-01T10:00:00",
|
|
939
|
+
changes="Test changes",
|
|
940
|
+
)
|
|
941
|
+
sheet = MagicMock()
|
|
942
|
+
assets = []
|
|
943
|
+
all_milestones = []
|
|
944
|
+
all_links = []
|
|
945
|
+
all_files = []
|
|
946
|
+
|
|
947
|
+
mock_property.get_all_by_parent.return_value = []
|
|
948
|
+
mock_strip_html.side_effect = lambda x: x if x else ""
|
|
949
|
+
mock_convert_list.return_value = ["Asset1"]
|
|
950
|
+
mock_short_date.return_value = "03/01/25"
|
|
951
|
+
mock_determine_id.return_value = "DC-123"
|
|
952
|
+
mock_datetime_obj.return_value = datetime(2025, 4, 1, 10, 0, 0)
|
|
953
|
+
|
|
954
|
+
process_row(ssp, poam, 0, sheet, assets, all_milestones, all_links, all_files, point_of_contact="John Doe")
|
|
955
|
+
|
|
956
|
+
# Verify the index adjustment (index 0 becomes row 6)
|
|
957
|
+
assert sheet.__getitem__.called
|
|
958
|
+
mock_set_milestones.assert_called_once()
|
|
959
|
+
mock_set_status.assert_called_once()
|
|
960
|
+
mock_set_vendor.assert_called_once()
|
|
961
|
+
mock_set_risk.assert_called_once()
|
|
962
|
+
mock_set_end.assert_called_once()
|
|
963
|
+
|
|
964
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Property")
|
|
965
|
+
def test_process_row_sap_concur_normalization(self, mock_property):
|
|
966
|
+
"""Test process_row normalizes SAP Concur to Tenable SC"""
|
|
967
|
+
ssp = Mock()
|
|
968
|
+
poam = Mock(
|
|
969
|
+
id=123,
|
|
970
|
+
sourceReport="SAP Concur",
|
|
971
|
+
title="Test Issue",
|
|
972
|
+
description="",
|
|
973
|
+
assetIdentifier="",
|
|
974
|
+
remediationDescription="",
|
|
975
|
+
dateFirstDetected="2025-03-01T10:00:00",
|
|
976
|
+
dueDate=None,
|
|
977
|
+
changes="",
|
|
978
|
+
)
|
|
979
|
+
sheet = MagicMock()
|
|
980
|
+
assets = []
|
|
981
|
+
|
|
982
|
+
mock_property.get_all_by_parent.return_value = []
|
|
983
|
+
|
|
984
|
+
with patch("regscale.integrations.public.fedramp.poam_export_v5.set_milestones"), patch(
|
|
985
|
+
"regscale.integrations.public.fedramp.poam_export_v5.set_status"
|
|
986
|
+
), patch("regscale.integrations.public.fedramp.poam_export_v5.set_vendor_info"), patch(
|
|
987
|
+
"regscale.integrations.public.fedramp.poam_export_v5.set_risk_info"
|
|
988
|
+
), patch(
|
|
989
|
+
"regscale.integrations.public.fedramp.poam_export_v5.set_end_columns"
|
|
990
|
+
):
|
|
991
|
+
process_row(ssp, poam, 0, sheet, assets, [], [], [], point_of_contact="")
|
|
992
|
+
|
|
993
|
+
# Verify sourceReport was normalized
|
|
994
|
+
assert poam.sourceReport == "Tenable SC"
|
|
995
|
+
|
|
996
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.openpyxl.load_workbook")
|
|
997
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.update_header")
|
|
998
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Asset")
|
|
999
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.process_row")
|
|
1000
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.update_column_widths")
|
|
1001
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.align_column")
|
|
1002
|
+
def test_process_worksheet_open_poams(
|
|
1003
|
+
self, mock_align, mock_update_widths, mock_process_row, mock_asset, mock_update_header, mock_load_wb
|
|
1004
|
+
):
|
|
1005
|
+
"""Test process_worksheet processes open POAMs worksheet"""
|
|
1006
|
+
ssp = Mock(id=123)
|
|
1007
|
+
workbook_path = Path("/tmp/test.xlsx")
|
|
1008
|
+
all_poams = [
|
|
1009
|
+
Mock(id=1, status=IssueStatus.Open),
|
|
1010
|
+
Mock(id=2, status=IssueStatus.Open),
|
|
1011
|
+
Mock(id=3, status=IssueStatus.Closed),
|
|
1012
|
+
]
|
|
1013
|
+
|
|
1014
|
+
mock_wb = MagicMock()
|
|
1015
|
+
mock_sheet = MagicMock()
|
|
1016
|
+
mock_wb.__getitem__.return_value = mock_sheet
|
|
1017
|
+
mock_load_wb.return_value = mock_wb
|
|
1018
|
+
|
|
1019
|
+
mock_asset.get_all_by_parent.return_value = []
|
|
1020
|
+
mock_update_header.return_value = mock_sheet
|
|
1021
|
+
|
|
1022
|
+
process_worksheet(ssp, "Open POA&M Items", workbook_path, all_poams, [], [], [], point_of_contact="John Doe")
|
|
1023
|
+
|
|
1024
|
+
# Should process only 2 open POAMs
|
|
1025
|
+
assert mock_process_row.call_count == 2
|
|
1026
|
+
mock_update_widths.assert_called_once_with(mock_sheet)
|
|
1027
|
+
mock_align.assert_called_once_with("G", mock_sheet)
|
|
1028
|
+
mock_wb.save.assert_called_once_with(workbook_path)
|
|
1029
|
+
|
|
1030
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.openpyxl.load_workbook")
|
|
1031
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.update_header")
|
|
1032
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Asset")
|
|
1033
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.process_row")
|
|
1034
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.update_column_widths")
|
|
1035
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.align_column")
|
|
1036
|
+
def test_process_worksheet_closed_poams(
|
|
1037
|
+
self, mock_align, mock_update_widths, mock_process_row, mock_asset, mock_update_header, mock_load_wb
|
|
1038
|
+
):
|
|
1039
|
+
"""Test process_worksheet processes closed POAMs worksheet"""
|
|
1040
|
+
ssp = Mock(id=123)
|
|
1041
|
+
workbook_path = Path("/tmp/test.xlsx")
|
|
1042
|
+
all_poams = [
|
|
1043
|
+
Mock(id=1, status=IssueStatus.Open),
|
|
1044
|
+
Mock(id=2, status=IssueStatus.Closed),
|
|
1045
|
+
Mock(id=3, status=IssueStatus.Closed),
|
|
1046
|
+
]
|
|
1047
|
+
|
|
1048
|
+
mock_wb = MagicMock()
|
|
1049
|
+
mock_sheet = MagicMock()
|
|
1050
|
+
mock_wb.__getitem__.return_value = mock_sheet
|
|
1051
|
+
mock_load_wb.return_value = mock_wb
|
|
1052
|
+
|
|
1053
|
+
mock_asset.get_all_by_parent.return_value = []
|
|
1054
|
+
mock_update_header.return_value = mock_sheet
|
|
1055
|
+
|
|
1056
|
+
process_worksheet(
|
|
1057
|
+
ssp, "Closed POA&M Items", workbook_path, all_poams, [], [], [], point_of_contact="Jane Smith"
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
# Should process only 2 closed POAMs
|
|
1061
|
+
assert mock_process_row.call_count == 2
|
|
1062
|
+
mock_wb.save.assert_called_once()
|
|
1063
|
+
|
|
1064
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
|
|
1065
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
|
|
1066
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
|
|
1067
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
|
|
1068
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_links")
|
|
1069
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_files")
|
|
1070
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_milestones")
|
|
1071
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.shutil.copy")
|
|
1072
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.process_worksheet")
|
|
1073
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Path")
|
|
1074
|
+
def test_export_poam_v5_success(
|
|
1075
|
+
self,
|
|
1076
|
+
mock_path_class,
|
|
1077
|
+
mock_process_ws,
|
|
1078
|
+
mock_copy,
|
|
1079
|
+
mock_gen_milestones,
|
|
1080
|
+
mock_gen_files,
|
|
1081
|
+
mock_gen_links,
|
|
1082
|
+
mock_get_poams,
|
|
1083
|
+
mock_ssp_class,
|
|
1084
|
+
mock_api_class,
|
|
1085
|
+
mock_app_class,
|
|
1086
|
+
):
|
|
1087
|
+
"""Test export_poam_v5 successfully exports POAMs"""
|
|
1088
|
+
ssp_id = "123"
|
|
1089
|
+
output_file = "/tmp/output.xlsx"
|
|
1090
|
+
|
|
1091
|
+
# Mock application and API
|
|
1092
|
+
mock_app = Mock()
|
|
1093
|
+
mock_app_class.return_value = mock_app
|
|
1094
|
+
mock_api = Mock()
|
|
1095
|
+
mock_api_class.return_value = mock_api
|
|
1096
|
+
|
|
1097
|
+
# Mock SSP
|
|
1098
|
+
mock_ssp = Mock(systemName="Test System")
|
|
1099
|
+
mock_ssp_class.get_object.return_value = mock_ssp
|
|
1100
|
+
|
|
1101
|
+
# Mock POAMs
|
|
1102
|
+
mock_poam = Mock(id=1)
|
|
1103
|
+
mock_get_poams.return_value = [mock_poam]
|
|
1104
|
+
|
|
1105
|
+
# Mock related data
|
|
1106
|
+
mock_gen_links.return_value = []
|
|
1107
|
+
mock_gen_files.return_value = []
|
|
1108
|
+
mock_gen_milestones.return_value = []
|
|
1109
|
+
|
|
1110
|
+
# Mock template path
|
|
1111
|
+
mock_template_path = Mock()
|
|
1112
|
+
mock_template_path.exists.return_value = True
|
|
1113
|
+
mock_output_path = Mock()
|
|
1114
|
+
mock_output_path.suffix = ".xlsx"
|
|
1115
|
+
mock_output_path.absolute.return_value = "/tmp/output.xlsx"
|
|
1116
|
+
|
|
1117
|
+
def path_side_effect(arg):
|
|
1118
|
+
if arg == "./FedRAMP-POAM-Template.xlsx":
|
|
1119
|
+
return mock_template_path
|
|
1120
|
+
return mock_output_path
|
|
1121
|
+
|
|
1122
|
+
mock_path_class.side_effect = path_side_effect
|
|
1123
|
+
|
|
1124
|
+
export_poam_v5(ssp_id, output_file)
|
|
1125
|
+
|
|
1126
|
+
# Verify calls
|
|
1127
|
+
mock_ssp_class.get_object.assert_called_once_with(ssp_id)
|
|
1128
|
+
mock_get_poams.assert_called_once_with(ssp_id)
|
|
1129
|
+
mock_gen_links.assert_called_once()
|
|
1130
|
+
mock_gen_files.assert_called_once()
|
|
1131
|
+
mock_gen_milestones.assert_called_once()
|
|
1132
|
+
mock_copy.assert_called_once()
|
|
1133
|
+
# Should process both open and closed worksheets
|
|
1134
|
+
assert mock_process_ws.call_count == 2
|
|
1135
|
+
|
|
1136
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
|
|
1137
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
|
|
1138
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
|
|
1139
|
+
def test_export_poam_v5_ssp_not_found(self, mock_ssp_class, mock_api_class, mock_app_class):
|
|
1140
|
+
"""Test export_poam_v5 when SSP not found"""
|
|
1141
|
+
ssp_id = "999"
|
|
1142
|
+
output_file = "/tmp/output.xlsx"
|
|
1143
|
+
|
|
1144
|
+
mock_app_class.return_value = Mock()
|
|
1145
|
+
mock_api_class.return_value = Mock()
|
|
1146
|
+
mock_ssp_class.get_object.return_value = None
|
|
1147
|
+
|
|
1148
|
+
export_poam_v5(ssp_id, output_file)
|
|
1149
|
+
|
|
1150
|
+
# Should return early without processing
|
|
1151
|
+
mock_ssp_class.get_object.assert_called_once_with(ssp_id)
|
|
1152
|
+
|
|
1153
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
|
|
1154
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
|
|
1155
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
|
|
1156
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
|
|
1157
|
+
def test_export_poam_v5_no_poams(self, mock_get_poams, mock_ssp_class, mock_api_class, mock_app_class):
|
|
1158
|
+
"""Test export_poam_v5 when no POAMs found"""
|
|
1159
|
+
ssp_id = "123"
|
|
1160
|
+
output_file = "/tmp/output.xlsx"
|
|
1161
|
+
|
|
1162
|
+
mock_app_class.return_value = Mock()
|
|
1163
|
+
mock_api_class.return_value = Mock()
|
|
1164
|
+
mock_ssp = Mock(systemName="Test System")
|
|
1165
|
+
mock_ssp_class.get_object.return_value = mock_ssp
|
|
1166
|
+
mock_get_poams.return_value = []
|
|
1167
|
+
|
|
1168
|
+
export_poam_v5(ssp_id, output_file)
|
|
1169
|
+
|
|
1170
|
+
# Should return early without processing
|
|
1171
|
+
mock_get_poams.assert_called_once_with(ssp_id)
|
|
1172
|
+
|
|
1173
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
|
|
1174
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
|
|
1175
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
|
|
1176
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
|
|
1177
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_links")
|
|
1178
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_files")
|
|
1179
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_milestones")
|
|
1180
|
+
def test_export_poam_v5_template_not_found(
|
|
1181
|
+
self,
|
|
1182
|
+
mock_gen_milestones,
|
|
1183
|
+
mock_gen_files,
|
|
1184
|
+
mock_gen_links,
|
|
1185
|
+
mock_get_poams,
|
|
1186
|
+
mock_ssp_class,
|
|
1187
|
+
mock_api_class,
|
|
1188
|
+
mock_app_class,
|
|
1189
|
+
):
|
|
1190
|
+
"""Test export_poam_v5 when template file not found"""
|
|
1191
|
+
ssp_id = "123"
|
|
1192
|
+
output_file = "/tmp/output.xlsx"
|
|
1193
|
+
|
|
1194
|
+
mock_app = Mock()
|
|
1195
|
+
mock_app.config = {"domain": "https://test.com"}
|
|
1196
|
+
mock_app_class.return_value = mock_app
|
|
1197
|
+
mock_api = Mock()
|
|
1198
|
+
mock_api.config = {"domain": "https://test.com"}
|
|
1199
|
+
mock_api_class.return_value = mock_api
|
|
1200
|
+
mock_ssp = Mock(systemName="Test System")
|
|
1201
|
+
mock_ssp_class.get_object.return_value = mock_ssp
|
|
1202
|
+
mock_poam = Mock(id=1)
|
|
1203
|
+
mock_get_poams.return_value = [mock_poam]
|
|
1204
|
+
mock_gen_links.return_value = []
|
|
1205
|
+
mock_gen_files.return_value = []
|
|
1206
|
+
mock_gen_milestones.return_value = []
|
|
1207
|
+
|
|
1208
|
+
# Don't provide template_path, so it will look for default which doesn't exist
|
|
1209
|
+
export_poam_v5(ssp_id, output_file)
|
|
1210
|
+
|
|
1211
|
+
# Should return early due to missing template
|
|
1212
|
+
mock_get_poams.assert_called_once()
|
|
1213
|
+
|
|
1214
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Application")
|
|
1215
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.Api")
|
|
1216
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.SecurityPlan")
|
|
1217
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.get_all_poams")
|
|
1218
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_links")
|
|
1219
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_files")
|
|
1220
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.gen_milestones")
|
|
1221
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.shutil.copy")
|
|
1222
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.process_worksheet")
|
|
1223
|
+
def test_export_poam_v5_with_custom_template(
|
|
1224
|
+
self,
|
|
1225
|
+
mock_process_ws,
|
|
1226
|
+
mock_copy,
|
|
1227
|
+
mock_gen_milestones,
|
|
1228
|
+
mock_gen_files,
|
|
1229
|
+
mock_gen_links,
|
|
1230
|
+
mock_get_poams,
|
|
1231
|
+
mock_ssp_class,
|
|
1232
|
+
mock_api_class,
|
|
1233
|
+
mock_app_class,
|
|
1234
|
+
):
|
|
1235
|
+
"""Test export_poam_v5 with custom template path"""
|
|
1236
|
+
ssp_id = "123"
|
|
1237
|
+
output_file = "/tmp/output.xlsx"
|
|
1238
|
+
|
|
1239
|
+
mock_app_class.return_value = Mock()
|
|
1240
|
+
mock_api_class.return_value = Mock()
|
|
1241
|
+
mock_ssp = Mock(systemName="Test System")
|
|
1242
|
+
mock_ssp_class.get_object.return_value = mock_ssp
|
|
1243
|
+
mock_poam = Mock(id=1)
|
|
1244
|
+
mock_get_poams.return_value = [mock_poam]
|
|
1245
|
+
mock_gen_links.return_value = []
|
|
1246
|
+
mock_gen_files.return_value = []
|
|
1247
|
+
mock_gen_milestones.return_value = []
|
|
1248
|
+
|
|
1249
|
+
# Create a real temporary template file
|
|
1250
|
+
import tempfile
|
|
1251
|
+
|
|
1252
|
+
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as temp_template:
|
|
1253
|
+
template_path = Path(temp_template.name)
|
|
1254
|
+
# Create a simple workbook
|
|
1255
|
+
from openpyxl import Workbook
|
|
1256
|
+
|
|
1257
|
+
wb = Workbook()
|
|
1258
|
+
ws = wb.active
|
|
1259
|
+
ws.title = "Open POA&M Items"
|
|
1260
|
+
wb.create_sheet("Closed POA&M Items")
|
|
1261
|
+
wb.save(template_path)
|
|
1262
|
+
|
|
1263
|
+
try:
|
|
1264
|
+
export_poam_v5(ssp_id, output_file, template_path=template_path)
|
|
1265
|
+
|
|
1266
|
+
# Should use custom template
|
|
1267
|
+
mock_copy.assert_called_once()
|
|
1268
|
+
finally:
|
|
1269
|
+
# Clean up
|
|
1270
|
+
if template_path.exists():
|
|
1271
|
+
template_path.unlink()
|
|
1272
|
+
|
|
1273
|
+
@patch("regscale.integrations.public.fedramp.poam_export_v5.pull_cisa_kev")
|
|
1274
|
+
def test_get_cached_cisa_kev_caching(self, mock_pull_kev):
|
|
1275
|
+
"""Test get_cached_cisa_kev caches the KEV data"""
|
|
1276
|
+
mock_kev_data = {"vulnerabilities": []}
|
|
1277
|
+
mock_pull_kev.return_value = mock_kev_data
|
|
1278
|
+
|
|
1279
|
+
# Clear the cache first
|
|
1280
|
+
get_cached_cisa_kev.cache_clear()
|
|
1281
|
+
|
|
1282
|
+
# First call should fetch data
|
|
1283
|
+
result1 = get_cached_cisa_kev()
|
|
1284
|
+
assert result1 == mock_kev_data
|
|
1285
|
+
assert mock_pull_kev.call_count == 1
|
|
1286
|
+
|
|
1287
|
+
# Second call should use cache
|
|
1288
|
+
result2 = get_cached_cisa_kev()
|
|
1289
|
+
assert result2 == mock_kev_data
|
|
1290
|
+
assert mock_pull_kev.call_count == 1 # Still 1, not called again
|
|
1291
|
+
|
|
1292
|
+
# Clean up
|
|
1293
|
+
get_cached_cisa_kev.cache_clear()
|