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,1397 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Unit and integration tests for control_matcher module
|
|
5
|
+
|
|
6
|
+
This module provides comprehensive test coverage for the ControlMatcher class,
|
|
7
|
+
including control ID parsing, catalog searches, implementation matching, and caching.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from regscale.core.app.api import Api
|
|
17
|
+
from regscale.core.app.application import Application
|
|
18
|
+
from regscale.integrations.control_matcher import ControlMatcher
|
|
19
|
+
from regscale.models.regscale_models.control_implementation import ControlImplementation
|
|
20
|
+
from regscale.models.regscale_models.security_control import SecurityControl
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestControlMatcherInit:
|
|
24
|
+
"""Test cases for ControlMatcher initialization"""
|
|
25
|
+
|
|
26
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
27
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
28
|
+
def test_init_with_no_app(self, mock_app_class, mock_api_class):
|
|
29
|
+
"""Test ControlMatcher initialization without providing an app"""
|
|
30
|
+
mock_app = MagicMock(spec=Application)
|
|
31
|
+
mock_app_class.return_value = mock_app
|
|
32
|
+
mock_api = MagicMock(spec=Api)
|
|
33
|
+
mock_api_class.return_value = mock_api
|
|
34
|
+
|
|
35
|
+
matcher = ControlMatcher()
|
|
36
|
+
|
|
37
|
+
mock_app_class.assert_called_once()
|
|
38
|
+
mock_api_class.assert_called_once()
|
|
39
|
+
assert matcher.app == mock_app
|
|
40
|
+
assert matcher.api == mock_api
|
|
41
|
+
assert matcher._catalog_cache == {}
|
|
42
|
+
assert matcher._control_impl_cache == {}
|
|
43
|
+
|
|
44
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
45
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
46
|
+
def test_init_with_app(self, mock_app_class, mock_api_class):
|
|
47
|
+
"""Test ControlMatcher initialization with an app instance"""
|
|
48
|
+
# Create a mock app that is truthy (has a return value)
|
|
49
|
+
mock_app = Mock(spec=Application)
|
|
50
|
+
# Set a non-None return value to make the mock truthy
|
|
51
|
+
mock_app.return_value = MagicMock()
|
|
52
|
+
|
|
53
|
+
mock_api = MagicMock(spec=Api)
|
|
54
|
+
mock_api_class.return_value = mock_api
|
|
55
|
+
|
|
56
|
+
matcher = ControlMatcher(app=mock_app)
|
|
57
|
+
|
|
58
|
+
# When app is provided and is truthy, it should be used
|
|
59
|
+
assert matcher.app == mock_app
|
|
60
|
+
assert matcher.api == mock_api
|
|
61
|
+
assert matcher._catalog_cache == {}
|
|
62
|
+
assert matcher._control_impl_cache == {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestControlMatcherParseControlId:
|
|
66
|
+
"""Test cases for parse_control_id method"""
|
|
67
|
+
|
|
68
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
69
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
70
|
+
def test_parse_control_id_none(self, mock_app_class, mock_api_class):
|
|
71
|
+
"""Test parsing None control ID returns None"""
|
|
72
|
+
matcher = ControlMatcher()
|
|
73
|
+
result = matcher.parse_control_id(None)
|
|
74
|
+
assert result is None
|
|
75
|
+
|
|
76
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
77
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
78
|
+
def test_parse_control_id_empty_string(self, mock_app_class, mock_api_class):
|
|
79
|
+
"""Test parsing empty string returns None"""
|
|
80
|
+
matcher = ControlMatcher()
|
|
81
|
+
result = matcher.parse_control_id("")
|
|
82
|
+
assert result is None
|
|
83
|
+
|
|
84
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
85
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
86
|
+
def test_parse_control_id_whitespace(self, mock_app_class, mock_api_class):
|
|
87
|
+
"""Test parsing whitespace-only string returns None"""
|
|
88
|
+
matcher = ControlMatcher()
|
|
89
|
+
result = matcher.parse_control_id(" ")
|
|
90
|
+
assert result is None
|
|
91
|
+
|
|
92
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
93
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
94
|
+
def test_parse_control_id_basic_format(self, mock_app_class, mock_api_class):
|
|
95
|
+
"""Test parsing basic NIST control ID format"""
|
|
96
|
+
matcher = ControlMatcher()
|
|
97
|
+
test_cases = [
|
|
98
|
+
("AC-1", "AC-1"),
|
|
99
|
+
("ac-1", "AC-1"),
|
|
100
|
+
("AC-10", "AC-10"),
|
|
101
|
+
("SI-2", "SI-2"),
|
|
102
|
+
("CM-6", "CM-6"),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
for input_id, expected in test_cases:
|
|
106
|
+
result = matcher.parse_control_id(input_id)
|
|
107
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
108
|
+
|
|
109
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
110
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
111
|
+
def test_parse_control_id_with_parentheses(self, mock_app_class, mock_api_class):
|
|
112
|
+
"""Test parsing control ID with parentheses converts to dots"""
|
|
113
|
+
matcher = ControlMatcher()
|
|
114
|
+
test_cases = [
|
|
115
|
+
("AC-1(1)", "AC-1.1"),
|
|
116
|
+
("ac-2(3)", "AC-2.3"),
|
|
117
|
+
("SI-4(10)", "SI-4.10"),
|
|
118
|
+
("CM-6(1)", "CM-6.1"),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for input_id, expected in test_cases:
|
|
122
|
+
result = matcher.parse_control_id(input_id)
|
|
123
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
124
|
+
|
|
125
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
126
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
127
|
+
def test_parse_control_id_with_dots(self, mock_app_class, mock_api_class):
|
|
128
|
+
"""Test parsing control ID already with dot notation"""
|
|
129
|
+
matcher = ControlMatcher()
|
|
130
|
+
test_cases = [
|
|
131
|
+
("AC-1.1", "AC-1.1"),
|
|
132
|
+
("ac-2.5", "AC-2.5"),
|
|
133
|
+
("SI-4.12", "SI-4.12"),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
for input_id, expected in test_cases:
|
|
137
|
+
result = matcher.parse_control_id(input_id)
|
|
138
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
139
|
+
|
|
140
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
141
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
142
|
+
def test_parse_control_id_with_text(self, mock_app_class, mock_api_class):
|
|
143
|
+
"""Test parsing control ID with descriptive text"""
|
|
144
|
+
matcher = ControlMatcher()
|
|
145
|
+
test_cases = [
|
|
146
|
+
("Access Control AC-1", "AC-1"),
|
|
147
|
+
("AC-1 Access Control Policy", "AC-1"),
|
|
148
|
+
("NIST Control AC-2 Account Management", "AC-2"),
|
|
149
|
+
("System Monitoring SI-4(5)", "SI-4.5"),
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
for input_id, expected in test_cases:
|
|
153
|
+
result = matcher.parse_control_id(input_id)
|
|
154
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
155
|
+
|
|
156
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
157
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
158
|
+
def test_parse_control_id_multiple_controls_returns_first(self, mock_app_class, mock_api_class):
|
|
159
|
+
"""Test parsing string with multiple controls returns first one"""
|
|
160
|
+
matcher = ControlMatcher()
|
|
161
|
+
test_cases = [
|
|
162
|
+
("AC-1, AC-2", "AC-1"),
|
|
163
|
+
("SI-2, SI-4, CM-6", "SI-2"),
|
|
164
|
+
("AC-1(1), AC-1(2)", "AC-1.1"),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
for input_id, expected in test_cases:
|
|
168
|
+
result = matcher.parse_control_id(input_id)
|
|
169
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
170
|
+
|
|
171
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
172
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
173
|
+
def test_parse_control_id_three_letter_family(self, mock_app_class, mock_api_class):
|
|
174
|
+
"""Test parsing control IDs with three-letter family codes"""
|
|
175
|
+
matcher = ControlMatcher()
|
|
176
|
+
test_cases = [
|
|
177
|
+
("PTA-1", "PTA-1"),
|
|
178
|
+
("SAR-10", "SAR-10"),
|
|
179
|
+
("PRM-3(2)", "PRM-3.2"),
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
for input_id, expected in test_cases:
|
|
183
|
+
result = matcher.parse_control_id(input_id)
|
|
184
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
185
|
+
|
|
186
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
187
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
188
|
+
def test_parse_control_id_no_match(self, mock_app_class, mock_api_class):
|
|
189
|
+
"""Test parsing invalid control ID format returns None"""
|
|
190
|
+
matcher = ControlMatcher()
|
|
191
|
+
test_cases = [
|
|
192
|
+
"No control here",
|
|
193
|
+
"12345",
|
|
194
|
+
"A-1", # Too short family code (single letter)
|
|
195
|
+
"AC", # Missing number
|
|
196
|
+
"Control without ID",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
for input_id in test_cases:
|
|
200
|
+
result = matcher.parse_control_id(input_id)
|
|
201
|
+
assert result is None, f"Expected None for input {input_id}, got {result}"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestControlMatcherParseControlIdWithSpaces:
|
|
205
|
+
"""Test cases for parse_control_id method with spaces in control IDs"""
|
|
206
|
+
|
|
207
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
208
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
209
|
+
def test_parse_control_id_with_space_before_parenthesis(self, mock_app_class, mock_api_class):
|
|
210
|
+
"""Test parsing control ID with space before parenthesis"""
|
|
211
|
+
matcher = ControlMatcher()
|
|
212
|
+
test_cases = [
|
|
213
|
+
("AC-1 (1)", "AC-1.1"),
|
|
214
|
+
("AC-2 (3)", "AC-2.3"),
|
|
215
|
+
("SI-4 (10)", "SI-4.10"),
|
|
216
|
+
("CM-6 (1)", "CM-6.1"),
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
for input_id, expected in test_cases:
|
|
220
|
+
result = matcher.parse_control_id(input_id)
|
|
221
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
222
|
+
|
|
223
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
224
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
225
|
+
def test_parse_control_id_with_spaces_inside_parentheses(self, mock_app_class, mock_api_class):
|
|
226
|
+
"""Test parsing control ID with spaces inside parentheses"""
|
|
227
|
+
matcher = ControlMatcher()
|
|
228
|
+
test_cases = [
|
|
229
|
+
("AC-1( 1 )", "AC-1.1"),
|
|
230
|
+
("AC-2( 3 )", "AC-2.3"),
|
|
231
|
+
("SI-4( 10)", "SI-4.10"),
|
|
232
|
+
("CM-6(1 )", "CM-6.1"),
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
for input_id, expected in test_cases:
|
|
236
|
+
result = matcher.parse_control_id(input_id)
|
|
237
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
238
|
+
|
|
239
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
240
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
241
|
+
def test_parse_control_id_with_leading_zeros_and_spaces(self, mock_app_class, mock_api_class):
|
|
242
|
+
"""Test parsing control ID with both leading zeros and spaces"""
|
|
243
|
+
matcher = ControlMatcher()
|
|
244
|
+
test_cases = [
|
|
245
|
+
("AC-01 (01)", "AC-1.1"),
|
|
246
|
+
("AC-02 (04)", "AC-2.4"),
|
|
247
|
+
("AC-17 (02)", "AC-17.2"),
|
|
248
|
+
("SI-04 (05)", "SI-4.5"),
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
for input_id, expected in test_cases:
|
|
252
|
+
result = matcher.parse_control_id(input_id)
|
|
253
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
254
|
+
|
|
255
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
256
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
257
|
+
def test_parse_control_id_with_various_space_combinations(self, mock_app_class, mock_api_class):
|
|
258
|
+
"""Test parsing control ID with various space combinations"""
|
|
259
|
+
matcher = ControlMatcher()
|
|
260
|
+
test_cases = [
|
|
261
|
+
("AC-1 (1)", "AC-1.1"), # Multiple spaces before
|
|
262
|
+
("AC-1 ( 1 )", "AC-1.1"), # Spaces everywhere
|
|
263
|
+
("AC-1 ( 1 )", "AC-1.1"), # Multiple spaces everywhere
|
|
264
|
+
("AC-01 ( 04 )", "AC-1.4"), # Leading zeros and multiple spaces
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
for input_id, expected in test_cases:
|
|
268
|
+
result = matcher.parse_control_id(input_id)
|
|
269
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
270
|
+
|
|
271
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
272
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
273
|
+
def test_parse_control_id_with_spaces_in_text(self, mock_app_class, mock_api_class):
|
|
274
|
+
"""Test parsing control ID with spaces in descriptive text"""
|
|
275
|
+
matcher = ControlMatcher()
|
|
276
|
+
test_cases = [
|
|
277
|
+
("Access Control AC-1 (1)", "AC-1.1"),
|
|
278
|
+
("AC-2 (3) Account Management", "AC-2.3"),
|
|
279
|
+
("NIST Control AC-17 (02)", "AC-17.2"),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
for input_id, expected in test_cases:
|
|
283
|
+
result = matcher.parse_control_id(input_id)
|
|
284
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestControlMatcherFindControlInCatalog:
|
|
288
|
+
"""Test cases for find_control_in_catalog method"""
|
|
289
|
+
|
|
290
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
291
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
292
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
293
|
+
def test_find_control_exact_match(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
294
|
+
"""Test finding control with exact match"""
|
|
295
|
+
matcher = ControlMatcher()
|
|
296
|
+
|
|
297
|
+
# Create mock controls
|
|
298
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
299
|
+
mock_control1.controlId = "AC-1"
|
|
300
|
+
mock_control1.id = 100
|
|
301
|
+
|
|
302
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
303
|
+
mock_control2.controlId = "AC-2"
|
|
304
|
+
mock_control2.id = 101
|
|
305
|
+
|
|
306
|
+
mock_get_controls.return_value = [mock_control1, mock_control2]
|
|
307
|
+
|
|
308
|
+
result = matcher.find_control_in_catalog("AC-1", 1)
|
|
309
|
+
|
|
310
|
+
assert result == mock_control1
|
|
311
|
+
mock_get_controls.assert_called_once_with(1)
|
|
312
|
+
|
|
313
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
314
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
315
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
316
|
+
def test_find_control_normalized_match(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
317
|
+
"""Test finding control with normalized match when exact match fails"""
|
|
318
|
+
matcher = ControlMatcher()
|
|
319
|
+
|
|
320
|
+
# Create mock controls with parentheses notation
|
|
321
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
322
|
+
mock_control1.controlId = "AC-1(1)"
|
|
323
|
+
mock_control1.id = 100
|
|
324
|
+
|
|
325
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
326
|
+
mock_control2.controlId = "AC-2"
|
|
327
|
+
mock_control2.id = 101
|
|
328
|
+
|
|
329
|
+
mock_get_controls.return_value = [mock_control1, mock_control2]
|
|
330
|
+
|
|
331
|
+
# Search using dot notation
|
|
332
|
+
result = matcher.find_control_in_catalog("AC-1.1", 1)
|
|
333
|
+
|
|
334
|
+
assert result == mock_control1
|
|
335
|
+
mock_get_controls.assert_called_once_with(1)
|
|
336
|
+
|
|
337
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
338
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
339
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
340
|
+
def test_find_control_not_found(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
341
|
+
"""Test finding control that doesn't exist returns None"""
|
|
342
|
+
matcher = ControlMatcher()
|
|
343
|
+
|
|
344
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
345
|
+
mock_control1.controlId = "AC-1"
|
|
346
|
+
mock_control1.id = 100
|
|
347
|
+
|
|
348
|
+
mock_get_controls.return_value = [mock_control1]
|
|
349
|
+
|
|
350
|
+
result = matcher.find_control_in_catalog("SI-4", 1)
|
|
351
|
+
|
|
352
|
+
assert result is None
|
|
353
|
+
mock_get_controls.assert_called_once_with(1)
|
|
354
|
+
|
|
355
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
356
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
357
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
358
|
+
def test_find_control_empty_catalog(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
359
|
+
"""Test finding control in empty catalog returns None"""
|
|
360
|
+
matcher = ControlMatcher()
|
|
361
|
+
mock_get_controls.return_value = []
|
|
362
|
+
|
|
363
|
+
result = matcher.find_control_in_catalog("AC-1", 1)
|
|
364
|
+
|
|
365
|
+
assert result is None
|
|
366
|
+
mock_get_controls.assert_called_once_with(1)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class TestControlMatcherFindControlImplementation:
|
|
370
|
+
"""Test cases for find_control_implementation method"""
|
|
371
|
+
|
|
372
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
373
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
374
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
375
|
+
def test_find_implementation_by_label(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
376
|
+
"""Test finding implementation by control label"""
|
|
377
|
+
matcher = ControlMatcher()
|
|
378
|
+
|
|
379
|
+
# Create mock implementations
|
|
380
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
381
|
+
mock_impl1.id = 200
|
|
382
|
+
mock_impl1.controlID = 100
|
|
383
|
+
|
|
384
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
385
|
+
mock_impl2.id = 201
|
|
386
|
+
mock_impl2.controlID = 101
|
|
387
|
+
|
|
388
|
+
mock_get_impls.return_value = {
|
|
389
|
+
"AC-1": mock_impl1,
|
|
390
|
+
"AC-2": mock_impl2,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
result = matcher.find_control_implementation("AC-1", 50, "securityplans")
|
|
394
|
+
|
|
395
|
+
assert result == mock_impl1
|
|
396
|
+
mock_get_impls.assert_called_once_with(50, "securityplans")
|
|
397
|
+
|
|
398
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
399
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
400
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
401
|
+
def test_find_implementation_case_insensitive(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
402
|
+
"""Test finding implementation with case-insensitive matching"""
|
|
403
|
+
matcher = ControlMatcher()
|
|
404
|
+
|
|
405
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
406
|
+
mock_impl1.id = 200
|
|
407
|
+
mock_impl1.controlID = 100
|
|
408
|
+
|
|
409
|
+
mock_get_impls.return_value = {
|
|
410
|
+
"ac-1": mock_impl1, # lowercase in dict
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
result = matcher.find_control_implementation("AC-1", 50)
|
|
414
|
+
|
|
415
|
+
assert result == mock_impl1
|
|
416
|
+
|
|
417
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_in_catalog")
|
|
418
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
419
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
420
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
421
|
+
def test_find_implementation_via_catalog(self, mock_app_class, mock_api_class, mock_get_impls, mock_find_control):
|
|
422
|
+
"""Test finding implementation via catalog when label match fails"""
|
|
423
|
+
matcher = ControlMatcher()
|
|
424
|
+
|
|
425
|
+
# No label match
|
|
426
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
427
|
+
mock_impl1.id = 200
|
|
428
|
+
mock_impl1.controlID = 100
|
|
429
|
+
|
|
430
|
+
mock_get_impls.return_value = {
|
|
431
|
+
"SI-2": mock_impl1, # Different control
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# But catalog returns a control
|
|
435
|
+
mock_control = MagicMock(spec=SecurityControl)
|
|
436
|
+
mock_control.id = 100
|
|
437
|
+
mock_control.controlId = "AC-1"
|
|
438
|
+
mock_find_control.return_value = mock_control
|
|
439
|
+
|
|
440
|
+
result = matcher.find_control_implementation("AC-1", 50, "securityplans", catalog_id=1)
|
|
441
|
+
|
|
442
|
+
assert result == mock_impl1
|
|
443
|
+
mock_find_control.assert_called_once_with("AC-1", 1)
|
|
444
|
+
|
|
445
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
446
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
447
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
448
|
+
def test_find_implementation_invalid_control_id(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
449
|
+
"""Test finding implementation with invalid control ID returns None"""
|
|
450
|
+
matcher = ControlMatcher()
|
|
451
|
+
mock_get_impls.return_value = {}
|
|
452
|
+
|
|
453
|
+
with patch("regscale.integrations.control_matcher.logger") as mock_logger:
|
|
454
|
+
result = matcher.find_control_implementation("Invalid", 50)
|
|
455
|
+
|
|
456
|
+
assert result is None
|
|
457
|
+
mock_logger.warning.assert_called_once()
|
|
458
|
+
|
|
459
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
460
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
461
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
462
|
+
def test_find_implementation_not_found(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
463
|
+
"""Test finding implementation that doesn't exist returns None"""
|
|
464
|
+
matcher = ControlMatcher()
|
|
465
|
+
mock_get_impls.return_value = {
|
|
466
|
+
"SI-2": MagicMock(spec=ControlImplementation),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
result = matcher.find_control_implementation("AC-1", 50)
|
|
470
|
+
|
|
471
|
+
assert result is None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class TestControlMatcherMatchControlsToImplementations:
|
|
475
|
+
"""Test cases for match_controls_to_implementations method"""
|
|
476
|
+
|
|
477
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
478
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
479
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
480
|
+
def test_match_multiple_controls(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
481
|
+
"""Test matching multiple control IDs to implementations"""
|
|
482
|
+
matcher = ControlMatcher()
|
|
483
|
+
|
|
484
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
485
|
+
mock_impl1.id = 200
|
|
486
|
+
|
|
487
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
488
|
+
mock_impl2.id = 201
|
|
489
|
+
|
|
490
|
+
def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
|
|
491
|
+
if control_id == "AC-1":
|
|
492
|
+
return mock_impl1
|
|
493
|
+
elif control_id == "AC-2":
|
|
494
|
+
return mock_impl2
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
mock_find_impl.side_effect = find_impl_side_effect
|
|
498
|
+
|
|
499
|
+
control_ids = ["AC-1", "AC-2", "SI-4"]
|
|
500
|
+
result = matcher.match_controls_to_implementations(control_ids, 50)
|
|
501
|
+
|
|
502
|
+
assert len(result) == 3
|
|
503
|
+
assert result["AC-1"] == mock_impl1
|
|
504
|
+
assert result["AC-2"] == mock_impl2
|
|
505
|
+
assert result["SI-4"] is None
|
|
506
|
+
assert mock_find_impl.call_count == 3
|
|
507
|
+
|
|
508
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
509
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
510
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
511
|
+
def test_match_empty_list(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
512
|
+
"""Test matching empty list returns empty dict"""
|
|
513
|
+
matcher = ControlMatcher()
|
|
514
|
+
result = matcher.match_controls_to_implementations([], 50)
|
|
515
|
+
|
|
516
|
+
assert result == {}
|
|
517
|
+
mock_find_impl.assert_not_called()
|
|
518
|
+
|
|
519
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
520
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
521
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
522
|
+
def test_match_with_catalog_id(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
523
|
+
"""Test matching controls with catalog ID provided"""
|
|
524
|
+
matcher = ControlMatcher()
|
|
525
|
+
mock_impl = MagicMock(spec=ControlImplementation)
|
|
526
|
+
mock_find_impl.return_value = mock_impl
|
|
527
|
+
|
|
528
|
+
control_ids = ["AC-1"]
|
|
529
|
+
result = matcher.match_controls_to_implementations(control_ids, 50, "securityplans", catalog_id=1)
|
|
530
|
+
|
|
531
|
+
assert result["AC-1"] == mock_impl
|
|
532
|
+
mock_find_impl.assert_called_once_with("AC-1", 50, "securityplans", 1)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class TestControlMatcherGetSecurityPlanControls:
|
|
536
|
+
"""Test cases for get_security_plan_controls method"""
|
|
537
|
+
|
|
538
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
539
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
540
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
541
|
+
def test_get_security_plan_controls(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
542
|
+
"""Test getting all control implementations for a security plan"""
|
|
543
|
+
matcher = ControlMatcher()
|
|
544
|
+
|
|
545
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
546
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
547
|
+
|
|
548
|
+
expected_dict = {
|
|
549
|
+
"AC-1": mock_impl1,
|
|
550
|
+
"AC-2": mock_impl2,
|
|
551
|
+
}
|
|
552
|
+
mock_get_impls.return_value = expected_dict
|
|
553
|
+
|
|
554
|
+
result = matcher.get_security_plan_controls(50)
|
|
555
|
+
|
|
556
|
+
assert result == expected_dict
|
|
557
|
+
mock_get_impls.assert_called_once_with(50, "securityplans")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class TestControlMatcherFindControlsByPattern:
|
|
561
|
+
"""Test cases for find_controls_by_pattern method"""
|
|
562
|
+
|
|
563
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
564
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
565
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
566
|
+
def test_find_by_control_id_pattern(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
567
|
+
"""Test finding controls by control ID pattern"""
|
|
568
|
+
matcher = ControlMatcher()
|
|
569
|
+
|
|
570
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
571
|
+
mock_control1.controlId = "AC-1"
|
|
572
|
+
mock_control1.title = "Access Control Policy"
|
|
573
|
+
|
|
574
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
575
|
+
mock_control2.controlId = "AC-2"
|
|
576
|
+
mock_control2.title = "Account Management"
|
|
577
|
+
|
|
578
|
+
mock_control3 = MagicMock(spec=SecurityControl)
|
|
579
|
+
mock_control3.controlId = "SI-2"
|
|
580
|
+
mock_control3.title = "Flaw Remediation"
|
|
581
|
+
|
|
582
|
+
mock_get_controls.return_value = [mock_control1, mock_control2, mock_control3]
|
|
583
|
+
|
|
584
|
+
result = matcher.find_controls_by_pattern("^AC-", 1)
|
|
585
|
+
|
|
586
|
+
assert len(result) == 2
|
|
587
|
+
assert mock_control1 in result
|
|
588
|
+
assert mock_control2 in result
|
|
589
|
+
assert mock_control3 not in result
|
|
590
|
+
|
|
591
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
592
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
593
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
594
|
+
def test_find_by_title_pattern(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
595
|
+
"""Test finding controls by title pattern"""
|
|
596
|
+
matcher = ControlMatcher()
|
|
597
|
+
|
|
598
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
599
|
+
mock_control1.controlId = "AC-1"
|
|
600
|
+
mock_control1.title = "Access Control Policy"
|
|
601
|
+
|
|
602
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
603
|
+
mock_control2.controlId = "AC-2"
|
|
604
|
+
mock_control2.title = "Account Management"
|
|
605
|
+
|
|
606
|
+
mock_control3 = MagicMock(spec=SecurityControl)
|
|
607
|
+
mock_control3.controlId = "SI-2"
|
|
608
|
+
mock_control3.title = "Access Review"
|
|
609
|
+
|
|
610
|
+
mock_get_controls.return_value = [mock_control1, mock_control2, mock_control3]
|
|
611
|
+
|
|
612
|
+
result = matcher.find_controls_by_pattern("Access", 1)
|
|
613
|
+
|
|
614
|
+
assert len(result) == 2
|
|
615
|
+
assert mock_control1 in result
|
|
616
|
+
assert mock_control3 in result
|
|
617
|
+
assert mock_control2 not in result
|
|
618
|
+
|
|
619
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
620
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
621
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
622
|
+
def test_find_by_pattern_case_insensitive(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
623
|
+
"""Test finding controls with case-insensitive pattern"""
|
|
624
|
+
matcher = ControlMatcher()
|
|
625
|
+
|
|
626
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
627
|
+
mock_control1.controlId = "ac-1"
|
|
628
|
+
mock_control1.title = "access control"
|
|
629
|
+
|
|
630
|
+
mock_get_controls.return_value = [mock_control1]
|
|
631
|
+
|
|
632
|
+
result = matcher.find_controls_by_pattern("ACCESS", 1)
|
|
633
|
+
|
|
634
|
+
assert len(result) == 1
|
|
635
|
+
assert mock_control1 in result
|
|
636
|
+
|
|
637
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
638
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
639
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
640
|
+
def test_find_by_pattern_no_matches(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
641
|
+
"""Test finding controls with pattern that has no matches"""
|
|
642
|
+
matcher = ControlMatcher()
|
|
643
|
+
|
|
644
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
645
|
+
mock_control1.controlId = "AC-1"
|
|
646
|
+
mock_control1.title = "Access Control"
|
|
647
|
+
|
|
648
|
+
mock_get_controls.return_value = [mock_control1]
|
|
649
|
+
|
|
650
|
+
result = matcher.find_controls_by_pattern("NOMATCH", 1)
|
|
651
|
+
|
|
652
|
+
assert len(result) == 0
|
|
653
|
+
|
|
654
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
655
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
656
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
657
|
+
def test_find_by_pattern_none_title(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
658
|
+
"""Test finding controls when title is None"""
|
|
659
|
+
matcher = ControlMatcher()
|
|
660
|
+
|
|
661
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
662
|
+
mock_control1.controlId = "AC-1"
|
|
663
|
+
mock_control1.title = None
|
|
664
|
+
|
|
665
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
666
|
+
mock_control2.controlId = "AC-2"
|
|
667
|
+
mock_control2.title = "Account Management"
|
|
668
|
+
|
|
669
|
+
mock_get_controls.return_value = [mock_control1, mock_control2]
|
|
670
|
+
|
|
671
|
+
result = matcher.find_controls_by_pattern("AC-1", 1)
|
|
672
|
+
|
|
673
|
+
assert len(result) == 1
|
|
674
|
+
assert mock_control1 in result
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class TestControlMatcherBulkMatchControls:
|
|
678
|
+
"""Test cases for bulk_match_controls method"""
|
|
679
|
+
|
|
680
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
681
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
682
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
683
|
+
def test_bulk_match_controls(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
684
|
+
"""Test bulk matching external IDs to control implementations"""
|
|
685
|
+
matcher = ControlMatcher()
|
|
686
|
+
|
|
687
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
688
|
+
mock_impl1.id = 200
|
|
689
|
+
|
|
690
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
691
|
+
mock_impl2.id = 201
|
|
692
|
+
|
|
693
|
+
def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
|
|
694
|
+
if control_id == "AC-1":
|
|
695
|
+
return mock_impl1
|
|
696
|
+
elif control_id == "AC-2":
|
|
697
|
+
return mock_impl2
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
mock_find_impl.side_effect = find_impl_side_effect
|
|
701
|
+
|
|
702
|
+
control_mappings = {
|
|
703
|
+
"ext-001": "AC-1",
|
|
704
|
+
"ext-002": "AC-2",
|
|
705
|
+
"ext-003": "SI-4",
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
result = matcher.bulk_match_controls(control_mappings, 50)
|
|
709
|
+
|
|
710
|
+
assert len(result) == 3
|
|
711
|
+
assert result["ext-001"] == mock_impl1
|
|
712
|
+
assert result["ext-002"] == mock_impl2
|
|
713
|
+
assert result["ext-003"] is None
|
|
714
|
+
assert mock_find_impl.call_count == 3
|
|
715
|
+
|
|
716
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
717
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
718
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
719
|
+
def test_bulk_match_empty_dict(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
720
|
+
"""Test bulk matching with empty dict returns empty dict"""
|
|
721
|
+
matcher = ControlMatcher()
|
|
722
|
+
result = matcher.bulk_match_controls({}, 50)
|
|
723
|
+
|
|
724
|
+
assert result == {}
|
|
725
|
+
mock_find_impl.assert_not_called()
|
|
726
|
+
|
|
727
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
728
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
729
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
730
|
+
def test_bulk_match_with_catalog(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
731
|
+
"""Test bulk matching with catalog ID"""
|
|
732
|
+
matcher = ControlMatcher()
|
|
733
|
+
mock_impl = MagicMock(spec=ControlImplementation)
|
|
734
|
+
mock_find_impl.return_value = mock_impl
|
|
735
|
+
|
|
736
|
+
control_mappings = {"ext-001": "AC-1"}
|
|
737
|
+
result = matcher.bulk_match_controls(control_mappings, 50, "securityplans", catalog_id=1)
|
|
738
|
+
|
|
739
|
+
assert result["ext-001"] == mock_impl
|
|
740
|
+
mock_find_impl.assert_called_once_with("AC-1", 50, "securityplans", 1)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class TestControlMatcherGetCatalogControls:
|
|
744
|
+
"""Test cases for _get_catalog_controls method"""
|
|
745
|
+
|
|
746
|
+
@patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
|
|
747
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
748
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
749
|
+
def test_get_catalog_controls_first_call(self, mock_app_class, mock_api_class, mock_get_list):
|
|
750
|
+
"""Test getting catalog controls on first call (not cached)"""
|
|
751
|
+
matcher = ControlMatcher()
|
|
752
|
+
|
|
753
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
754
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
755
|
+
mock_get_list.return_value = [mock_control1, mock_control2]
|
|
756
|
+
|
|
757
|
+
result = matcher._get_catalog_controls(1)
|
|
758
|
+
|
|
759
|
+
assert len(result) == 2
|
|
760
|
+
assert mock_control1 in result
|
|
761
|
+
assert mock_control2 in result
|
|
762
|
+
mock_get_list.assert_called_once_with(1)
|
|
763
|
+
assert 1 in matcher._catalog_cache
|
|
764
|
+
|
|
765
|
+
@patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
|
|
766
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
767
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
768
|
+
def test_get_catalog_controls_cached(self, mock_app_class, mock_api_class, mock_get_list):
|
|
769
|
+
"""Test getting catalog controls from cache on subsequent calls"""
|
|
770
|
+
matcher = ControlMatcher()
|
|
771
|
+
|
|
772
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
773
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
774
|
+
cached_controls = [mock_control1, mock_control2]
|
|
775
|
+
|
|
776
|
+
# Pre-populate cache
|
|
777
|
+
matcher._catalog_cache[1] = cached_controls
|
|
778
|
+
|
|
779
|
+
result = matcher._get_catalog_controls(1)
|
|
780
|
+
|
|
781
|
+
assert result == cached_controls
|
|
782
|
+
mock_get_list.assert_not_called()
|
|
783
|
+
|
|
784
|
+
@patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
|
|
785
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
786
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
787
|
+
def test_get_catalog_controls_error(self, mock_app_class, mock_api_class, mock_get_list):
|
|
788
|
+
"""Test getting catalog controls handles exception"""
|
|
789
|
+
matcher = ControlMatcher()
|
|
790
|
+
mock_get_list.side_effect = Exception("API Error")
|
|
791
|
+
|
|
792
|
+
with patch("regscale.integrations.control_matcher.logger") as mock_logger:
|
|
793
|
+
result = matcher._get_catalog_controls(1)
|
|
794
|
+
|
|
795
|
+
assert result == []
|
|
796
|
+
mock_logger.error.assert_called_once()
|
|
797
|
+
assert 1 not in matcher._catalog_cache
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
class TestControlMatcherGetControlImplementations:
|
|
801
|
+
"""Test cases for _get_control_implementations method"""
|
|
802
|
+
|
|
803
|
+
@patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
|
|
804
|
+
@patch(
|
|
805
|
+
"regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
|
|
806
|
+
)
|
|
807
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
808
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
809
|
+
def test_get_control_implementations_first_call(
|
|
810
|
+
self, mock_app_class, mock_api_class, mock_get_label_map, mock_get_object
|
|
811
|
+
):
|
|
812
|
+
"""Test getting control implementations on first call (not cached)"""
|
|
813
|
+
matcher = ControlMatcher()
|
|
814
|
+
|
|
815
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
816
|
+
mock_impl1.id = 200
|
|
817
|
+
|
|
818
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
819
|
+
mock_impl2.id = 201
|
|
820
|
+
|
|
821
|
+
mock_get_label_map.return_value = {
|
|
822
|
+
"AC-1": 200,
|
|
823
|
+
"AC-2": 201,
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
def get_object_side_effect(impl_id):
|
|
827
|
+
if impl_id == 200:
|
|
828
|
+
return mock_impl1
|
|
829
|
+
elif impl_id == 201:
|
|
830
|
+
return mock_impl2
|
|
831
|
+
return None
|
|
832
|
+
|
|
833
|
+
mock_get_object.side_effect = get_object_side_effect
|
|
834
|
+
|
|
835
|
+
result = matcher._get_control_implementations(50, "securityplans")
|
|
836
|
+
|
|
837
|
+
assert len(result) == 2
|
|
838
|
+
assert result["AC-1"] == mock_impl1
|
|
839
|
+
assert result["AC-2"] == mock_impl2
|
|
840
|
+
mock_get_label_map.assert_called_once_with(50, "securityplans")
|
|
841
|
+
assert (50, "securityplans") in matcher._control_impl_cache
|
|
842
|
+
|
|
843
|
+
@patch(
|
|
844
|
+
"regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
|
|
845
|
+
)
|
|
846
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
847
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
848
|
+
def test_get_control_implementations_cached(self, mock_app_class, mock_api_class, mock_get_label_map):
|
|
849
|
+
"""Test getting control implementations from cache"""
|
|
850
|
+
matcher = ControlMatcher()
|
|
851
|
+
|
|
852
|
+
mock_impl = MagicMock(spec=ControlImplementation)
|
|
853
|
+
cached_impls = {"AC-1": mock_impl}
|
|
854
|
+
|
|
855
|
+
# Pre-populate cache
|
|
856
|
+
matcher._control_impl_cache[(50, "securityplans")] = cached_impls
|
|
857
|
+
|
|
858
|
+
result = matcher._get_control_implementations(50, "securityplans")
|
|
859
|
+
|
|
860
|
+
assert result == cached_impls
|
|
861
|
+
mock_get_label_map.assert_not_called()
|
|
862
|
+
|
|
863
|
+
@patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
|
|
864
|
+
@patch(
|
|
865
|
+
"regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
|
|
866
|
+
)
|
|
867
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
868
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
869
|
+
def test_get_control_implementations_with_none(
|
|
870
|
+
self, mock_app_class, mock_api_class, mock_get_label_map, mock_get_object
|
|
871
|
+
):
|
|
872
|
+
"""Test getting control implementations when some objects return None"""
|
|
873
|
+
matcher = ControlMatcher()
|
|
874
|
+
|
|
875
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
876
|
+
mock_impl1.id = 200
|
|
877
|
+
|
|
878
|
+
mock_get_label_map.return_value = {
|
|
879
|
+
"AC-1": 200,
|
|
880
|
+
"AC-2": 201, # This will return None
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
def get_object_side_effect(impl_id):
|
|
884
|
+
if impl_id == 200:
|
|
885
|
+
return mock_impl1
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
mock_get_object.side_effect = get_object_side_effect
|
|
889
|
+
|
|
890
|
+
result = matcher._get_control_implementations(50, "securityplans")
|
|
891
|
+
|
|
892
|
+
# Should only include the valid implementation
|
|
893
|
+
assert len(result) == 1
|
|
894
|
+
assert result["AC-1"] == mock_impl1
|
|
895
|
+
assert "AC-2" not in result
|
|
896
|
+
|
|
897
|
+
@patch(
|
|
898
|
+
"regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
|
|
899
|
+
)
|
|
900
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
901
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
902
|
+
def test_get_control_implementations_error(self, mock_app_class, mock_api_class, mock_get_label_map):
|
|
903
|
+
"""Test getting control implementations handles exception"""
|
|
904
|
+
matcher = ControlMatcher()
|
|
905
|
+
mock_get_label_map.side_effect = Exception("API Error")
|
|
906
|
+
|
|
907
|
+
with patch("regscale.integrations.control_matcher.logger") as mock_logger:
|
|
908
|
+
result = matcher._get_control_implementations(50, "securityplans")
|
|
909
|
+
|
|
910
|
+
assert result == {}
|
|
911
|
+
mock_logger.error.assert_called_once()
|
|
912
|
+
assert (50, "securityplans") not in matcher._control_impl_cache
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
class TestControlMatcherClearCache:
|
|
916
|
+
"""Test cases for clear_cache method"""
|
|
917
|
+
|
|
918
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
919
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
920
|
+
def test_clear_cache_empty(self, mock_app_class, mock_api_class):
|
|
921
|
+
"""Test clearing cache when already empty"""
|
|
922
|
+
matcher = ControlMatcher()
|
|
923
|
+
|
|
924
|
+
with patch("regscale.integrations.control_matcher.logger") as mock_logger:
|
|
925
|
+
matcher.clear_cache()
|
|
926
|
+
|
|
927
|
+
assert matcher._catalog_cache == {}
|
|
928
|
+
assert matcher._control_impl_cache == {}
|
|
929
|
+
mock_logger.info.assert_called_once_with("Cleared control matcher cache")
|
|
930
|
+
|
|
931
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
932
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
933
|
+
def test_clear_cache_with_data(self, mock_app_class, mock_api_class):
|
|
934
|
+
"""Test clearing cache with data"""
|
|
935
|
+
matcher = ControlMatcher()
|
|
936
|
+
|
|
937
|
+
# Add data to caches
|
|
938
|
+
matcher._catalog_cache[1] = [MagicMock(spec=SecurityControl)]
|
|
939
|
+
matcher._control_impl_cache[(50, "securityplans")] = {"AC-1": MagicMock(spec=ControlImplementation)}
|
|
940
|
+
|
|
941
|
+
with patch("regscale.integrations.control_matcher.logger") as mock_logger:
|
|
942
|
+
matcher.clear_cache()
|
|
943
|
+
|
|
944
|
+
assert matcher._catalog_cache == {}
|
|
945
|
+
assert matcher._control_impl_cache == {}
|
|
946
|
+
mock_logger.info.assert_called_once_with("Cleared control matcher cache")
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
class TestControlMatcherEdgeCases:
|
|
950
|
+
"""Test cases for edge cases and error scenarios"""
|
|
951
|
+
|
|
952
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
953
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
954
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
955
|
+
def test_find_control_with_special_characters(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
956
|
+
"""Test finding control with special characters in ID"""
|
|
957
|
+
matcher = ControlMatcher()
|
|
958
|
+
|
|
959
|
+
mock_control = MagicMock(spec=SecurityControl)
|
|
960
|
+
mock_control.controlId = "AC-1(1)"
|
|
961
|
+
mock_get_controls.return_value = [mock_control]
|
|
962
|
+
|
|
963
|
+
# Test with different variations
|
|
964
|
+
result1 = matcher.find_control_in_catalog("AC-1(1)", 1)
|
|
965
|
+
result2 = matcher.find_control_in_catalog("AC-1.1", 1)
|
|
966
|
+
|
|
967
|
+
assert result1 == mock_control
|
|
968
|
+
assert result2 == mock_control
|
|
969
|
+
|
|
970
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
971
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
972
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
973
|
+
def test_match_controls_with_duplicates(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
974
|
+
"""Test matching controls when list contains duplicates"""
|
|
975
|
+
matcher = ControlMatcher()
|
|
976
|
+
|
|
977
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
978
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
979
|
+
|
|
980
|
+
def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
|
|
981
|
+
if control_id == "AC-1":
|
|
982
|
+
return mock_impl1
|
|
983
|
+
elif control_id == "AC-2":
|
|
984
|
+
return mock_impl2
|
|
985
|
+
return None
|
|
986
|
+
|
|
987
|
+
mock_find_impl.side_effect = find_impl_side_effect
|
|
988
|
+
|
|
989
|
+
control_ids = ["AC-1", "AC-1", "AC-2"]
|
|
990
|
+
result = matcher.match_controls_to_implementations(control_ids, 50)
|
|
991
|
+
|
|
992
|
+
# Result is a dict, so duplicates are collapsed - should have 2 unique keys
|
|
993
|
+
assert len(result) == 2
|
|
994
|
+
assert result["AC-1"] == mock_impl1
|
|
995
|
+
assert result["AC-2"] == mock_impl2
|
|
996
|
+
# Should still call find_impl for each entry in the list, including duplicates
|
|
997
|
+
assert mock_find_impl.call_count == 3
|
|
998
|
+
|
|
999
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
1000
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1001
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1002
|
+
def test_find_by_pattern_with_empty_string(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
1003
|
+
"""Test finding controls with empty string pattern"""
|
|
1004
|
+
matcher = ControlMatcher()
|
|
1005
|
+
|
|
1006
|
+
mock_control = MagicMock(spec=SecurityControl)
|
|
1007
|
+
mock_control.controlId = "AC-1"
|
|
1008
|
+
mock_control.title = "Access Control"
|
|
1009
|
+
mock_get_controls.return_value = [mock_control]
|
|
1010
|
+
|
|
1011
|
+
result = matcher.find_controls_by_pattern("", 1)
|
|
1012
|
+
|
|
1013
|
+
# Empty pattern should match everything
|
|
1014
|
+
assert len(result) == 1
|
|
1015
|
+
assert mock_control in result
|
|
1016
|
+
|
|
1017
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
1018
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1019
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1020
|
+
def test_find_implementation_different_parent_modules(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
1021
|
+
"""Test finding implementations with different parent modules"""
|
|
1022
|
+
matcher = ControlMatcher()
|
|
1023
|
+
|
|
1024
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
1025
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
1026
|
+
|
|
1027
|
+
def get_impls_side_effect(parent_id, parent_module):
|
|
1028
|
+
if parent_module == "securityplans":
|
|
1029
|
+
return {"AC-1": mock_impl1}
|
|
1030
|
+
elif parent_module == "assessments":
|
|
1031
|
+
return {"AC-1": mock_impl2}
|
|
1032
|
+
return {}
|
|
1033
|
+
|
|
1034
|
+
mock_get_impls.side_effect = get_impls_side_effect
|
|
1035
|
+
|
|
1036
|
+
result1 = matcher.find_control_implementation("AC-1", 50, "securityplans")
|
|
1037
|
+
result2 = matcher.find_control_implementation("AC-1", 51, "assessments")
|
|
1038
|
+
|
|
1039
|
+
assert result1 == mock_impl1
|
|
1040
|
+
assert result2 == mock_impl2
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
class TestControlMatcherLeadingZeros:
|
|
1044
|
+
"""Test cases for control IDs with leading zeros"""
|
|
1045
|
+
|
|
1046
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1047
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1048
|
+
def test_normalize_control_id_with_leading_zeros(self, mock_app_class, mock_api_class):
|
|
1049
|
+
"""Test normalizing control IDs with leading zeros"""
|
|
1050
|
+
matcher = ControlMatcher()
|
|
1051
|
+
|
|
1052
|
+
test_cases = [
|
|
1053
|
+
("AC-01", "AC-1"),
|
|
1054
|
+
("AC-17", "AC-17"),
|
|
1055
|
+
("AC-01.02", "AC-1.2"),
|
|
1056
|
+
("AC-17.02", "AC-17.2"),
|
|
1057
|
+
("AC-1.1", "AC-1.1"),
|
|
1058
|
+
]
|
|
1059
|
+
|
|
1060
|
+
for input_id, expected in test_cases:
|
|
1061
|
+
result = matcher._normalize_control_id(input_id)
|
|
1062
|
+
assert result == expected, f"Failed for input {input_id}"
|
|
1063
|
+
|
|
1064
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1065
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1066
|
+
def test_get_control_id_variations_simple(self, mock_app_class, mock_api_class):
|
|
1067
|
+
"""Test generating variations for simple control IDs"""
|
|
1068
|
+
matcher = ControlMatcher()
|
|
1069
|
+
|
|
1070
|
+
result = matcher._get_control_id_variations("AC-1")
|
|
1071
|
+
expected = {"AC-1", "AC-01"}
|
|
1072
|
+
assert result == expected
|
|
1073
|
+
|
|
1074
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1075
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1076
|
+
def test_get_control_id_variations_with_enhancement(self, mock_app_class, mock_api_class):
|
|
1077
|
+
"""Test generating variations for control IDs with enhancements"""
|
|
1078
|
+
matcher = ControlMatcher()
|
|
1079
|
+
|
|
1080
|
+
result = matcher._get_control_id_variations("AC-17.2")
|
|
1081
|
+
expected = {
|
|
1082
|
+
"AC-17.2",
|
|
1083
|
+
"AC-17.02",
|
|
1084
|
+
"AC-17(2)",
|
|
1085
|
+
"AC-17(02)",
|
|
1086
|
+
}
|
|
1087
|
+
assert result == expected
|
|
1088
|
+
|
|
1089
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1090
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1091
|
+
def test_get_control_id_variations_with_leading_zeros_input(self, mock_app_class, mock_api_class):
|
|
1092
|
+
"""Test generating variations when input has leading zeros"""
|
|
1093
|
+
matcher = ControlMatcher()
|
|
1094
|
+
|
|
1095
|
+
result = matcher._get_control_id_variations("AC-01")
|
|
1096
|
+
expected = {"AC-1", "AC-01"}
|
|
1097
|
+
assert result == expected
|
|
1098
|
+
|
|
1099
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1100
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1101
|
+
def test_get_control_id_variations_with_parentheses_input(self, mock_app_class, mock_api_class):
|
|
1102
|
+
"""Test generating variations when input has parentheses"""
|
|
1103
|
+
matcher = ControlMatcher()
|
|
1104
|
+
|
|
1105
|
+
result = matcher._get_control_id_variations("AC-17(02)")
|
|
1106
|
+
expected = {
|
|
1107
|
+
"AC-17.2",
|
|
1108
|
+
"AC-17.02",
|
|
1109
|
+
"AC-17(2)",
|
|
1110
|
+
"AC-17(02)",
|
|
1111
|
+
}
|
|
1112
|
+
assert result == expected
|
|
1113
|
+
|
|
1114
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
1115
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1116
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1117
|
+
def test_find_control_in_catalog_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
1118
|
+
"""Test finding controls with leading zeros in catalog"""
|
|
1119
|
+
matcher = ControlMatcher()
|
|
1120
|
+
|
|
1121
|
+
# Catalog has control with leading zeros
|
|
1122
|
+
mock_control = MagicMock(spec=SecurityControl)
|
|
1123
|
+
mock_control.controlId = "AC-01"
|
|
1124
|
+
mock_control.id = 100
|
|
1125
|
+
|
|
1126
|
+
mock_get_controls.return_value = [mock_control]
|
|
1127
|
+
|
|
1128
|
+
# Search without leading zero should find it
|
|
1129
|
+
result = matcher.find_control_in_catalog("AC-1", 1)
|
|
1130
|
+
assert result == mock_control
|
|
1131
|
+
|
|
1132
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
1133
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1134
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1135
|
+
def test_find_control_in_catalog_search_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
1136
|
+
"""Test finding controls when search ID has leading zeros"""
|
|
1137
|
+
matcher = ControlMatcher()
|
|
1138
|
+
|
|
1139
|
+
# Catalog has control without leading zeros
|
|
1140
|
+
mock_control = MagicMock(spec=SecurityControl)
|
|
1141
|
+
mock_control.controlId = "AC-1"
|
|
1142
|
+
mock_control.id = 100
|
|
1143
|
+
|
|
1144
|
+
mock_get_controls.return_value = [mock_control]
|
|
1145
|
+
|
|
1146
|
+
# Search with leading zero should find it
|
|
1147
|
+
result = matcher.find_control_in_catalog("AC-01", 1)
|
|
1148
|
+
assert result == mock_control
|
|
1149
|
+
|
|
1150
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
|
|
1151
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1152
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1153
|
+
def test_find_control_with_leading_zeros_enhancement(self, mock_app_class, mock_api_class, mock_get_controls):
|
|
1154
|
+
"""Test finding controls with leading zeros in enhancement numbers"""
|
|
1155
|
+
matcher = ControlMatcher()
|
|
1156
|
+
|
|
1157
|
+
# Catalog has control with leading zeros in enhancement
|
|
1158
|
+
mock_control = MagicMock(spec=SecurityControl)
|
|
1159
|
+
mock_control.controlId = "AC-17(02)"
|
|
1160
|
+
mock_control.id = 100
|
|
1161
|
+
|
|
1162
|
+
mock_get_controls.return_value = [mock_control]
|
|
1163
|
+
|
|
1164
|
+
# Search with different formats should all find it
|
|
1165
|
+
result1 = matcher.find_control_in_catalog("AC-17.2", 1)
|
|
1166
|
+
result2 = matcher.find_control_in_catalog("AC-17(2)", 1)
|
|
1167
|
+
result3 = matcher.find_control_in_catalog("AC-17.02", 1)
|
|
1168
|
+
|
|
1169
|
+
assert result1 == mock_control
|
|
1170
|
+
assert result2 == mock_control
|
|
1171
|
+
assert result3 == mock_control
|
|
1172
|
+
|
|
1173
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
1174
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1175
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1176
|
+
def test_find_implementation_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
1177
|
+
"""Test finding implementation when control IDs have leading zeros"""
|
|
1178
|
+
matcher = ControlMatcher()
|
|
1179
|
+
|
|
1180
|
+
mock_impl = MagicMock(spec=ControlImplementation)
|
|
1181
|
+
mock_impl.id = 200
|
|
1182
|
+
mock_impl.controlID = 100
|
|
1183
|
+
|
|
1184
|
+
# Implementation key has leading zero
|
|
1185
|
+
mock_get_impls.return_value = {
|
|
1186
|
+
"AC-01": mock_impl,
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
# Search without leading zero should find it
|
|
1190
|
+
result = matcher.find_control_implementation("AC-1", 50)
|
|
1191
|
+
assert result == mock_impl
|
|
1192
|
+
|
|
1193
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
1194
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1195
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1196
|
+
def test_find_implementation_search_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
1197
|
+
"""Test finding implementation when search ID has leading zeros"""
|
|
1198
|
+
matcher = ControlMatcher()
|
|
1199
|
+
|
|
1200
|
+
mock_impl = MagicMock(spec=ControlImplementation)
|
|
1201
|
+
mock_impl.id = 200
|
|
1202
|
+
mock_impl.controlID = 100
|
|
1203
|
+
|
|
1204
|
+
# Implementation key has no leading zero
|
|
1205
|
+
mock_get_impls.return_value = {
|
|
1206
|
+
"AC-1": mock_impl,
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
# Search with leading zero should find it
|
|
1210
|
+
result = matcher.find_control_implementation("AC-01", 50)
|
|
1211
|
+
assert result == mock_impl
|
|
1212
|
+
|
|
1213
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
|
|
1214
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1215
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1216
|
+
def test_find_implementation_with_leading_zeros_complex(self, mock_app_class, mock_api_class, mock_get_impls):
|
|
1217
|
+
"""Test finding implementation with complex leading zero scenarios"""
|
|
1218
|
+
matcher = ControlMatcher()
|
|
1219
|
+
|
|
1220
|
+
mock_impl = MagicMock(spec=ControlImplementation)
|
|
1221
|
+
mock_impl.id = 200
|
|
1222
|
+
|
|
1223
|
+
# Implementation key has leading zeros in enhancement
|
|
1224
|
+
mock_get_impls.return_value = {
|
|
1225
|
+
"AC-17(02)": mock_impl,
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
# Search with different formats should find it
|
|
1229
|
+
result1 = matcher.find_control_implementation("AC-17.2", 50)
|
|
1230
|
+
result2 = matcher.find_control_implementation("AC-17(2)", 50)
|
|
1231
|
+
result3 = matcher.find_control_implementation("AC-17.02", 50)
|
|
1232
|
+
|
|
1233
|
+
assert result1 == mock_impl
|
|
1234
|
+
assert result2 == mock_impl
|
|
1235
|
+
assert result3 == mock_impl
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
class TestControlMatcherIntegrationScenarios:
|
|
1239
|
+
"""Integration test scenarios for complex workflows"""
|
|
1240
|
+
|
|
1241
|
+
@patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
|
|
1242
|
+
@patch(
|
|
1243
|
+
"regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
|
|
1244
|
+
)
|
|
1245
|
+
@patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
|
|
1246
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1247
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1248
|
+
def test_full_workflow_with_caching(
|
|
1249
|
+
self, mock_app_class, mock_api_class, mock_get_catalog, mock_get_label_map, mock_get_object
|
|
1250
|
+
):
|
|
1251
|
+
"""Test full workflow with multiple operations and caching"""
|
|
1252
|
+
matcher = ControlMatcher()
|
|
1253
|
+
|
|
1254
|
+
# Setup catalog controls
|
|
1255
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
1256
|
+
mock_control1.controlId = "AC-1"
|
|
1257
|
+
mock_control1.id = 100
|
|
1258
|
+
mock_control1.title = "Access Control Policy"
|
|
1259
|
+
|
|
1260
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
1261
|
+
mock_control2.controlId = "AC-2"
|
|
1262
|
+
mock_control2.id = 101
|
|
1263
|
+
mock_control2.title = "Account Management"
|
|
1264
|
+
|
|
1265
|
+
mock_get_catalog.return_value = [mock_control1, mock_control2]
|
|
1266
|
+
|
|
1267
|
+
# Setup implementations
|
|
1268
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
1269
|
+
mock_impl1.id = 200
|
|
1270
|
+
mock_impl1.controlID = 100
|
|
1271
|
+
|
|
1272
|
+
mock_get_label_map.return_value = {"AC-1": 200}
|
|
1273
|
+
mock_get_object.return_value = mock_impl1
|
|
1274
|
+
|
|
1275
|
+
# First operation: find control in catalog
|
|
1276
|
+
control = matcher.find_control_in_catalog("AC-1", 1)
|
|
1277
|
+
assert control == mock_control1
|
|
1278
|
+
assert mock_get_catalog.call_count == 1
|
|
1279
|
+
|
|
1280
|
+
# Second operation: find same control (should use cache)
|
|
1281
|
+
control2 = matcher.find_control_in_catalog("AC-2", 1)
|
|
1282
|
+
assert control2 == mock_control2
|
|
1283
|
+
assert mock_get_catalog.call_count == 1 # Should not increase
|
|
1284
|
+
|
|
1285
|
+
# Third operation: find implementation
|
|
1286
|
+
impl = matcher.find_control_implementation("AC-1", 50)
|
|
1287
|
+
assert impl == mock_impl1
|
|
1288
|
+
assert mock_get_label_map.call_count == 1
|
|
1289
|
+
|
|
1290
|
+
# Fourth operation: find same implementation (should use cache)
|
|
1291
|
+
impl2 = matcher.find_control_implementation("AC-1", 50)
|
|
1292
|
+
assert impl2 == mock_impl1
|
|
1293
|
+
assert mock_get_label_map.call_count == 1 # Should not increase
|
|
1294
|
+
|
|
1295
|
+
# Clear cache
|
|
1296
|
+
matcher.clear_cache()
|
|
1297
|
+
|
|
1298
|
+
# Fifth operation: after cache clear, should fetch again
|
|
1299
|
+
control3 = matcher.find_control_in_catalog("AC-1", 1)
|
|
1300
|
+
assert control3 == mock_control1
|
|
1301
|
+
assert mock_get_catalog.call_count == 2 # Should increase now
|
|
1302
|
+
|
|
1303
|
+
@patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
|
|
1304
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1305
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1306
|
+
def test_bulk_operations_with_mixed_results(self, mock_app_class, mock_api_class, mock_find_impl):
|
|
1307
|
+
"""Test bulk operations with some successes and some failures"""
|
|
1308
|
+
matcher = ControlMatcher()
|
|
1309
|
+
|
|
1310
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
1311
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
1312
|
+
|
|
1313
|
+
def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
|
|
1314
|
+
impl_map = {
|
|
1315
|
+
"AC-1": mock_impl1,
|
|
1316
|
+
"AC-2": mock_impl2,
|
|
1317
|
+
}
|
|
1318
|
+
return impl_map.get(control_id)
|
|
1319
|
+
|
|
1320
|
+
mock_find_impl.side_effect = find_impl_side_effect
|
|
1321
|
+
|
|
1322
|
+
# Bulk match with mixed results
|
|
1323
|
+
mappings = {
|
|
1324
|
+
"finding-001": "AC-1", # Will find
|
|
1325
|
+
"finding-002": "AC-2", # Will find
|
|
1326
|
+
"finding-003": "SI-4", # Won't find
|
|
1327
|
+
"finding-004": "CM-6", # Won't find
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
result = matcher.bulk_match_controls(mappings, 50)
|
|
1331
|
+
|
|
1332
|
+
assert result["finding-001"] == mock_impl1
|
|
1333
|
+
assert result["finding-002"] == mock_impl2
|
|
1334
|
+
assert result["finding-003"] is None
|
|
1335
|
+
assert result["finding-004"] is None
|
|
1336
|
+
assert len(result) == 4
|
|
1337
|
+
|
|
1338
|
+
@patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
|
|
1339
|
+
@patch(
|
|
1340
|
+
"regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
|
|
1341
|
+
)
|
|
1342
|
+
@patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
|
|
1343
|
+
@patch("regscale.integrations.control_matcher.Api")
|
|
1344
|
+
@patch("regscale.integrations.control_matcher.Application")
|
|
1345
|
+
def test_workflow_with_leading_zeros_catalog(
|
|
1346
|
+
self, mock_app_class, mock_api_class, mock_get_catalog, mock_get_label_map, mock_get_object
|
|
1347
|
+
):
|
|
1348
|
+
"""Test workflow when catalog has control IDs with leading zeros"""
|
|
1349
|
+
matcher = ControlMatcher()
|
|
1350
|
+
|
|
1351
|
+
# Catalog has controls with leading zeros
|
|
1352
|
+
mock_control1 = MagicMock(spec=SecurityControl)
|
|
1353
|
+
mock_control1.controlId = "AC-01"
|
|
1354
|
+
mock_control1.id = 100
|
|
1355
|
+
|
|
1356
|
+
mock_control2 = MagicMock(spec=SecurityControl)
|
|
1357
|
+
mock_control2.controlId = "AC-17(02)"
|
|
1358
|
+
mock_control2.id = 101
|
|
1359
|
+
|
|
1360
|
+
mock_get_catalog.return_value = [mock_control1, mock_control2]
|
|
1361
|
+
|
|
1362
|
+
# Implementations have standard format
|
|
1363
|
+
mock_impl1 = MagicMock(spec=ControlImplementation)
|
|
1364
|
+
mock_impl1.id = 200
|
|
1365
|
+
mock_impl1.controlID = 100
|
|
1366
|
+
|
|
1367
|
+
mock_impl2 = MagicMock(spec=ControlImplementation)
|
|
1368
|
+
mock_impl2.id = 201
|
|
1369
|
+
mock_impl2.controlID = 101
|
|
1370
|
+
|
|
1371
|
+
mock_get_label_map.return_value = {
|
|
1372
|
+
"AC-1": 200,
|
|
1373
|
+
"AC-17.2": 201,
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
def get_object_side_effect(impl_id):
|
|
1377
|
+
if impl_id == 200:
|
|
1378
|
+
return mock_impl1
|
|
1379
|
+
elif impl_id == 201:
|
|
1380
|
+
return mock_impl2
|
|
1381
|
+
return None
|
|
1382
|
+
|
|
1383
|
+
mock_get_object.side_effect = get_object_side_effect
|
|
1384
|
+
|
|
1385
|
+
# Search with standard format should find controls with leading zeros
|
|
1386
|
+
control1 = matcher.find_control_in_catalog("AC-1", 1)
|
|
1387
|
+
assert control1 == mock_control1
|
|
1388
|
+
|
|
1389
|
+
control2 = matcher.find_control_in_catalog("AC-17.2", 1)
|
|
1390
|
+
assert control2 == mock_control2
|
|
1391
|
+
|
|
1392
|
+
# Find implementations should work with either format
|
|
1393
|
+
impl1 = matcher.find_control_implementation("AC-01", 50)
|
|
1394
|
+
assert impl1 == mock_impl1
|
|
1395
|
+
|
|
1396
|
+
impl2 = matcher.find_control_implementation("AC-17(02)", 50)
|
|
1397
|
+
assert impl2 == mock_impl2
|