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 @@
|
|
|
1
|
+
"""Tests for Wiz processors module."""
|
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Comprehensive unit tests for Wiz finding processors module."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from unittest.mock import MagicMock, patch, PropertyMock
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from regscale.integrations.commercial.wizv2.processors.finding import (
|
|
10
|
+
WizComplianceItem,
|
|
11
|
+
FindingConsolidator,
|
|
12
|
+
FindingToIssueProcessor,
|
|
13
|
+
)
|
|
14
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
15
|
+
from regscale.models import regscale_models
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("regscale")
|
|
18
|
+
|
|
19
|
+
PATH = "regscale.integrations.commercial.wizv2.processors.finding"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================
|
|
23
|
+
# WizComplianceItem Tests
|
|
24
|
+
# =============================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestWizComplianceItem:
|
|
28
|
+
"""Test WizComplianceItem wrapper class."""
|
|
29
|
+
|
|
30
|
+
def test_initialization(self):
|
|
31
|
+
"""Test creating WizComplianceItem wrapper."""
|
|
32
|
+
mock_item = MagicMock()
|
|
33
|
+
mock_item.resource_id = "res-123"
|
|
34
|
+
mock_item.control_id = "AC-2(1)"
|
|
35
|
+
mock_item.is_fail = True
|
|
36
|
+
|
|
37
|
+
wrapper = WizComplianceItem(mock_item)
|
|
38
|
+
|
|
39
|
+
assert wrapper._item == mock_item
|
|
40
|
+
|
|
41
|
+
def test_resource_id_property(self):
|
|
42
|
+
"""Test resource_id property returns correct value."""
|
|
43
|
+
mock_item = MagicMock()
|
|
44
|
+
mock_item.resource_id = "resource-456"
|
|
45
|
+
|
|
46
|
+
wrapper = WizComplianceItem(mock_item)
|
|
47
|
+
|
|
48
|
+
assert wrapper.resource_id == "resource-456"
|
|
49
|
+
|
|
50
|
+
def test_resource_id_property_missing_attribute(self):
|
|
51
|
+
"""Test resource_id property when attribute is missing."""
|
|
52
|
+
mock_item = MagicMock(spec=[])
|
|
53
|
+
|
|
54
|
+
wrapper = WizComplianceItem(mock_item)
|
|
55
|
+
|
|
56
|
+
assert wrapper.resource_id == ""
|
|
57
|
+
|
|
58
|
+
def test_control_id_property(self):
|
|
59
|
+
"""Test control_id property returns correct value."""
|
|
60
|
+
mock_item = MagicMock()
|
|
61
|
+
mock_item.control_id = "SC-7"
|
|
62
|
+
|
|
63
|
+
wrapper = WizComplianceItem(mock_item)
|
|
64
|
+
|
|
65
|
+
assert wrapper.control_id == "SC-7"
|
|
66
|
+
|
|
67
|
+
def test_control_id_property_missing_attribute(self):
|
|
68
|
+
"""Test control_id property when attribute is missing."""
|
|
69
|
+
mock_item = MagicMock(spec=[])
|
|
70
|
+
|
|
71
|
+
wrapper = WizComplianceItem(mock_item)
|
|
72
|
+
|
|
73
|
+
assert wrapper.control_id == ""
|
|
74
|
+
|
|
75
|
+
def test_is_fail_property(self):
|
|
76
|
+
"""Test is_fail property returns correct value."""
|
|
77
|
+
mock_item = MagicMock()
|
|
78
|
+
mock_item.is_fail = True
|
|
79
|
+
|
|
80
|
+
wrapper = WizComplianceItem(mock_item)
|
|
81
|
+
|
|
82
|
+
assert wrapper.is_fail is True
|
|
83
|
+
|
|
84
|
+
def test_is_fail_property_missing_attribute(self):
|
|
85
|
+
"""Test is_fail property when attribute is missing."""
|
|
86
|
+
mock_item = MagicMock(spec=[])
|
|
87
|
+
|
|
88
|
+
wrapper = WizComplianceItem(mock_item)
|
|
89
|
+
|
|
90
|
+
assert wrapper.is_fail is False
|
|
91
|
+
|
|
92
|
+
def test_get_all_control_ids_with_method(self):
|
|
93
|
+
"""Test get_all_control_ids when wrapped item has the method."""
|
|
94
|
+
mock_item = MagicMock()
|
|
95
|
+
mock_item.control_id = "AC-2"
|
|
96
|
+
mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2", "AC-2(1)", "AC-2(2)"])
|
|
97
|
+
|
|
98
|
+
wrapper = WizComplianceItem(mock_item)
|
|
99
|
+
result = wrapper.get_all_control_ids()
|
|
100
|
+
|
|
101
|
+
assert result == ["AC-2", "AC-2(1)", "AC-2(2)"]
|
|
102
|
+
mock_item._get_all_control_ids_for_compliance_item.assert_called_once_with(mock_item)
|
|
103
|
+
|
|
104
|
+
def test_get_all_control_ids_fallback_single_control(self):
|
|
105
|
+
"""Test get_all_control_ids falls back to control_id."""
|
|
106
|
+
mock_item = MagicMock()
|
|
107
|
+
mock_item.control_id = "SC-7"
|
|
108
|
+
del mock_item._get_all_control_ids_for_compliance_item
|
|
109
|
+
|
|
110
|
+
wrapper = WizComplianceItem(mock_item)
|
|
111
|
+
result = wrapper.get_all_control_ids()
|
|
112
|
+
|
|
113
|
+
assert result == ["SC-7"]
|
|
114
|
+
|
|
115
|
+
def test_get_all_control_ids_fallback_no_control(self):
|
|
116
|
+
"""Test get_all_control_ids when no control_id exists."""
|
|
117
|
+
mock_item = MagicMock(spec=[])
|
|
118
|
+
|
|
119
|
+
wrapper = WizComplianceItem(mock_item)
|
|
120
|
+
result = wrapper.get_all_control_ids()
|
|
121
|
+
|
|
122
|
+
assert result == []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# =============================
|
|
126
|
+
# FindingConsolidator Tests
|
|
127
|
+
# =============================
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestFindingConsolidator:
|
|
131
|
+
"""Test FindingConsolidator class."""
|
|
132
|
+
|
|
133
|
+
def setup_method(self):
|
|
134
|
+
"""Set up test fixtures."""
|
|
135
|
+
self.mock_integration = MagicMock()
|
|
136
|
+
self.consolidator = FindingConsolidator(self.mock_integration)
|
|
137
|
+
|
|
138
|
+
def test_initialization(self):
|
|
139
|
+
"""Test FindingConsolidator initialization."""
|
|
140
|
+
assert self.consolidator.integration == self.mock_integration
|
|
141
|
+
assert self.consolidator.asset_consolidator is not None
|
|
142
|
+
|
|
143
|
+
def test_create_consolidated_findings_empty_list(self):
|
|
144
|
+
"""Test creating findings with empty list."""
|
|
145
|
+
result = list(self.consolidator.create_consolidated_findings([]))
|
|
146
|
+
|
|
147
|
+
assert result == []
|
|
148
|
+
|
|
149
|
+
def test_create_consolidated_findings_no_control_groups(self):
|
|
150
|
+
"""Test creating findings when no control groups are created."""
|
|
151
|
+
mock_item = MagicMock()
|
|
152
|
+
mock_item.resource_id = ""
|
|
153
|
+
mock_item.control_id = ""
|
|
154
|
+
mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=[])
|
|
155
|
+
|
|
156
|
+
result = list(self.consolidator.create_consolidated_findings([mock_item]))
|
|
157
|
+
|
|
158
|
+
assert result == []
|
|
159
|
+
|
|
160
|
+
@patch(f"{PATH}.FindingConsolidator._create_consolidated_finding_for_control")
|
|
161
|
+
def test_create_consolidated_findings_success(self, mock_create_finding):
|
|
162
|
+
"""Test creating consolidated findings successfully."""
|
|
163
|
+
# Create mock compliance items
|
|
164
|
+
item1 = MagicMock()
|
|
165
|
+
item1.resource_id = "res-1"
|
|
166
|
+
item1.control_id = "AC-2"
|
|
167
|
+
item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
168
|
+
|
|
169
|
+
item2 = MagicMock()
|
|
170
|
+
item2.resource_id = "res-2"
|
|
171
|
+
item2.control_id = "AC-2"
|
|
172
|
+
item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
173
|
+
|
|
174
|
+
# Mock finding creation
|
|
175
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
176
|
+
mock_create_finding.return_value = mock_finding
|
|
177
|
+
|
|
178
|
+
result = list(self.consolidator.create_consolidated_findings([item1, item2]))
|
|
179
|
+
|
|
180
|
+
assert len(result) == 1
|
|
181
|
+
assert result[0] == mock_finding
|
|
182
|
+
mock_create_finding.assert_called_once()
|
|
183
|
+
|
|
184
|
+
@patch(f"{PATH}.FindingConsolidator._create_consolidated_finding_for_control")
|
|
185
|
+
def test_create_consolidated_findings_multiple_controls(self, mock_create_finding):
|
|
186
|
+
"""Test creating findings for multiple controls."""
|
|
187
|
+
item1 = MagicMock()
|
|
188
|
+
item1.resource_id = "res-1"
|
|
189
|
+
item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2", "SC-7"])
|
|
190
|
+
|
|
191
|
+
mock_finding1 = MagicMock(spec=IntegrationFinding)
|
|
192
|
+
mock_finding2 = MagicMock(spec=IntegrationFinding)
|
|
193
|
+
mock_create_finding.side_effect = [mock_finding1, mock_finding2]
|
|
194
|
+
|
|
195
|
+
result = list(self.consolidator.create_consolidated_findings([item1]))
|
|
196
|
+
|
|
197
|
+
assert len(result) == 2
|
|
198
|
+
assert mock_create_finding.call_count == 2
|
|
199
|
+
|
|
200
|
+
def test_group_by_control_empty_list(self):
|
|
201
|
+
"""Test grouping empty compliance items."""
|
|
202
|
+
result = self.consolidator._group_by_control([])
|
|
203
|
+
|
|
204
|
+
assert result == {}
|
|
205
|
+
|
|
206
|
+
def test_group_by_control_single_control_single_resource(self):
|
|
207
|
+
"""Test grouping single control with single resource."""
|
|
208
|
+
mock_item = MagicMock()
|
|
209
|
+
mock_item.resource_id = "res-123"
|
|
210
|
+
mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
211
|
+
|
|
212
|
+
result = self.consolidator._group_by_control([mock_item])
|
|
213
|
+
|
|
214
|
+
assert "AC-2" in result
|
|
215
|
+
assert "res-123" in result["AC-2"]
|
|
216
|
+
assert result["AC-2"]["res-123"] == mock_item
|
|
217
|
+
|
|
218
|
+
def test_group_by_control_multiple_resources_same_control(self):
|
|
219
|
+
"""Test grouping multiple resources for same control."""
|
|
220
|
+
item1 = MagicMock()
|
|
221
|
+
item1.resource_id = "res-1"
|
|
222
|
+
item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
223
|
+
|
|
224
|
+
item2 = MagicMock()
|
|
225
|
+
item2.resource_id = "res-2"
|
|
226
|
+
item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
227
|
+
|
|
228
|
+
result = self.consolidator._group_by_control([item1, item2])
|
|
229
|
+
|
|
230
|
+
assert len(result["AC-2"]) == 2
|
|
231
|
+
assert "res-1" in result["AC-2"]
|
|
232
|
+
assert "res-2" in result["AC-2"]
|
|
233
|
+
|
|
234
|
+
def test_group_by_control_case_normalization(self):
|
|
235
|
+
"""Test that control IDs are normalized to uppercase."""
|
|
236
|
+
item1 = MagicMock()
|
|
237
|
+
item1.resource_id = "res-1"
|
|
238
|
+
item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["ac-2"])
|
|
239
|
+
|
|
240
|
+
item2 = MagicMock()
|
|
241
|
+
item2.resource_id = "res-2"
|
|
242
|
+
item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
243
|
+
|
|
244
|
+
result = self.consolidator._group_by_control([item1, item2])
|
|
245
|
+
|
|
246
|
+
assert "AC-2" in result
|
|
247
|
+
assert len(result["AC-2"]) == 2
|
|
248
|
+
|
|
249
|
+
def test_group_by_control_resource_id_normalization(self):
|
|
250
|
+
"""Test that resource IDs are normalized to lowercase."""
|
|
251
|
+
item1 = MagicMock()
|
|
252
|
+
item1.resource_id = "RES-1"
|
|
253
|
+
item1._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
254
|
+
|
|
255
|
+
item2 = MagicMock()
|
|
256
|
+
item2.resource_id = "res-1"
|
|
257
|
+
item2._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
258
|
+
|
|
259
|
+
result = self.consolidator._group_by_control([item1, item2])
|
|
260
|
+
|
|
261
|
+
# Should only have one resource (first occurrence wins)
|
|
262
|
+
assert len(result["AC-2"]) == 1
|
|
263
|
+
assert "res-1" in result["AC-2"]
|
|
264
|
+
|
|
265
|
+
def test_group_by_control_multiple_controls_per_item(self):
|
|
266
|
+
"""Test grouping item that maps to multiple controls."""
|
|
267
|
+
mock_item = MagicMock()
|
|
268
|
+
mock_item.resource_id = "res-1"
|
|
269
|
+
mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2", "SC-7", "AU-2"])
|
|
270
|
+
|
|
271
|
+
result = self.consolidator._group_by_control([mock_item])
|
|
272
|
+
|
|
273
|
+
assert len(result) == 3
|
|
274
|
+
assert "AC-2" in result
|
|
275
|
+
assert "SC-7" in result
|
|
276
|
+
assert "AU-2" in result
|
|
277
|
+
assert result["AC-2"]["res-1"] == mock_item
|
|
278
|
+
assert result["SC-7"]["res-1"] == mock_item
|
|
279
|
+
assert result["AU-2"]["res-1"] == mock_item
|
|
280
|
+
|
|
281
|
+
def test_group_by_control_skip_empty_resource_id(self):
|
|
282
|
+
"""Test that items without resource_id are skipped."""
|
|
283
|
+
mock_item = MagicMock()
|
|
284
|
+
mock_item.resource_id = ""
|
|
285
|
+
mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=["AC-2"])
|
|
286
|
+
|
|
287
|
+
result = self.consolidator._group_by_control([mock_item])
|
|
288
|
+
|
|
289
|
+
assert result == {}
|
|
290
|
+
|
|
291
|
+
def test_group_by_control_skip_empty_control_ids(self):
|
|
292
|
+
"""Test that items without control IDs are skipped."""
|
|
293
|
+
mock_item = MagicMock()
|
|
294
|
+
mock_item.resource_id = "res-1"
|
|
295
|
+
mock_item._get_all_control_ids_for_compliance_item = MagicMock(return_value=[])
|
|
296
|
+
|
|
297
|
+
result = self.consolidator._group_by_control([mock_item])
|
|
298
|
+
|
|
299
|
+
assert result == {}
|
|
300
|
+
|
|
301
|
+
@patch(f"{PATH}.FindingConsolidator._update_finding_with_assets")
|
|
302
|
+
@patch(f"{PATH}.FindingConsolidator._create_base_finding")
|
|
303
|
+
@patch(f"{PATH}.FindingConsolidator._build_asset_mappings")
|
|
304
|
+
def test_create_consolidated_finding_for_control_success(
|
|
305
|
+
self, mock_build_mappings, mock_create_base, mock_update_finding
|
|
306
|
+
):
|
|
307
|
+
"""Test creating consolidated finding successfully."""
|
|
308
|
+
resources = {"res-1": MagicMock(), "res-2": MagicMock()}
|
|
309
|
+
asset_mappings = {"res-1": {"name": "Asset 1", "wiz_id": "res-1"}}
|
|
310
|
+
|
|
311
|
+
mock_build_mappings.return_value = asset_mappings
|
|
312
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
313
|
+
mock_create_base.return_value = mock_finding
|
|
314
|
+
|
|
315
|
+
result = self.consolidator._create_consolidated_finding_for_control("AC-2", resources)
|
|
316
|
+
|
|
317
|
+
assert result == mock_finding
|
|
318
|
+
mock_build_mappings.assert_called_once()
|
|
319
|
+
mock_create_base.assert_called_once()
|
|
320
|
+
mock_update_finding.assert_called_once_with(mock_finding, asset_mappings)
|
|
321
|
+
|
|
322
|
+
@patch(f"{PATH}.FindingConsolidator._build_asset_mappings")
|
|
323
|
+
def test_create_consolidated_finding_for_control_no_assets(self, mock_build_mappings):
|
|
324
|
+
"""Test creating finding when no assets exist in RegScale."""
|
|
325
|
+
resources = {"res-1": MagicMock()}
|
|
326
|
+
mock_build_mappings.return_value = {}
|
|
327
|
+
|
|
328
|
+
result = self.consolidator._create_consolidated_finding_for_control("AC-2", resources)
|
|
329
|
+
|
|
330
|
+
assert result is None
|
|
331
|
+
|
|
332
|
+
@patch(f"{PATH}.FindingConsolidator._create_base_finding")
|
|
333
|
+
@patch(f"{PATH}.FindingConsolidator._build_asset_mappings")
|
|
334
|
+
def test_create_consolidated_finding_for_control_base_finding_fails(self, mock_build_mappings, mock_create_base):
|
|
335
|
+
"""Test handling when base finding creation fails."""
|
|
336
|
+
resources = {"res-1": MagicMock()}
|
|
337
|
+
asset_mappings = {"res-1": {"name": "Asset 1", "wiz_id": "res-1"}}
|
|
338
|
+
|
|
339
|
+
mock_build_mappings.return_value = asset_mappings
|
|
340
|
+
mock_create_base.return_value = None
|
|
341
|
+
|
|
342
|
+
result = self.consolidator._create_consolidated_finding_for_control("AC-2", resources)
|
|
343
|
+
|
|
344
|
+
assert result is None
|
|
345
|
+
|
|
346
|
+
def test_build_asset_mappings_all_assets_exist(self):
|
|
347
|
+
"""Test building asset mappings when all assets exist."""
|
|
348
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
349
|
+
|
|
350
|
+
mock_asset1 = MagicMock()
|
|
351
|
+
mock_asset1.name = "Asset 1"
|
|
352
|
+
mock_asset2 = MagicMock()
|
|
353
|
+
mock_asset2.name = "Asset 2"
|
|
354
|
+
|
|
355
|
+
self.mock_integration.get_asset_by_identifier.side_effect = [mock_asset1, mock_asset2]
|
|
356
|
+
|
|
357
|
+
result = self.consolidator._build_asset_mappings(["res-1", "res-2"])
|
|
358
|
+
|
|
359
|
+
assert len(result) == 2
|
|
360
|
+
assert result["res-1"]["name"] == "Asset 1"
|
|
361
|
+
assert result["res-1"]["wiz_id"] == "res-1"
|
|
362
|
+
assert result["res-2"]["name"] == "Asset 2"
|
|
363
|
+
assert result["res-2"]["wiz_id"] == "res-2"
|
|
364
|
+
|
|
365
|
+
def test_build_asset_mappings_some_assets_missing(self):
|
|
366
|
+
"""Test building asset mappings when some assets don't exist."""
|
|
367
|
+
self.mock_integration._asset_exists_in_regscale.side_effect = [True, False, True]
|
|
368
|
+
|
|
369
|
+
mock_asset1 = MagicMock()
|
|
370
|
+
mock_asset1.name = "Asset 1"
|
|
371
|
+
mock_asset3 = MagicMock()
|
|
372
|
+
mock_asset3.name = "Asset 3"
|
|
373
|
+
|
|
374
|
+
self.mock_integration.get_asset_by_identifier.side_effect = [mock_asset1, mock_asset3]
|
|
375
|
+
|
|
376
|
+
result = self.consolidator._build_asset_mappings(["res-1", "res-2", "res-3"])
|
|
377
|
+
|
|
378
|
+
assert len(result) == 2
|
|
379
|
+
assert "res-1" in result
|
|
380
|
+
assert "res-2" not in result
|
|
381
|
+
assert "res-3" in result
|
|
382
|
+
|
|
383
|
+
def test_build_asset_mappings_asset_has_no_name(self):
|
|
384
|
+
"""Test building asset mappings when asset has no name."""
|
|
385
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
386
|
+
|
|
387
|
+
mock_asset = MagicMock(spec=[])
|
|
388
|
+
self.mock_integration.get_asset_by_identifier.return_value = mock_asset
|
|
389
|
+
|
|
390
|
+
result = self.consolidator._build_asset_mappings(["res-1"])
|
|
391
|
+
|
|
392
|
+
# Should fall back to resource ID
|
|
393
|
+
assert result["res-1"]["name"] == "res-1"
|
|
394
|
+
assert result["res-1"]["wiz_id"] == "res-1"
|
|
395
|
+
|
|
396
|
+
def test_build_asset_mappings_get_asset_returns_none(self):
|
|
397
|
+
"""Test building asset mappings when get_asset returns None."""
|
|
398
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
399
|
+
self.mock_integration.get_asset_by_identifier.return_value = None
|
|
400
|
+
|
|
401
|
+
result = self.consolidator._build_asset_mappings(["res-1"])
|
|
402
|
+
|
|
403
|
+
# Should fall back to resource ID
|
|
404
|
+
assert result["res-1"]["name"] == "res-1"
|
|
405
|
+
assert result["res-1"]["wiz_id"] == "res-1"
|
|
406
|
+
|
|
407
|
+
def test_create_base_finding_with_specific_control_method(self):
|
|
408
|
+
"""Test creating base finding using specific control method."""
|
|
409
|
+
mock_item = MagicMock()
|
|
410
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
411
|
+
|
|
412
|
+
self.mock_integration._create_finding_for_specific_control = MagicMock(return_value=mock_finding)
|
|
413
|
+
|
|
414
|
+
result = self.consolidator._create_base_finding(mock_item, "AC-2")
|
|
415
|
+
|
|
416
|
+
assert result == mock_finding
|
|
417
|
+
self.mock_integration._create_finding_for_specific_control.assert_called_once_with(mock_item, "AC-2")
|
|
418
|
+
|
|
419
|
+
def test_create_base_finding_fallback_to_generic_method(self):
|
|
420
|
+
"""Test creating base finding using generic method."""
|
|
421
|
+
mock_item = MagicMock()
|
|
422
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
423
|
+
|
|
424
|
+
del self.mock_integration._create_finding_for_specific_control
|
|
425
|
+
self.mock_integration.create_finding_from_compliance_item = MagicMock(return_value=mock_finding)
|
|
426
|
+
|
|
427
|
+
result = self.consolidator._create_base_finding(mock_item, "AC-2")
|
|
428
|
+
|
|
429
|
+
assert result == mock_finding
|
|
430
|
+
self.mock_integration.create_finding_from_compliance_item.assert_called_once_with(mock_item)
|
|
431
|
+
|
|
432
|
+
def test_create_base_finding_exception_handling(self):
|
|
433
|
+
"""Test exception handling during base finding creation."""
|
|
434
|
+
mock_item = MagicMock()
|
|
435
|
+
|
|
436
|
+
self.mock_integration._create_finding_for_specific_control = MagicMock(side_effect=Exception("Creation failed"))
|
|
437
|
+
|
|
438
|
+
result = self.consolidator._create_base_finding(mock_item, "AC-2")
|
|
439
|
+
|
|
440
|
+
assert result is None
|
|
441
|
+
|
|
442
|
+
def test_update_finding_with_assets(self):
|
|
443
|
+
"""Test updating finding with consolidated asset information."""
|
|
444
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
445
|
+
mock_finding.description = "Control failure"
|
|
446
|
+
|
|
447
|
+
asset_mappings = {"res-1": {"name": "Asset 1"}, "res-2": {"name": "Asset 2"}}
|
|
448
|
+
|
|
449
|
+
self.consolidator._update_finding_with_assets(mock_finding, asset_mappings)
|
|
450
|
+
|
|
451
|
+
# Verify asset_identifier was set (mocked consolidator would have been called)
|
|
452
|
+
assert mock_finding.asset_identifier is not None
|
|
453
|
+
# Description should be updated with multiple assets
|
|
454
|
+
assert "2 assets" in mock_finding.description
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# =============================
|
|
458
|
+
# FindingToIssueProcessor Tests
|
|
459
|
+
# =============================
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class TestFindingToIssueProcessor:
|
|
463
|
+
"""Test FindingToIssueProcessor class."""
|
|
464
|
+
|
|
465
|
+
def setup_method(self):
|
|
466
|
+
"""Set up test fixtures."""
|
|
467
|
+
self.mock_integration = MagicMock()
|
|
468
|
+
self.processor = FindingToIssueProcessor(self.mock_integration)
|
|
469
|
+
|
|
470
|
+
def test_initialization(self):
|
|
471
|
+
"""Test FindingToIssueProcessor initialization."""
|
|
472
|
+
assert self.processor.integration == self.mock_integration
|
|
473
|
+
|
|
474
|
+
def test_process_findings_to_issues_empty_list(self):
|
|
475
|
+
"""Test processing empty findings list."""
|
|
476
|
+
created, skipped = self.processor.process_findings_to_issues([])
|
|
477
|
+
|
|
478
|
+
assert created == 0
|
|
479
|
+
assert skipped == 0
|
|
480
|
+
|
|
481
|
+
@patch(f"{PATH}.FindingToIssueProcessor._process_single_finding")
|
|
482
|
+
def test_process_findings_to_issues_all_successful(self, mock_process_single):
|
|
483
|
+
"""Test processing findings where all succeed."""
|
|
484
|
+
mock_process_single.return_value = True
|
|
485
|
+
|
|
486
|
+
findings = [MagicMock(spec=IntegrationFinding) for _ in range(3)]
|
|
487
|
+
created, skipped = self.processor.process_findings_to_issues(findings) # type: ignore[arg-type]
|
|
488
|
+
|
|
489
|
+
assert created == 3
|
|
490
|
+
assert skipped == 0
|
|
491
|
+
assert mock_process_single.call_count == 3
|
|
492
|
+
|
|
493
|
+
@patch(f"{PATH}.FindingToIssueProcessor._process_single_finding")
|
|
494
|
+
def test_process_findings_to_issues_some_skipped(self, mock_process_single):
|
|
495
|
+
"""Test processing findings where some are skipped."""
|
|
496
|
+
mock_process_single.side_effect = [True, False, True, False]
|
|
497
|
+
|
|
498
|
+
findings = [MagicMock(spec=IntegrationFinding) for _ in range(4)]
|
|
499
|
+
created, skipped = self.processor.process_findings_to_issues(findings) # type: ignore[arg-type]
|
|
500
|
+
|
|
501
|
+
assert created == 2
|
|
502
|
+
assert skipped == 2
|
|
503
|
+
|
|
504
|
+
@patch(f"{PATH}.FindingToIssueProcessor._process_single_finding")
|
|
505
|
+
def test_process_findings_to_issues_exception_handling(self, mock_process_single):
|
|
506
|
+
"""Test exception handling during finding processing."""
|
|
507
|
+
mock_process_single.side_effect = [True, Exception("Processing error"), True]
|
|
508
|
+
|
|
509
|
+
findings = [MagicMock(spec=IntegrationFinding) for _ in range(3)]
|
|
510
|
+
created, skipped = self.processor.process_findings_to_issues(findings) # type: ignore[arg-type]
|
|
511
|
+
|
|
512
|
+
assert created == 2
|
|
513
|
+
assert skipped == 1
|
|
514
|
+
|
|
515
|
+
@patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
|
|
516
|
+
def test_process_single_finding_assets_not_found(self, mock_verify):
|
|
517
|
+
"""Test processing single finding when assets don't exist."""
|
|
518
|
+
mock_verify.return_value = False
|
|
519
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
520
|
+
mock_finding.external_id = "finding-123"
|
|
521
|
+
|
|
522
|
+
result = self.processor._process_single_finding(mock_finding)
|
|
523
|
+
|
|
524
|
+
assert result is False
|
|
525
|
+
mock_verify.assert_called_once_with(mock_finding)
|
|
526
|
+
|
|
527
|
+
@patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
|
|
528
|
+
def test_process_single_finding_success(self, mock_verify):
|
|
529
|
+
"""Test successfully processing single finding."""
|
|
530
|
+
mock_verify.return_value = True
|
|
531
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
532
|
+
|
|
533
|
+
self.mock_integration.get_issue_title.return_value = "Issue Title"
|
|
534
|
+
mock_issue = MagicMock(spec=regscale_models.Issue)
|
|
535
|
+
self.mock_integration.create_or_update_issue_from_finding.return_value = mock_issue
|
|
536
|
+
|
|
537
|
+
result = self.processor._process_single_finding(mock_finding)
|
|
538
|
+
|
|
539
|
+
assert result is True
|
|
540
|
+
self.mock_integration.get_issue_title.assert_called_once_with(mock_finding)
|
|
541
|
+
self.mock_integration.create_or_update_issue_from_finding.assert_called_once()
|
|
542
|
+
|
|
543
|
+
@patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
|
|
544
|
+
def test_process_single_finding_issue_creation_fails(self, mock_verify):
|
|
545
|
+
"""Test processing when issue creation returns None."""
|
|
546
|
+
mock_verify.return_value = True
|
|
547
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
548
|
+
|
|
549
|
+
self.mock_integration.get_issue_title.return_value = "Issue Title"
|
|
550
|
+
self.mock_integration.create_or_update_issue_from_finding.return_value = None
|
|
551
|
+
|
|
552
|
+
result = self.processor._process_single_finding(mock_finding)
|
|
553
|
+
|
|
554
|
+
assert result is False
|
|
555
|
+
|
|
556
|
+
@patch(f"{PATH}.FindingToIssueProcessor._verify_assets_exist")
|
|
557
|
+
def test_process_single_finding_exception_during_creation(self, mock_verify):
|
|
558
|
+
"""Test exception handling during issue creation."""
|
|
559
|
+
mock_verify.return_value = True
|
|
560
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
561
|
+
|
|
562
|
+
self.mock_integration.get_issue_title.side_effect = Exception("Title generation failed")
|
|
563
|
+
|
|
564
|
+
result = self.processor._process_single_finding(mock_finding)
|
|
565
|
+
|
|
566
|
+
assert result is False
|
|
567
|
+
|
|
568
|
+
def test_verify_assets_exist_no_asset_identifier(self):
|
|
569
|
+
"""Test verification when finding has no asset_identifier."""
|
|
570
|
+
mock_finding = MagicMock(spec=[])
|
|
571
|
+
|
|
572
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
573
|
+
|
|
574
|
+
assert result is False
|
|
575
|
+
|
|
576
|
+
def test_verify_assets_exist_empty_asset_identifier(self):
|
|
577
|
+
"""Test verification when asset_identifier is empty."""
|
|
578
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
579
|
+
mock_finding.asset_identifier = ""
|
|
580
|
+
|
|
581
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
582
|
+
|
|
583
|
+
assert result is False
|
|
584
|
+
|
|
585
|
+
def test_verify_assets_exist_single_asset_exists(self):
|
|
586
|
+
"""Test verification for single existing asset."""
|
|
587
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
588
|
+
mock_finding.asset_identifier = "Asset 1 (res-123)"
|
|
589
|
+
|
|
590
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
591
|
+
|
|
592
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
593
|
+
|
|
594
|
+
assert result is True
|
|
595
|
+
self.mock_integration._asset_exists_in_regscale.assert_called_once_with("res-123")
|
|
596
|
+
|
|
597
|
+
def test_verify_assets_exist_single_asset_not_exists(self):
|
|
598
|
+
"""Test verification when single asset doesn't exist."""
|
|
599
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
600
|
+
mock_finding.asset_identifier = "Asset 1 (res-123)"
|
|
601
|
+
|
|
602
|
+
self.mock_integration._asset_exists_in_regscale.return_value = False
|
|
603
|
+
|
|
604
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
605
|
+
|
|
606
|
+
assert result is False
|
|
607
|
+
|
|
608
|
+
def test_verify_assets_exist_multiple_assets_all_exist(self):
|
|
609
|
+
"""Test verification for multiple existing assets."""
|
|
610
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
611
|
+
mock_finding.asset_identifier = "Asset 1 (res-1)\nAsset 2 (res-2)\nAsset 3 (res-3)"
|
|
612
|
+
|
|
613
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
614
|
+
|
|
615
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
616
|
+
|
|
617
|
+
assert result is True
|
|
618
|
+
assert self.mock_integration._asset_exists_in_regscale.call_count == 3
|
|
619
|
+
|
|
620
|
+
def test_verify_assets_exist_multiple_assets_one_missing(self):
|
|
621
|
+
"""Test verification when one asset in multiple is missing."""
|
|
622
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
623
|
+
mock_finding.asset_identifier = "Asset 1 (res-1)\nAsset 2 (res-2)"
|
|
624
|
+
|
|
625
|
+
self.mock_integration._asset_exists_in_regscale.side_effect = [True, False]
|
|
626
|
+
|
|
627
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
628
|
+
|
|
629
|
+
assert result is False
|
|
630
|
+
|
|
631
|
+
def test_verify_assets_exist_skip_empty_lines(self):
|
|
632
|
+
"""Test verification skips empty lines in asset_identifier."""
|
|
633
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
634
|
+
mock_finding.asset_identifier = "Asset 1 (res-1)\n\n\nAsset 2 (res-2)"
|
|
635
|
+
|
|
636
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
637
|
+
|
|
638
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
639
|
+
|
|
640
|
+
assert result is True
|
|
641
|
+
# Should only check 2 assets, not 4
|
|
642
|
+
assert self.mock_integration._asset_exists_in_regscale.call_count == 2
|
|
643
|
+
|
|
644
|
+
def test_verify_assets_exist_identifier_without_parentheses(self):
|
|
645
|
+
"""Test verification with identifier that has no parentheses."""
|
|
646
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
647
|
+
mock_finding.asset_identifier = "res-simple-id"
|
|
648
|
+
|
|
649
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
650
|
+
|
|
651
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
652
|
+
|
|
653
|
+
assert result is True
|
|
654
|
+
self.mock_integration._asset_exists_in_regscale.assert_called_once_with("res-simple-id")
|
|
655
|
+
|
|
656
|
+
def test_verify_assets_exist_identifier_extraction(self):
|
|
657
|
+
"""Test proper extraction of resource ID from formatted identifier."""
|
|
658
|
+
mock_finding = MagicMock(spec=IntegrationFinding)
|
|
659
|
+
mock_finding.asset_identifier = "Complex Asset Name (res-complex-123)"
|
|
660
|
+
|
|
661
|
+
self.mock_integration._asset_exists_in_regscale.return_value = True
|
|
662
|
+
|
|
663
|
+
result = self.processor._verify_assets_exist(mock_finding)
|
|
664
|
+
|
|
665
|
+
assert result is True
|
|
666
|
+
self.mock_integration._asset_exists_in_regscale.assert_called_once_with("res-complex-123")
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# Run tests with pytest
|
|
670
|
+
if __name__ == "__main__":
|
|
671
|
+
pytest.main([__file__, "-v"])
|