regscale-cli 6.25.0.1__py3-none-any.whl → 6.26.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/constants.py +20 -71
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/due_date_handler.py +118 -6
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +199 -130
- regscale/models/integration_models/cisa_kev_data.json +199 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
"""Test module for GCP scanner integration in RegScale CLI.
|
|
2
|
+
|
|
3
|
+
This module contains tests for the GCP scanner integration, focusing on asset fetching,
|
|
4
|
+
parsing, and the generation of integration findings based on GCP assets.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import datetime
|
|
9
|
+
import logging
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from unittest.mock import ANY, patch, MagicMock, Mock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from freezegun import freeze_time
|
|
15
|
+
from google.cloud import asset_v1
|
|
16
|
+
from proto.datetime_helpers import DatetimeWithNanoseconds
|
|
17
|
+
|
|
18
|
+
from regscale.core.utils.date import default_date_format
|
|
19
|
+
from regscale.integrations.commercial.gcp import gcp_control_tests
|
|
20
|
+
from regscale.integrations.commercial.gcp.__init__ import GCPScannerIntegration
|
|
21
|
+
from regscale.integrations.scanner_integration import IntegrationFinding, IntegrationAsset
|
|
22
|
+
from regscale.models import regscale_models
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def test_identifiers():
|
|
27
|
+
"""Provide consistent test identifiers."""
|
|
28
|
+
return {
|
|
29
|
+
"test_string": "test_gcp_integration_12345",
|
|
30
|
+
"plan_id": 999999,
|
|
31
|
+
"assessor_id": 888888,
|
|
32
|
+
"component_id_1": 777777,
|
|
33
|
+
"component_id_2": 777778,
|
|
34
|
+
"asset_id_1": 666666,
|
|
35
|
+
"asset_id_2": 666667,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_control_mappings():
|
|
41
|
+
"""Provide mock control implementation mappings with fake IDs."""
|
|
42
|
+
return {
|
|
43
|
+
"ac-2": 100001,
|
|
44
|
+
"au-2": 100002,
|
|
45
|
+
"ac-3": 100003,
|
|
46
|
+
"ac-5": 100004,
|
|
47
|
+
"ac-6": 100005,
|
|
48
|
+
"au-9": 100006,
|
|
49
|
+
"au-11": 100007,
|
|
50
|
+
"ca-3": 100008,
|
|
51
|
+
"cp-9": 100009,
|
|
52
|
+
"ia-2": 100010,
|
|
53
|
+
"sc-7": 100011,
|
|
54
|
+
"sc-12": 100012,
|
|
55
|
+
"si-4": 100013,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def mock_regscale_models(mocker):
|
|
61
|
+
"""Mock all RegScale model operations to avoid database dependencies."""
|
|
62
|
+
return {
|
|
63
|
+
"SecurityPlan": mocker.patch("regscale.models.regscale_models.SecurityPlan"),
|
|
64
|
+
"Component": mocker.patch("regscale.models.regscale_models.Component"),
|
|
65
|
+
"Asset": mocker.patch("regscale.models.regscale_models.Asset"),
|
|
66
|
+
"ComponentMapping": mocker.patch("regscale.models.regscale_models.ComponentMapping"),
|
|
67
|
+
"ControlImplementation": mocker.patch("regscale.models.regscale_models.ControlImplementation"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_mock_asset(
|
|
72
|
+
update_seconds: int, update_nanos: int, name: str, asset_type: str, ancestors: list
|
|
73
|
+
) -> asset_v1.Asset:
|
|
74
|
+
"""Create a mock GCP Asset with specified properties for testing."""
|
|
75
|
+
mock_asset = asset_v1.Asset()
|
|
76
|
+
dt = datetime.datetime.fromtimestamp(update_seconds, tz=datetime.timezone.utc)
|
|
77
|
+
mock_asset.update_time = DatetimeWithNanoseconds(
|
|
78
|
+
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, nanosecond=update_nanos, tzinfo=datetime.timezone.utc
|
|
79
|
+
)
|
|
80
|
+
mock_asset.name = name
|
|
81
|
+
mock_asset.asset_type = asset_type
|
|
82
|
+
mock_asset.ancestors.extend(ancestors)
|
|
83
|
+
return mock_asset
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture
|
|
87
|
+
def sample_gcp_assets(test_identifiers):
|
|
88
|
+
"""Create consistent test data for GCP assets."""
|
|
89
|
+
test_string = test_identifiers["test_string"]
|
|
90
|
+
return [
|
|
91
|
+
create_mock_asset(
|
|
92
|
+
update_seconds=1703126248,
|
|
93
|
+
update_nanos=185897000,
|
|
94
|
+
name=f"//cloudbilling.googleapis.com/projects/test-project-1/billingInfo/{test_string}",
|
|
95
|
+
asset_type=f"cloudbilling.googleapis.com/ProjectBillingInfo/{test_string}",
|
|
96
|
+
ancestors=["projects/100001", "folders/200001", "organizations/300001"],
|
|
97
|
+
),
|
|
98
|
+
create_mock_asset(
|
|
99
|
+
update_seconds=1703126249,
|
|
100
|
+
update_nanos=185897001,
|
|
101
|
+
name=f"//cloudbilling.googleapis.com/projects/test-project-2/billingInfo/{test_string}",
|
|
102
|
+
asset_type=f"cloudbilling.googleapis.com/ProjectBillingInfo/{test_string}",
|
|
103
|
+
ancestors=["projects/100002", "folders/200002", "organizations/300002"],
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.fixture
|
|
109
|
+
def mock_asset_v1_client(sample_gcp_assets):
|
|
110
|
+
"""Mock GCP AssetServiceClient for testing."""
|
|
111
|
+
with patch("google.cloud.asset_v1.AssetServiceClient") as mock_client, patch(
|
|
112
|
+
"regscale.integrations.commercial.gcp.auth.ensure_gcp_api_enabled"
|
|
113
|
+
) as mock_api_enabled:
|
|
114
|
+
mock_client.return_value.list_assets.return_value = sample_gcp_assets
|
|
115
|
+
mock_api_enabled.return_value = None
|
|
116
|
+
yield mock_client
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def assert_integration_finding_structure(finding: IntegrationFinding, expected_status=None, expected_severity=None):
|
|
120
|
+
"""Helper to validate IntegrationFinding structure."""
|
|
121
|
+
assert isinstance(finding, IntegrationFinding)
|
|
122
|
+
assert finding.title == "GCP Scanner Integration Control Assessment"
|
|
123
|
+
assert finding.date_created == "2024-01-24 16:16:25"
|
|
124
|
+
assert finding.date_last_updated == "2024-01-24 16:16:25"
|
|
125
|
+
if expected_status:
|
|
126
|
+
assert finding.status == expected_status
|
|
127
|
+
if expected_severity:
|
|
128
|
+
assert finding.severity == expected_severity
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def assert_integration_asset_structure(asset: IntegrationAsset, test_identifiers: dict):
|
|
132
|
+
"""Helper to validate IntegrationAsset structure."""
|
|
133
|
+
assert isinstance(asset, IntegrationAsset)
|
|
134
|
+
assert asset.asset_owner_id == str(test_identifiers["assessor_id"])
|
|
135
|
+
assert asset.parent_id == test_identifiers["plan_id"]
|
|
136
|
+
assert asset.parent_module == "security_plans"
|
|
137
|
+
assert asset.asset_category == "GCP"
|
|
138
|
+
assert asset.status == "Active (On Network)"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@freeze_time("2024-01-24 16:16:25")
|
|
142
|
+
class TestGCPScannerIntegration:
|
|
143
|
+
"""Test suite for GCP Scanner Integration."""
|
|
144
|
+
|
|
145
|
+
@pytest.fixture(autouse=True)
|
|
146
|
+
def setup_test_isolation(self, mocker, test_identifiers, mock_control_mappings):
|
|
147
|
+
"""Ensure each test runs in isolation with mocked dependencies."""
|
|
148
|
+
# Mock GCP variables to avoid external dependencies
|
|
149
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", "test-project-123")
|
|
150
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpOrganizationId", "test-org-12345")
|
|
151
|
+
mocker.patch(
|
|
152
|
+
"regscale.integrations.commercial.gcp.variables.GcpVariables.gcpCredentials", "test/path/credentials.json"
|
|
153
|
+
)
|
|
154
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpScanType", "project")
|
|
155
|
+
|
|
156
|
+
# Mock all API calls made during ScannerIntegration.__init__
|
|
157
|
+
mocker.patch(
|
|
158
|
+
"regscale.models.regscale_models.ControlImplementation.get_control_label_map_by_parent",
|
|
159
|
+
return_value=mock_control_mappings,
|
|
160
|
+
)
|
|
161
|
+
mocker.patch(
|
|
162
|
+
"regscale.models.regscale_models.ControlImplementation.get_control_id_map_by_parent",
|
|
163
|
+
return_value={v: k for k, v in mock_control_mappings.items()},
|
|
164
|
+
)
|
|
165
|
+
mocker.patch(
|
|
166
|
+
"regscale.models.regscale_models.Issue.get_open_issues_ids_by_implementation_id",
|
|
167
|
+
return_value={},
|
|
168
|
+
)
|
|
169
|
+
mocker.patch(
|
|
170
|
+
"regscale.models.regscale_models.Issue.get_user_id",
|
|
171
|
+
return_value=str(test_identifiers["assessor_id"]),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Mock get_assessor_id method to avoid authentication dependencies
|
|
175
|
+
mocker.patch.object(GCPScannerIntegration, "get_assessor_id", return_value=str(test_identifiers["assessor_id"]))
|
|
176
|
+
|
|
177
|
+
# Mock GCP control tests to avoid import dependencies
|
|
178
|
+
mock_control_tests = {
|
|
179
|
+
"ac-2": {
|
|
180
|
+
"PUBLIC_BUCKET_ACL": {
|
|
181
|
+
"severity": "HIGH",
|
|
182
|
+
"description": "Cloud Storage buckets should not be anonymously or publicly accessible",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
"ac-3": {
|
|
186
|
+
"PUBLIC_BUCKET_ACL": {
|
|
187
|
+
"severity": "HIGH",
|
|
188
|
+
"description": "Test PUBLIC_BUCKET_ACL description",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"au-9": {
|
|
192
|
+
"PUBLIC_LOG_BUCKET": {
|
|
193
|
+
"severity": "HIGH",
|
|
194
|
+
"description": "Storage buckets used as log sinks should not be publicly accessible",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
"si-4": {
|
|
198
|
+
"FLOW_LOGS_DISABLED": {
|
|
199
|
+
"severity": "LOW",
|
|
200
|
+
"description": "VPC Flow logs should be Enabled for every subnet in VPC Network",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
mocker.patch("regscale.integrations.commercial.gcp.control_tests.gcp_control_tests", mock_control_tests)
|
|
205
|
+
|
|
206
|
+
# Mock module slugs and strings to avoid RegScale internal dependencies
|
|
207
|
+
mocker.patch.object(regscale_models.SecurityPlan, "get_module_slug", return_value="security_plans")
|
|
208
|
+
mocker.patch.object(regscale_models.Component, "get_module_slug", return_value="components")
|
|
209
|
+
mocker.patch.object(regscale_models.SecurityPlan, "get_module_string", return_value="security_plans")
|
|
210
|
+
mocker.patch.object(regscale_models.Component, "get_module_string", return_value="components")
|
|
211
|
+
|
|
212
|
+
# Mock Application and APIHandler to avoid initialization API calls
|
|
213
|
+
mock_app = mocker.patch("regscale.core.app.application.Application")
|
|
214
|
+
mock_app.return_value.config = {"domain": "https://test.regscale.com"}
|
|
215
|
+
|
|
216
|
+
mock_api_handler = mocker.patch("regscale.core.app.utils.api_handler.APIHandler")
|
|
217
|
+
mock_api_handler.return_value.regscale_version = "test-version"
|
|
218
|
+
|
|
219
|
+
mocker.patch("regscale.integrations.public.cisa.pull_cisa_kev", return_value={})
|
|
220
|
+
|
|
221
|
+
# Mock AssetMapping and other model operations that make API calls
|
|
222
|
+
mocker.patch("regscale.models.regscale_models.AssetMapping.populate_cache_by_plan", return_value=None)
|
|
223
|
+
mocker.patch("regscale.models.regscale_models.AssetMapping.get_plan_objects", return_value=[])
|
|
224
|
+
|
|
225
|
+
# Mock STIG mapper loading
|
|
226
|
+
mocker.patch.object(GCPScannerIntegration, "load_stig_mapper", return_value=None)
|
|
227
|
+
|
|
228
|
+
# Mock GCP authentication functions to prevent real API calls
|
|
229
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials", return_value=None)
|
|
230
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_api_enabled", return_value=None)
|
|
231
|
+
|
|
232
|
+
def test_get_passed_findings(self, mocker, test_identifiers, mock_control_mappings):
|
|
233
|
+
"""Test get_passed_findings returns properly structured passed findings."""
|
|
234
|
+
mocker.patch(
|
|
235
|
+
"regscale.models.regscale_models.ControlImplementation.get_control_label_map_by_plan",
|
|
236
|
+
return_value=mock_control_mappings,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
240
|
+
|
|
241
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
242
|
+
|
|
243
|
+
assert isinstance(passed_findings, list)
|
|
244
|
+
assert len(passed_findings) > 0
|
|
245
|
+
|
|
246
|
+
first_finding = passed_findings[0]
|
|
247
|
+
assert_integration_finding_structure(
|
|
248
|
+
first_finding,
|
|
249
|
+
expected_status=regscale_models.ControlTestResultStatus.PASS,
|
|
250
|
+
expected_severity=regscale_models.IssueSeverity.Low,
|
|
251
|
+
)
|
|
252
|
+
assert first_finding.plugin_name
|
|
253
|
+
assert isinstance(first_finding.control_labels, list)
|
|
254
|
+
assert len(first_finding.control_labels) > 0
|
|
255
|
+
|
|
256
|
+
expected_finding_count = sum(len(categories) for categories in gcp_control_tests.values())
|
|
257
|
+
assert len(passed_findings) == expected_finding_count
|
|
258
|
+
|
|
259
|
+
for finding in passed_findings:
|
|
260
|
+
assert_integration_finding_structure(finding, expected_status=regscale_models.ControlTestResultStatus.PASS)
|
|
261
|
+
|
|
262
|
+
def test_fetch_assets_calls_list_assets(self, mock_asset_v1_client: MagicMock, test_identifiers, sample_gcp_assets):
|
|
263
|
+
"""Test fetch_assets calls AssetServiceClient.list_assets with correct parameters."""
|
|
264
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
265
|
+
assets = list(gcp_integration.fetch_assets())
|
|
266
|
+
|
|
267
|
+
assert len(assets) == len(sample_gcp_assets)
|
|
268
|
+
mock_asset_v1_client.return_value.list_assets.assert_called_once_with(request=ANY)
|
|
269
|
+
|
|
270
|
+
@pytest.mark.parametrize("asset_index", [0, 1])
|
|
271
|
+
def test_parse_asset_transforms_gcp_asset(
|
|
272
|
+
self, mock_asset_v1_client: MagicMock, test_identifiers, sample_gcp_assets, asset_index
|
|
273
|
+
):
|
|
274
|
+
"""Test parse_asset transforms GCP asset into properly structured IntegrationAsset."""
|
|
275
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
276
|
+
asset = sample_gcp_assets[asset_index]
|
|
277
|
+
|
|
278
|
+
integration_asset = gcp_integration.parse_asset(asset)
|
|
279
|
+
|
|
280
|
+
assert_integration_asset_structure(integration_asset, test_identifiers)
|
|
281
|
+
assert integration_asset.name == asset.name
|
|
282
|
+
assert integration_asset.identifier == asset.name
|
|
283
|
+
assert integration_asset.asset_type == asset.asset_type
|
|
284
|
+
assert integration_asset.date_last_updated == asset.update_time.strftime(default_date_format)
|
|
285
|
+
|
|
286
|
+
@pytest.fixture
|
|
287
|
+
def mock_regscale_data(self, test_identifiers, sample_gcp_assets):
|
|
288
|
+
"""Create mock RegScale component and asset data for testing."""
|
|
289
|
+
test_string = test_identifiers["test_string"]
|
|
290
|
+
return {
|
|
291
|
+
"components": [
|
|
292
|
+
Mock(id=test_identifiers["component_id_1"], title=f"Test Component 1/{test_string}", delete=Mock()),
|
|
293
|
+
Mock(id=test_identifiers["component_id_2"], title=f"Test Component 2/{test_string}", delete=Mock()),
|
|
294
|
+
],
|
|
295
|
+
"assets": [
|
|
296
|
+
Mock(id=test_identifiers["asset_id_1"], name=sample_gcp_assets[0].name, delete=Mock()),
|
|
297
|
+
Mock(id=test_identifiers["asset_id_2"], name=sample_gcp_assets[1].name, delete=Mock()),
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
def create_gcp_integration_with_control_tests(self, test_identifiers):
|
|
302
|
+
"""Helper to create GCP integration with control tests setup."""
|
|
303
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
304
|
+
gcp_integration.gcp_control_tests = copy.copy(gcp_control_tests)
|
|
305
|
+
return gcp_integration
|
|
306
|
+
|
|
307
|
+
def test_sync_assets(
|
|
308
|
+
self,
|
|
309
|
+
mocker,
|
|
310
|
+
mock_asset_v1_client: MagicMock,
|
|
311
|
+
mock_regscale_models,
|
|
312
|
+
test_identifiers,
|
|
313
|
+
sample_gcp_assets,
|
|
314
|
+
mock_regscale_data,
|
|
315
|
+
):
|
|
316
|
+
"""Test sync_assets calls parent ScannerIntegration.sync_assets method."""
|
|
317
|
+
plan_id = test_identifiers["plan_id"]
|
|
318
|
+
|
|
319
|
+
# Setup mocks for database operations
|
|
320
|
+
mock_regscale_models["Component"].get_all_by_parent.return_value = []
|
|
321
|
+
mock_regscale_models["Asset"].get_all_by_parent.return_value = []
|
|
322
|
+
|
|
323
|
+
# Mock ComponentMapping to return None (not found)
|
|
324
|
+
mock_component_mapping = Mock()
|
|
325
|
+
mock_component_mapping.find_by_unique.return_value = None
|
|
326
|
+
mock_regscale_models["ComponentMapping"].return_value = mock_component_mapping
|
|
327
|
+
|
|
328
|
+
# Mock asset creation to prevent actual database calls
|
|
329
|
+
mock_asset_instance = Mock()
|
|
330
|
+
mock_asset_instance.create_or_update_with_status.return_value = None
|
|
331
|
+
mocker.patch("regscale.models.regscale_models.Asset", return_value=mock_asset_instance)
|
|
332
|
+
|
|
333
|
+
# Mock the parent class sync_assets method to avoid real execution
|
|
334
|
+
mock_parent_sync = mocker.patch(
|
|
335
|
+
"regscale.integrations.scanner_integration.ScannerIntegration.sync_assets",
|
|
336
|
+
return_value=len(sample_gcp_assets),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Call the classmethod to test inheritance
|
|
340
|
+
result = GCPScannerIntegration.sync_assets(plan_id=plan_id)
|
|
341
|
+
|
|
342
|
+
# Verify that parent sync_assets was called with correct parameters
|
|
343
|
+
mock_parent_sync.assert_called_once_with(plan_id=plan_id)
|
|
344
|
+
assert result == len(sample_gcp_assets)
|
|
345
|
+
|
|
346
|
+
@pytest.fixture
|
|
347
|
+
def mock_gcp_finding(self):
|
|
348
|
+
"""Create a comprehensive mock GCP Security Center finding."""
|
|
349
|
+
mock_finding = Mock()
|
|
350
|
+
mock_finding.name = "organizations/123456789012/sources/12345/findings/test-finding-001"
|
|
351
|
+
mock_finding.category = "PUBLIC_BUCKET_ACL"
|
|
352
|
+
mock_finding.severity = 2 # HIGH
|
|
353
|
+
mock_finding.description = "Test finding description"
|
|
354
|
+
mock_finding.external_uri = "https://console.cloud.google.com/test"
|
|
355
|
+
mock_finding.resource_name = "//storage.googleapis.com/projects/test-project/global/buckets/test-bucket"
|
|
356
|
+
|
|
357
|
+
# Mock source_properties with proper get() method behavior
|
|
358
|
+
mock_source_properties = Mock()
|
|
359
|
+
mock_source_properties.get.side_effect = lambda key, default="": {
|
|
360
|
+
"Recommendation": "Test recommendation",
|
|
361
|
+
"Explanation": "Test explanation",
|
|
362
|
+
}.get(key, default)
|
|
363
|
+
mock_finding.source_properties = mock_source_properties
|
|
364
|
+
|
|
365
|
+
# Mock compliances
|
|
366
|
+
mock_compliance = Mock()
|
|
367
|
+
mock_compliance.standard = "nist"
|
|
368
|
+
mock_compliance.ids = ["AC-2", "AC-3"]
|
|
369
|
+
mock_finding.compliances = [mock_compliance]
|
|
370
|
+
|
|
371
|
+
return mock_finding
|
|
372
|
+
|
|
373
|
+
@pytest.fixture
|
|
374
|
+
def mock_gcp_finding_no_nist(self):
|
|
375
|
+
"""Create mock GCP finding without NIST controls."""
|
|
376
|
+
mock_finding = Mock()
|
|
377
|
+
mock_finding.name = "organizations/123456789012/sources/12345/findings/test-finding-002"
|
|
378
|
+
mock_finding.category = "OPEN_FIREWALL"
|
|
379
|
+
mock_finding.description = "Firewall rule allows unrestricted access"
|
|
380
|
+
mock_finding.severity = 1 # CRITICAL
|
|
381
|
+
mock_finding.compliances = [] # No NIST controls
|
|
382
|
+
mock_source_properties = Mock()
|
|
383
|
+
mock_source_properties.get.return_value = ""
|
|
384
|
+
mock_finding.source_properties = mock_source_properties
|
|
385
|
+
return mock_finding
|
|
386
|
+
|
|
387
|
+
@pytest.fixture
|
|
388
|
+
def mock_failed_findings_response(self, mock_gcp_finding):
|
|
389
|
+
"""Create mock ListFindingsPager response."""
|
|
390
|
+
mock_finding_wrapper = Mock()
|
|
391
|
+
mock_finding_wrapper.finding = mock_gcp_finding
|
|
392
|
+
return [mock_finding_wrapper]
|
|
393
|
+
|
|
394
|
+
@pytest.fixture
|
|
395
|
+
def mock_security_center_client(self, mocker, mock_failed_findings_response):
|
|
396
|
+
"""Mock GCP Security Center client."""
|
|
397
|
+
mock_client = Mock()
|
|
398
|
+
mock_client.list_findings.return_value = mock_failed_findings_response
|
|
399
|
+
mocker.patch("google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client)
|
|
400
|
+
return mock_client
|
|
401
|
+
|
|
402
|
+
@pytest.mark.parametrize(
|
|
403
|
+
"scan_type,expected_sources",
|
|
404
|
+
[
|
|
405
|
+
("project", "projects/test-project-123/sources/-"),
|
|
406
|
+
("organization", "organizations/test-org-12345/sources/-"),
|
|
407
|
+
],
|
|
408
|
+
)
|
|
409
|
+
def test_get_failed_findings_scan_type_handling(self, mocker, scan_type, expected_sources):
|
|
410
|
+
"""Test get_failed_findings handles different scan types correctly."""
|
|
411
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpScanType", scan_type)
|
|
412
|
+
mock_client = Mock()
|
|
413
|
+
mock_findings = Mock()
|
|
414
|
+
mock_client.list_findings.return_value = mock_findings
|
|
415
|
+
|
|
416
|
+
# Mock the entire client creation flow
|
|
417
|
+
mocker.patch("google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client)
|
|
418
|
+
|
|
419
|
+
result = GCPScannerIntegration.get_failed_findings()
|
|
420
|
+
|
|
421
|
+
assert result == mock_findings
|
|
422
|
+
mock_client.list_findings.assert_called_once_with(request={"parent": expected_sources})
|
|
423
|
+
|
|
424
|
+
def test_get_failed_findings_invalid_sources(self, mocker):
|
|
425
|
+
"""Test get_failed_findings raises NameError for invalid GCP sources."""
|
|
426
|
+
from google.api_core.exceptions import InvalidArgument
|
|
427
|
+
|
|
428
|
+
mock_client = Mock()
|
|
429
|
+
mock_client.list_findings.side_effect = InvalidArgument("Invalid parent")
|
|
430
|
+
mocker.patch("google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client)
|
|
431
|
+
|
|
432
|
+
with pytest.raises(NameError, match="gcpFindingSources is set incorrectly"):
|
|
433
|
+
GCPScannerIntegration.get_failed_findings()
|
|
434
|
+
|
|
435
|
+
def test_get_failed_findings_success(self, mock_security_center_client, mock_failed_findings_response):
|
|
436
|
+
"""Test get_failed_findings successful authentication and client creation."""
|
|
437
|
+
result = GCPScannerIntegration.get_failed_findings()
|
|
438
|
+
|
|
439
|
+
assert result == mock_failed_findings_response
|
|
440
|
+
mock_security_center_client.list_findings.assert_called_once_with(
|
|
441
|
+
request={"parent": "projects/test-project-123/sources/-"}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def test_get_failed_findings_empty_response(self, mocker):
|
|
445
|
+
"""Test get_failed_findings with empty findings response."""
|
|
446
|
+
mock_client = Mock()
|
|
447
|
+
mock_client.list_findings.return_value = []
|
|
448
|
+
mocker.patch("google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client)
|
|
449
|
+
|
|
450
|
+
result = GCPScannerIntegration.get_failed_findings()
|
|
451
|
+
|
|
452
|
+
assert result == []
|
|
453
|
+
mock_client.list_findings.assert_called_once()
|
|
454
|
+
|
|
455
|
+
def test_get_failed_findings_logging_behavior(self, mocker, caplog):
|
|
456
|
+
"""Test get_failed_findings logging behavior."""
|
|
457
|
+
mock_client = Mock()
|
|
458
|
+
mock_client.list_findings.return_value = []
|
|
459
|
+
mocker.patch("google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client)
|
|
460
|
+
|
|
461
|
+
with caplog.at_level(logging.INFO):
|
|
462
|
+
GCPScannerIntegration.get_failed_findings()
|
|
463
|
+
|
|
464
|
+
assert "Fetching GCP findings..." in caplog.text
|
|
465
|
+
assert "Fetched GCP findings." in caplog.text
|
|
466
|
+
|
|
467
|
+
def test_get_failed_findings_error_logging(self, mocker, caplog):
|
|
468
|
+
"""Test get_failed_findings logs errors properly."""
|
|
469
|
+
from google.api_core.exceptions import InvalidArgument
|
|
470
|
+
|
|
471
|
+
mock_client = Mock()
|
|
472
|
+
mock_client.list_findings.side_effect = InvalidArgument("Invalid parent")
|
|
473
|
+
mocker.patch("google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client)
|
|
474
|
+
|
|
475
|
+
with caplog.at_level(logging.ERROR):
|
|
476
|
+
with pytest.raises(NameError):
|
|
477
|
+
GCPScannerIntegration.get_failed_findings()
|
|
478
|
+
|
|
479
|
+
assert "gcpFindingSources is set incorrectly" in caplog.text
|
|
480
|
+
|
|
481
|
+
def test_parse_finding_with_nist_controls(self, test_identifiers, mock_gcp_finding):
|
|
482
|
+
"""Test parse_finding successfully processes finding with NIST controls."""
|
|
483
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
484
|
+
|
|
485
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json_data"}'):
|
|
486
|
+
result = gcp_integration.parse_finding(mock_gcp_finding)
|
|
487
|
+
|
|
488
|
+
assert result is not None
|
|
489
|
+
assert isinstance(result, IntegrationFinding)
|
|
490
|
+
assert result.control_labels == ["ac-2", "ac-3"]
|
|
491
|
+
assert result.category == "PUBLIC_BUCKET_ACL"
|
|
492
|
+
assert result.severity == regscale_models.IssueSeverity.High
|
|
493
|
+
assert result.status == regscale_models.ControlTestResultStatus.FAIL
|
|
494
|
+
|
|
495
|
+
def test_parse_finding_no_nist_controls(self, test_identifiers, mock_gcp_finding_no_nist):
|
|
496
|
+
"""Test parse_finding returns None when no NIST controls found."""
|
|
497
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
498
|
+
|
|
499
|
+
result = gcp_integration.parse_finding(mock_gcp_finding_no_nist)
|
|
500
|
+
|
|
501
|
+
assert result is None
|
|
502
|
+
|
|
503
|
+
def test_parse_finding_multiple_nist_controls(self, test_identifiers):
|
|
504
|
+
"""Test parse_finding handles multiple NIST controls correctly."""
|
|
505
|
+
mock_finding = Mock()
|
|
506
|
+
mock_finding.category = "PUBLIC_BUCKET_ACL"
|
|
507
|
+
mock_finding.description = "Test finding"
|
|
508
|
+
mock_finding.severity = 2
|
|
509
|
+
mock_finding.external_uri = ""
|
|
510
|
+
mock_finding.resource_name = ""
|
|
511
|
+
mock_source_properties = Mock()
|
|
512
|
+
mock_source_properties.get.return_value = ""
|
|
513
|
+
mock_finding.source_properties = mock_source_properties
|
|
514
|
+
|
|
515
|
+
mock_compliance = Mock()
|
|
516
|
+
mock_compliance.standard = "nist"
|
|
517
|
+
mock_compliance.ids = ["AC-2", "au-2", "Si-4"] # Mixed case
|
|
518
|
+
mock_finding.compliances = [mock_compliance]
|
|
519
|
+
|
|
520
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
521
|
+
|
|
522
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
523
|
+
result = gcp_integration.parse_finding(mock_finding)
|
|
524
|
+
|
|
525
|
+
assert result.control_labels == ["ac-2", "au-2", "si-4"]
|
|
526
|
+
|
|
527
|
+
def test_parse_finding_control_test_status_update(self, test_identifiers, mock_gcp_finding):
|
|
528
|
+
"""Test parse_finding updates control test status to Failed."""
|
|
529
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
530
|
+
|
|
531
|
+
# Use minimal control tests for this test
|
|
532
|
+
test_control_tests = {
|
|
533
|
+
"ac-2": {"PUBLIC_BUCKET_ACL": {"severity": "HIGH", "description": "Test description"}},
|
|
534
|
+
"ac-3": {"PUBLIC_BUCKET_ACL": {"severity": "HIGH", "description": "Test description"}},
|
|
535
|
+
}
|
|
536
|
+
gcp_integration.gcp_control_tests = copy.deepcopy(test_control_tests)
|
|
537
|
+
|
|
538
|
+
# Verify initial state
|
|
539
|
+
assert "status" not in gcp_integration.gcp_control_tests["ac-2"]["PUBLIC_BUCKET_ACL"]
|
|
540
|
+
|
|
541
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
542
|
+
gcp_integration.parse_finding(mock_gcp_finding)
|
|
543
|
+
|
|
544
|
+
# Verify status was set to Failed
|
|
545
|
+
assert gcp_integration.gcp_control_tests["ac-2"]["PUBLIC_BUCKET_ACL"]["status"] == "Failed"
|
|
546
|
+
assert gcp_integration.gcp_control_tests["ac-3"]["PUBLIC_BUCKET_ACL"]["status"] == "Failed"
|
|
547
|
+
|
|
548
|
+
@pytest.mark.parametrize(
|
|
549
|
+
"severity,expected_regscale_severity",
|
|
550
|
+
[
|
|
551
|
+
(0, regscale_models.IssueSeverity.Low),
|
|
552
|
+
(1, regscale_models.IssueSeverity.Critical),
|
|
553
|
+
(2, regscale_models.IssueSeverity.High),
|
|
554
|
+
(3, regscale_models.IssueSeverity.Moderate),
|
|
555
|
+
(4, regscale_models.IssueSeverity.Low),
|
|
556
|
+
(999, regscale_models.IssueSeverity.Low), # Unknown severity defaults to Low
|
|
557
|
+
],
|
|
558
|
+
)
|
|
559
|
+
def test_parse_finding_severity_mapping(self, test_identifiers, severity, expected_regscale_severity):
|
|
560
|
+
"""Test parse_finding correctly maps GCP severities to RegScale severities."""
|
|
561
|
+
mock_finding = Mock()
|
|
562
|
+
mock_finding.category = "TEST_SEVERITY"
|
|
563
|
+
mock_finding.description = "Test severity mapping"
|
|
564
|
+
mock_finding.severity = severity
|
|
565
|
+
mock_finding.external_uri = ""
|
|
566
|
+
mock_finding.resource_name = ""
|
|
567
|
+
mock_source_properties = Mock()
|
|
568
|
+
mock_source_properties.get.return_value = ""
|
|
569
|
+
mock_finding.source_properties = mock_source_properties
|
|
570
|
+
|
|
571
|
+
mock_compliance = Mock()
|
|
572
|
+
mock_compliance.standard = "nist"
|
|
573
|
+
mock_compliance.ids = ["ac-2"]
|
|
574
|
+
mock_finding.compliances = [mock_compliance]
|
|
575
|
+
|
|
576
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
577
|
+
|
|
578
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
579
|
+
result = gcp_integration.parse_finding(mock_finding)
|
|
580
|
+
|
|
581
|
+
assert result.severity == expected_regscale_severity
|
|
582
|
+
assert result.impact == expected_regscale_severity
|
|
583
|
+
|
|
584
|
+
def test_parse_finding_missing_source_properties(self, test_identifiers, mock_gcp_finding):
|
|
585
|
+
"""Test parse_finding handles missing source properties gracefully."""
|
|
586
|
+
# Remove source_properties entirely
|
|
587
|
+
mock_gcp_finding.source_properties = {}
|
|
588
|
+
|
|
589
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
590
|
+
|
|
591
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
592
|
+
result = gcp_integration.parse_finding(mock_gcp_finding)
|
|
593
|
+
|
|
594
|
+
assert result is not None
|
|
595
|
+
# Should handle missing keys gracefully
|
|
596
|
+
assert result.gaps is not None
|
|
597
|
+
assert result.observations is not None
|
|
598
|
+
|
|
599
|
+
def test_parse_finding_control_labels_case_insensitive(self, test_identifiers):
|
|
600
|
+
"""Test parse_finding handles case-insensitive control labels properly."""
|
|
601
|
+
mock_finding = Mock()
|
|
602
|
+
mock_finding.category = "PUBLIC_BUCKET_ACL"
|
|
603
|
+
mock_finding.description = "Test case handling"
|
|
604
|
+
mock_finding.severity = 2
|
|
605
|
+
mock_finding.external_uri = ""
|
|
606
|
+
mock_finding.resource_name = ""
|
|
607
|
+
mock_source_properties = Mock()
|
|
608
|
+
mock_source_properties.get.return_value = ""
|
|
609
|
+
mock_finding.source_properties = mock_source_properties
|
|
610
|
+
|
|
611
|
+
mock_compliance = Mock()
|
|
612
|
+
mock_compliance.standard = "nist"
|
|
613
|
+
mock_compliance.ids = ["AC-2", "au-2", "Si-4"] # Mixed case
|
|
614
|
+
mock_finding.compliances = [mock_compliance]
|
|
615
|
+
|
|
616
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
617
|
+
|
|
618
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
619
|
+
result = gcp_integration.parse_finding(mock_finding)
|
|
620
|
+
|
|
621
|
+
# Verify control test was marked as failed for ac-2, PUBLIC_BUCKET_ACL
|
|
622
|
+
assert gcp_integration.gcp_control_tests["ac-2"]["PUBLIC_BUCKET_ACL"]["status"] == "Failed"
|
|
623
|
+
# Verify other control tests are still available for passed findings
|
|
624
|
+
assert result.control_labels == ["ac-2", "au-2", "si-4"]
|
|
625
|
+
|
|
626
|
+
def test_parse_finding_json_serialization(self, test_identifiers, mock_gcp_finding):
|
|
627
|
+
"""Test parse_finding handles JSON serialization properly."""
|
|
628
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
629
|
+
|
|
630
|
+
test_json = '{"name": "test-finding", "category": "PUBLIC_BUCKET_ACL"}'
|
|
631
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value=test_json):
|
|
632
|
+
result = gcp_integration.parse_finding(mock_gcp_finding)
|
|
633
|
+
|
|
634
|
+
assert result.evidence == test_json
|
|
635
|
+
|
|
636
|
+
def test_parse_finding_non_nist_compliance_ignored(self, test_identifiers):
|
|
637
|
+
"""Test parse_finding ignores non-NIST compliance standards."""
|
|
638
|
+
mock_finding = Mock()
|
|
639
|
+
mock_finding.category = "TEST_CATEGORY"
|
|
640
|
+
mock_finding.description = "Test non-NIST compliance"
|
|
641
|
+
mock_finding.severity = 2
|
|
642
|
+
mock_finding.external_uri = ""
|
|
643
|
+
mock_finding.resource_name = ""
|
|
644
|
+
mock_source_properties = Mock()
|
|
645
|
+
mock_source_properties.get.return_value = ""
|
|
646
|
+
mock_finding.source_properties = mock_source_properties
|
|
647
|
+
|
|
648
|
+
# Non-NIST compliance
|
|
649
|
+
mock_compliance_iso = Mock()
|
|
650
|
+
mock_compliance_iso.standard = "iso27001"
|
|
651
|
+
mock_compliance_iso.ids = ["A.5.1.1"]
|
|
652
|
+
|
|
653
|
+
# Mixed with NIST
|
|
654
|
+
mock_compliance_nist = Mock()
|
|
655
|
+
mock_compliance_nist.standard = "nist"
|
|
656
|
+
mock_compliance_nist.ids = ["AC-2"]
|
|
657
|
+
|
|
658
|
+
mock_finding.compliances = [mock_compliance_iso, mock_compliance_nist]
|
|
659
|
+
|
|
660
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
661
|
+
|
|
662
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
663
|
+
result = gcp_integration.parse_finding(mock_finding)
|
|
664
|
+
|
|
665
|
+
# Should only include NIST controls
|
|
666
|
+
assert result.control_labels == ["ac-2"]
|
|
667
|
+
|
|
668
|
+
def test_fetch_findings_combines_failed_and_passed(self, mocker, test_identifiers, mock_gcp_finding):
|
|
669
|
+
"""Test fetch_findings combines failed and passed findings correctly."""
|
|
670
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
671
|
+
|
|
672
|
+
# Mock get_failed_findings to return our test finding
|
|
673
|
+
mock_finding_wrapper = Mock()
|
|
674
|
+
mock_finding_wrapper.finding = mock_gcp_finding
|
|
675
|
+
mock_failed_findings = [mock_finding_wrapper]
|
|
676
|
+
mocker.patch.object(GCPScannerIntegration, "get_failed_findings", return_value=mock_failed_findings)
|
|
677
|
+
|
|
678
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
679
|
+
findings = gcp_integration.fetch_findings()
|
|
680
|
+
|
|
681
|
+
# Should have both failed and passed findings
|
|
682
|
+
assert len(findings) > 1
|
|
683
|
+
failed_findings = [f for f in findings if f.status == regscale_models.ControlTestResultStatus.FAIL]
|
|
684
|
+
passed_findings = [f for f in findings if f.status == regscale_models.ControlTestResultStatus.PASS]
|
|
685
|
+
assert len(failed_findings) >= 1
|
|
686
|
+
assert len(passed_findings) >= 1
|
|
687
|
+
|
|
688
|
+
def test_fetch_findings_no_failed_findings(self, mocker, test_identifiers):
|
|
689
|
+
"""Test fetch_findings when no failed findings exist."""
|
|
690
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
691
|
+
|
|
692
|
+
# Mock get_failed_findings to return empty list
|
|
693
|
+
mocker.patch.object(GCPScannerIntegration, "get_failed_findings", return_value=[])
|
|
694
|
+
|
|
695
|
+
findings = gcp_integration.fetch_findings()
|
|
696
|
+
|
|
697
|
+
# Should still have passed findings
|
|
698
|
+
assert len(findings) > 0
|
|
699
|
+
assert all(f.status == regscale_models.ControlTestResultStatus.PASS for f in findings)
|
|
700
|
+
|
|
701
|
+
def test_fetch_findings_no_passed_findings(self, mocker, test_identifiers, mock_gcp_finding):
|
|
702
|
+
"""Test fetch_findings when all control tests are marked as failed."""
|
|
703
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
704
|
+
|
|
705
|
+
# Mark all control tests as failed
|
|
706
|
+
gcp_integration.gcp_control_tests = copy.copy(gcp_control_tests)
|
|
707
|
+
for control_label, categories in gcp_integration.gcp_control_tests.items():
|
|
708
|
+
for category in categories:
|
|
709
|
+
gcp_integration.gcp_control_tests[control_label][category]["status"] = "Failed"
|
|
710
|
+
|
|
711
|
+
mock_finding_wrapper = Mock()
|
|
712
|
+
mock_finding_wrapper.finding = mock_gcp_finding
|
|
713
|
+
mock_failed_findings = [mock_finding_wrapper]
|
|
714
|
+
mocker.patch.object(GCPScannerIntegration, "get_failed_findings", return_value=mock_failed_findings)
|
|
715
|
+
|
|
716
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
717
|
+
findings = gcp_integration.fetch_findings()
|
|
718
|
+
|
|
719
|
+
# Should only have failed findings
|
|
720
|
+
failed_findings = [f for f in findings if f.status == regscale_models.ControlTestResultStatus.FAIL]
|
|
721
|
+
passed_findings = [f for f in findings if f.status == regscale_models.ControlTestResultStatus.PASS]
|
|
722
|
+
assert len(failed_findings) >= 1
|
|
723
|
+
assert len(passed_findings) == 0
|
|
724
|
+
|
|
725
|
+
def test_fetch_findings_control_tests_state_management(self, mocker, test_identifiers, mock_gcp_finding):
|
|
726
|
+
"""Test fetch_findings properly manages control test state."""
|
|
727
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
728
|
+
|
|
729
|
+
mock_finding_wrapper = Mock()
|
|
730
|
+
mock_finding_wrapper.finding = mock_gcp_finding
|
|
731
|
+
mocker.patch.object(GCPScannerIntegration, "get_failed_findings", return_value=[mock_finding_wrapper])
|
|
732
|
+
|
|
733
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
734
|
+
gcp_integration.fetch_findings()
|
|
735
|
+
|
|
736
|
+
# Verify control test was marked as failed for ac-2, PUBLIC_BUCKET_ACL
|
|
737
|
+
assert gcp_integration.gcp_control_tests["ac-2"]["PUBLIC_BUCKET_ACL"]["status"] == "Failed"
|
|
738
|
+
# Verify other control tests are still available for passed findings
|
|
739
|
+
|
|
740
|
+
def test_fetch_findings_failed_findings_filtering(
|
|
741
|
+
self, mocker, test_identifiers, mock_gcp_finding, mock_gcp_finding_no_nist
|
|
742
|
+
):
|
|
743
|
+
"""Test fetch_findings properly filters findings that return None."""
|
|
744
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
745
|
+
|
|
746
|
+
# Mix of findings - one with NIST controls, one without
|
|
747
|
+
mock_finding_wrapper1 = Mock()
|
|
748
|
+
mock_finding_wrapper1.finding = mock_gcp_finding
|
|
749
|
+
mock_finding_wrapper2 = Mock()
|
|
750
|
+
mock_finding_wrapper2.finding = mock_gcp_finding_no_nist
|
|
751
|
+
|
|
752
|
+
mock_failed_findings = [mock_finding_wrapper1, mock_finding_wrapper2]
|
|
753
|
+
mocker.patch.object(GCPScannerIntegration, "get_failed_findings", return_value=mock_failed_findings)
|
|
754
|
+
|
|
755
|
+
with patch("google.cloud.securitycenter_v1.Finding.to_json", return_value='{"test": "json"}'):
|
|
756
|
+
findings = gcp_integration.fetch_findings()
|
|
757
|
+
|
|
758
|
+
# Should filter out the finding without NIST controls
|
|
759
|
+
failed_findings = [f for f in findings if f.status == regscale_models.ControlTestResultStatus.FAIL]
|
|
760
|
+
# Only the finding with NIST controls should be included
|
|
761
|
+
assert len(failed_findings) >= 1
|
|
762
|
+
|
|
763
|
+
def test_fetch_findings_kwargs_handling(self, mocker, test_identifiers):
|
|
764
|
+
"""Test fetch_findings accepts and handles kwargs properly."""
|
|
765
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
766
|
+
|
|
767
|
+
mocker.patch.object(GCPScannerIntegration, "get_failed_findings", return_value=[])
|
|
768
|
+
|
|
769
|
+
# Should not raise error when called with kwargs
|
|
770
|
+
findings = gcp_integration.fetch_findings(some_param="test_value", another_param=123)
|
|
771
|
+
|
|
772
|
+
assert isinstance(findings, list)
|
|
773
|
+
|
|
774
|
+
def test_get_passed_findings_skips_failed_control_tests(self, test_identifiers):
|
|
775
|
+
"""Test get_passed_findings skips control tests already marked as Failed."""
|
|
776
|
+
gcp_integration = self.create_gcp_integration_with_control_tests(test_identifiers)
|
|
777
|
+
|
|
778
|
+
# Mark specific control test as Failed
|
|
779
|
+
gcp_integration.gcp_control_tests["ac-2"]["PUBLIC_BUCKET_ACL"]["status"] = "Failed"
|
|
780
|
+
|
|
781
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
782
|
+
|
|
783
|
+
# Should skip the failed control test
|
|
784
|
+
failed_findings = [
|
|
785
|
+
f for f in passed_findings if f.category == "PUBLIC_BUCKET_ACL" and "ac-2" in f.control_labels
|
|
786
|
+
]
|
|
787
|
+
assert len(failed_findings) == 0
|
|
788
|
+
|
|
789
|
+
def test_get_passed_findings_empty_control_tests(self, mocker, test_identifiers):
|
|
790
|
+
"""Test get_passed_findings handles empty control tests gracefully."""
|
|
791
|
+
# Mock the gcp_control_tests import to return empty dict
|
|
792
|
+
mocker.patch("regscale.integrations.commercial.gcp.__init__.gcp_control_tests", {})
|
|
793
|
+
|
|
794
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
795
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
796
|
+
|
|
797
|
+
assert passed_findings == []
|
|
798
|
+
|
|
799
|
+
@pytest.mark.parametrize(
|
|
800
|
+
"control_label,expected_label",
|
|
801
|
+
[
|
|
802
|
+
("AC-2", "ac-2"),
|
|
803
|
+
("ac-3", "ac-3"),
|
|
804
|
+
("Au-9", "au-9"),
|
|
805
|
+
("SI-4", "si-4"),
|
|
806
|
+
],
|
|
807
|
+
)
|
|
808
|
+
def test_get_passed_findings_control_label_case_handling(
|
|
809
|
+
self, mocker, test_identifiers, control_label, expected_label
|
|
810
|
+
):
|
|
811
|
+
"""Test get_passed_findings handles control label case consistently."""
|
|
812
|
+
# Mock the gcp_control_tests import to use our custom test data
|
|
813
|
+
test_control_tests = {control_label: {"TEST_CATEGORY": {"severity": "HIGH", "description": "Test description"}}}
|
|
814
|
+
mocker.patch("regscale.integrations.commercial.gcp.__init__.gcp_control_tests", test_control_tests)
|
|
815
|
+
|
|
816
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
817
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
818
|
+
|
|
819
|
+
assert len(passed_findings) == 1
|
|
820
|
+
assert passed_findings[0].control_labels == [expected_label]
|
|
821
|
+
|
|
822
|
+
def test_get_passed_findings_different_severity_types(self, mocker, test_identifiers):
|
|
823
|
+
"""Test get_passed_findings handles different severity types correctly."""
|
|
824
|
+
# Create control tests with different severities
|
|
825
|
+
test_control_tests = {
|
|
826
|
+
"ac-2": {
|
|
827
|
+
"HIGH_SEVERITY": {"severity": "HIGH", "description": "High severity test"},
|
|
828
|
+
"LOW_SEVERITY": {"severity": "LOW", "description": "Low severity test"},
|
|
829
|
+
"MEDIUM_SEVERITY": {"severity": "MEDIUM", "description": "Medium severity test"},
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
mocker.patch("regscale.integrations.commercial.gcp.__init__.gcp_control_tests", test_control_tests)
|
|
833
|
+
|
|
834
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
835
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
836
|
+
|
|
837
|
+
assert len(passed_findings) == 3
|
|
838
|
+
categories = [f.category for f in passed_findings]
|
|
839
|
+
assert "HIGH_SEVERITY" in categories
|
|
840
|
+
assert "LOW_SEVERITY" in categories
|
|
841
|
+
assert "MEDIUM_SEVERITY" in categories
|
|
842
|
+
|
|
843
|
+
def test_get_passed_findings_missing_descriptions(self, mocker, test_identifiers):
|
|
844
|
+
"""Test get_passed_findings handles missing descriptions gracefully."""
|
|
845
|
+
# Create control test without description
|
|
846
|
+
test_control_tests = {"ac-2": {"NO_DESCRIPTION": {"severity": "HIGH"}}} # Missing description
|
|
847
|
+
mocker.patch("regscale.integrations.commercial.gcp.__init__.gcp_control_tests", test_control_tests)
|
|
848
|
+
|
|
849
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
850
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
851
|
+
|
|
852
|
+
assert len(passed_findings) == 1
|
|
853
|
+
# The IntegrationFinding class must set a default value when description is empty
|
|
854
|
+
assert passed_findings[0].description in ["", "No description provided"]
|
|
855
|
+
|
|
856
|
+
def test_get_passed_findings_deep_copy_behavior(self, test_identifiers):
|
|
857
|
+
"""Test get_passed_findings doesn't modify the original control tests."""
|
|
858
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
859
|
+
original_control_tests = copy.deepcopy(gcp_control_tests)
|
|
860
|
+
gcp_integration.gcp_control_tests = copy.copy(gcp_control_tests)
|
|
861
|
+
|
|
862
|
+
gcp_integration.get_passed_findings()
|
|
863
|
+
|
|
864
|
+
# Original should be unchanged (deep comparison)
|
|
865
|
+
assert gcp_integration.gcp_control_tests == original_control_tests
|
|
866
|
+
|
|
867
|
+
def test_get_passed_findings_integration_finding_structure(self, mocker, test_identifiers):
|
|
868
|
+
"""Test get_passed_findings creates properly structured IntegrationFindings."""
|
|
869
|
+
test_control_tests = {"ac-2": {"TEST_CAT": {"description": "Test desc"}}}
|
|
870
|
+
mocker.patch("regscale.integrations.commercial.gcp.__init__.gcp_control_tests", test_control_tests)
|
|
871
|
+
|
|
872
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
873
|
+
passed_findings = gcp_integration.get_passed_findings()
|
|
874
|
+
|
|
875
|
+
finding = passed_findings[0]
|
|
876
|
+
assert finding.title == "GCP Scanner Integration Control Assessment"
|
|
877
|
+
assert finding.status == regscale_models.ControlTestResultStatus.PASS
|
|
878
|
+
assert finding.severity == regscale_models.IssueSeverity.Low
|
|
879
|
+
assert finding.impact == regscale_models.IssueSeverity.Low
|
|
880
|
+
assert finding.plugin_name == "TEST_CAT"
|
|
881
|
+
|
|
882
|
+
def test_parse_asset_missing_name_field(self, test_identifiers):
|
|
883
|
+
"""Test parse_asset handles missing name field gracefully."""
|
|
884
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
885
|
+
|
|
886
|
+
mock_asset = Mock()
|
|
887
|
+
mock_asset.name = "" # Empty name
|
|
888
|
+
mock_asset.asset_type = "test.asset.type"
|
|
889
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
890
|
+
|
|
891
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
892
|
+
|
|
893
|
+
assert result.name == ""
|
|
894
|
+
assert result.identifier == ""
|
|
895
|
+
|
|
896
|
+
def test_parse_asset_missing_asset_type(self, test_identifiers):
|
|
897
|
+
"""Test parse_asset handles missing asset_type field gracefully."""
|
|
898
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
899
|
+
|
|
900
|
+
mock_asset = Mock()
|
|
901
|
+
mock_asset.name = "test-asset-name"
|
|
902
|
+
mock_asset.asset_type = "" # Empty asset type
|
|
903
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
904
|
+
|
|
905
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
906
|
+
|
|
907
|
+
assert result.asset_type == ""
|
|
908
|
+
assert result.component_names == [""]
|
|
909
|
+
|
|
910
|
+
def test_parse_asset_missing_update_time(self, test_identifiers):
|
|
911
|
+
"""Test parse_asset handles missing update_time field."""
|
|
912
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
913
|
+
|
|
914
|
+
mock_asset = Mock()
|
|
915
|
+
mock_asset.name = "test-asset-name"
|
|
916
|
+
mock_asset.asset_type = "test.asset.type"
|
|
917
|
+
mock_asset.update_time = None
|
|
918
|
+
|
|
919
|
+
# Should raise AttributeError when trying to format None
|
|
920
|
+
with pytest.raises(AttributeError):
|
|
921
|
+
gcp_integration.parse_asset(mock_asset)
|
|
922
|
+
|
|
923
|
+
def test_parse_asset_invalid_timestamp_format(self, test_identifiers):
|
|
924
|
+
"""Test parse_asset handles invalid timestamp gracefully."""
|
|
925
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
926
|
+
|
|
927
|
+
mock_asset = Mock()
|
|
928
|
+
mock_asset.name = "test-asset-name"
|
|
929
|
+
mock_asset.asset_type = "test.asset.type"
|
|
930
|
+
mock_update_time = Mock()
|
|
931
|
+
mock_update_time.strftime.side_effect = ValueError("Invalid format")
|
|
932
|
+
mock_asset.update_time = mock_update_time
|
|
933
|
+
|
|
934
|
+
# Should raise ValueError for invalid timestamp
|
|
935
|
+
with pytest.raises(ValueError):
|
|
936
|
+
gcp_integration.parse_asset(mock_asset)
|
|
937
|
+
|
|
938
|
+
def test_parse_asset_very_long_names(self, test_identifiers):
|
|
939
|
+
"""Test parse_asset handles very long asset names."""
|
|
940
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
941
|
+
|
|
942
|
+
long_name = "a" * 1000 # Very long name
|
|
943
|
+
mock_asset = Mock()
|
|
944
|
+
mock_asset.name = long_name
|
|
945
|
+
mock_asset.asset_type = "test.asset.type"
|
|
946
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
947
|
+
|
|
948
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
949
|
+
|
|
950
|
+
assert result.name == long_name
|
|
951
|
+
assert result.identifier == long_name
|
|
952
|
+
|
|
953
|
+
def test_parse_asset_special_characters_in_fields(self, test_identifiers):
|
|
954
|
+
"""Test parse_asset handles special characters in asset fields."""
|
|
955
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
956
|
+
|
|
957
|
+
special_name = "//test-asset@#$%^&*()_+{}[]|:;<>?,./"
|
|
958
|
+
mock_asset = Mock()
|
|
959
|
+
mock_asset.name = special_name
|
|
960
|
+
mock_asset.asset_type = "test.asset.type!@#"
|
|
961
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
962
|
+
|
|
963
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
964
|
+
|
|
965
|
+
assert result.name == special_name
|
|
966
|
+
assert result.asset_type == "test.asset.type!@#"
|
|
967
|
+
|
|
968
|
+
def test_parse_asset_different_asset_types(self, test_identifiers):
|
|
969
|
+
"""Test parse_asset handles different GCP asset types."""
|
|
970
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
971
|
+
|
|
972
|
+
asset_types = [
|
|
973
|
+
"compute.googleapis.com/Instance",
|
|
974
|
+
"storage.googleapis.com/Bucket",
|
|
975
|
+
"cloudsql.googleapis.com/Instance",
|
|
976
|
+
"container.googleapis.com/Cluster",
|
|
977
|
+
]
|
|
978
|
+
|
|
979
|
+
for asset_type in asset_types:
|
|
980
|
+
mock_asset = Mock()
|
|
981
|
+
mock_asset.name = f"test-{asset_type.replace('/', '-')}"
|
|
982
|
+
mock_asset.asset_type = asset_type
|
|
983
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
984
|
+
|
|
985
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
986
|
+
|
|
987
|
+
assert result.asset_type == asset_type
|
|
988
|
+
assert result.component_names == [asset_type]
|
|
989
|
+
|
|
990
|
+
def test_parse_asset_component_names_variations(self, test_identifiers):
|
|
991
|
+
"""Test parse_asset component names are based on asset_type."""
|
|
992
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
993
|
+
|
|
994
|
+
mock_asset = Mock()
|
|
995
|
+
mock_asset.name = "test-asset"
|
|
996
|
+
mock_asset.asset_type = "custom.service.type"
|
|
997
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
998
|
+
|
|
999
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
1000
|
+
|
|
1001
|
+
assert result.component_names == ["custom.service.type"]
|
|
1002
|
+
|
|
1003
|
+
def test_parse_asset_minimal_required_fields(self, test_identifiers):
|
|
1004
|
+
"""Test parse_asset with minimal required fields."""
|
|
1005
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1006
|
+
|
|
1007
|
+
mock_asset = Mock()
|
|
1008
|
+
mock_asset.name = "minimal-asset"
|
|
1009
|
+
mock_asset.asset_type = "minimal.type"
|
|
1010
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
1011
|
+
|
|
1012
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
1013
|
+
|
|
1014
|
+
# Verify all required IntegrationAsset fields are present
|
|
1015
|
+
assert result.name == "minimal-asset"
|
|
1016
|
+
assert result.identifier == "minimal-asset"
|
|
1017
|
+
assert result.asset_type == "minimal.type"
|
|
1018
|
+
assert result.asset_owner_id == str(test_identifiers["assessor_id"])
|
|
1019
|
+
assert result.parent_id == test_identifiers["plan_id"]
|
|
1020
|
+
assert result.parent_module == "security_plans"
|
|
1021
|
+
assert result.asset_category == "GCP"
|
|
1022
|
+
assert result.date_last_updated == "2024-01-24 16:16:25"
|
|
1023
|
+
assert result.component_names == ["minimal.type"]
|
|
1024
|
+
assert result.status == "Active (On Network)"
|
|
1025
|
+
|
|
1026
|
+
def test_parse_asset_maximum_field_lengths(self, test_identifiers):
|
|
1027
|
+
"""Test parse_asset with maximum realistic field lengths."""
|
|
1028
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1029
|
+
|
|
1030
|
+
# Google Cloud resource names can be quite long
|
|
1031
|
+
max_name = "//compute.googleapis.com/projects/" + "x" * 100 + "/zones/us-central1-a/instances/" + "y" * 63
|
|
1032
|
+
max_type = "compute.googleapis.com/" + "z" * 50
|
|
1033
|
+
|
|
1034
|
+
mock_asset = Mock()
|
|
1035
|
+
mock_asset.name = max_name
|
|
1036
|
+
mock_asset.asset_type = max_type
|
|
1037
|
+
mock_asset.update_time.strftime.return_value = "2024-01-24 16:16:25"
|
|
1038
|
+
|
|
1039
|
+
result = gcp_integration.parse_asset(mock_asset)
|
|
1040
|
+
|
|
1041
|
+
assert result.name == max_name
|
|
1042
|
+
assert result.asset_type == max_type
|
|
1043
|
+
|
|
1044
|
+
@pytest.mark.parametrize(
|
|
1045
|
+
"scan_type,expected_sources",
|
|
1046
|
+
[
|
|
1047
|
+
("project", "projects/test-project-123"),
|
|
1048
|
+
("organization", "organizations/test-org-12345"),
|
|
1049
|
+
],
|
|
1050
|
+
)
|
|
1051
|
+
def test_fetch_assets_scan_type_handling(self, mocker, test_identifiers, scan_type, expected_sources):
|
|
1052
|
+
"""Test fetch_assets handles different scan types correctly."""
|
|
1053
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpScanType", scan_type)
|
|
1054
|
+
|
|
1055
|
+
mock_client = Mock()
|
|
1056
|
+
mock_assets = [Mock(), Mock()] # Two mock assets
|
|
1057
|
+
mock_client.list_assets.return_value = mock_assets
|
|
1058
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1059
|
+
|
|
1060
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1061
|
+
list(gcp_integration.fetch_assets())
|
|
1062
|
+
|
|
1063
|
+
# Verify correct parent was used
|
|
1064
|
+
mock_client.list_assets.assert_called_once()
|
|
1065
|
+
call_args = mock_client.list_assets.call_args
|
|
1066
|
+
request = call_args[1]["request"]
|
|
1067
|
+
assert request.parent == expected_sources
|
|
1068
|
+
|
|
1069
|
+
def test_fetch_assets_empty_response(self, mocker, test_identifiers):
|
|
1070
|
+
"""Test fetch_assets handles empty asset response."""
|
|
1071
|
+
mock_client = Mock()
|
|
1072
|
+
mock_client.list_assets.return_value = [] # Empty response
|
|
1073
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1074
|
+
|
|
1075
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1076
|
+
assets = list(gcp_integration.fetch_assets())
|
|
1077
|
+
|
|
1078
|
+
assert assets == []
|
|
1079
|
+
assert gcp_integration.num_assets_to_process == 0
|
|
1080
|
+
|
|
1081
|
+
def test_fetch_assets_large_asset_sets(self, mocker, test_identifiers):
|
|
1082
|
+
"""Test fetch_assets handles large numbers of assets."""
|
|
1083
|
+
mock_client = Mock()
|
|
1084
|
+
# Create 1000 mock assets
|
|
1085
|
+
large_asset_set = [Mock() for _ in range(1000)]
|
|
1086
|
+
mock_client.list_assets.return_value = large_asset_set
|
|
1087
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1088
|
+
|
|
1089
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1090
|
+
assets = list(gcp_integration.fetch_assets())
|
|
1091
|
+
|
|
1092
|
+
assert len(assets) == 1000
|
|
1093
|
+
assert gcp_integration.num_assets_to_process == 1000
|
|
1094
|
+
|
|
1095
|
+
def test_fetch_assets_logging_behavior(self, mocker, test_identifiers, caplog):
|
|
1096
|
+
"""Test fetch_assets logging behavior."""
|
|
1097
|
+
mock_client = Mock()
|
|
1098
|
+
mock_client.list_assets.return_value = [Mock()]
|
|
1099
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1100
|
+
|
|
1101
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1102
|
+
|
|
1103
|
+
with caplog.at_level(logging.INFO):
|
|
1104
|
+
list(gcp_integration.fetch_assets())
|
|
1105
|
+
|
|
1106
|
+
assert "Fetching GCP assets..." in caplog.text
|
|
1107
|
+
assert "Fetched GCP assets." in caplog.text
|
|
1108
|
+
|
|
1109
|
+
def test_fetch_assets_authentication_error_handling(self, mocker, test_identifiers):
|
|
1110
|
+
"""Test fetch_assets handles authentication errors."""
|
|
1111
|
+
from google.auth.exceptions import DefaultCredentialsError
|
|
1112
|
+
|
|
1113
|
+
mock_client = Mock()
|
|
1114
|
+
mock_client.list_assets.side_effect = DefaultCredentialsError("No credentials")
|
|
1115
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1116
|
+
|
|
1117
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1118
|
+
|
|
1119
|
+
# Should propagate authentication errors
|
|
1120
|
+
with pytest.raises(DefaultCredentialsError):
|
|
1121
|
+
list(gcp_integration.fetch_assets())
|
|
1122
|
+
|
|
1123
|
+
def test_fetch_assets_api_client_error_handling(self, mocker, test_identifiers):
|
|
1124
|
+
"""Test fetch_assets handles API client errors."""
|
|
1125
|
+
from google.api_core.exceptions import NotFound
|
|
1126
|
+
|
|
1127
|
+
mock_client = Mock()
|
|
1128
|
+
mock_client.list_assets.side_effect = NotFound("Project not found")
|
|
1129
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1130
|
+
|
|
1131
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1132
|
+
|
|
1133
|
+
# Should propagate API errors
|
|
1134
|
+
with pytest.raises(NotFound):
|
|
1135
|
+
list(gcp_integration.fetch_assets())
|
|
1136
|
+
|
|
1137
|
+
def test_fetch_assets_parse_asset_integration(self, mocker, test_identifiers, sample_gcp_assets):
|
|
1138
|
+
"""Test fetch_assets integrates with parse_asset correctly."""
|
|
1139
|
+
mock_client = Mock()
|
|
1140
|
+
mock_client.list_assets.return_value = sample_gcp_assets
|
|
1141
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1142
|
+
|
|
1143
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1144
|
+
assets = list(gcp_integration.fetch_assets())
|
|
1145
|
+
|
|
1146
|
+
# Should return IntegrationAsset objects
|
|
1147
|
+
assert len(assets) == len(sample_gcp_assets)
|
|
1148
|
+
for asset in assets:
|
|
1149
|
+
assert isinstance(asset, IntegrationAsset)
|
|
1150
|
+
|
|
1151
|
+
def test_fetch_assets_num_assets_counting(self, mocker, test_identifiers):
|
|
1152
|
+
"""Test fetch_assets correctly counts processed assets."""
|
|
1153
|
+
mock_client = Mock()
|
|
1154
|
+
test_assets = [Mock() for _ in range(5)]
|
|
1155
|
+
mock_client.list_assets.return_value = test_assets
|
|
1156
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1157
|
+
|
|
1158
|
+
gcp_integration = GCPScannerIntegration(plan_id=test_identifiers["plan_id"])
|
|
1159
|
+
assets = list(gcp_integration.fetch_assets())
|
|
1160
|
+
|
|
1161
|
+
assert gcp_integration.num_assets_to_process == 5
|
|
1162
|
+
assert len(assets) == 5
|
|
1163
|
+
|
|
1164
|
+
def test_sync_assets_error_handling(self, mocker, test_identifiers):
|
|
1165
|
+
"""Test sync_assets handles errors during asset processing."""
|
|
1166
|
+
# Mock parent class sync_assets to raise an exception
|
|
1167
|
+
mock_error = Exception("Processing error")
|
|
1168
|
+
mocker.patch("regscale.integrations.scanner_integration.ScannerIntegration.sync_assets", side_effect=mock_error)
|
|
1169
|
+
|
|
1170
|
+
# Should propagate processing errors
|
|
1171
|
+
with pytest.raises(Exception, match="Processing error"):
|
|
1172
|
+
GCPScannerIntegration.sync_assets(plan_id=test_identifiers["plan_id"])
|
|
1173
|
+
|
|
1174
|
+
def test_sync_assets_empty_assets_response(self, mocker, test_identifiers):
|
|
1175
|
+
"""Test sync_assets when no assets are returned."""
|
|
1176
|
+
# Mock parent class sync_assets to return 0 for empty response
|
|
1177
|
+
mock_parent_sync = mocker.patch(
|
|
1178
|
+
"regscale.integrations.scanner_integration.ScannerIntegration.sync_assets", return_value=0
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
result = GCPScannerIntegration.sync_assets(plan_id=test_identifiers["plan_id"])
|
|
1182
|
+
|
|
1183
|
+
# Should call parent method and return 0 for no assets
|
|
1184
|
+
mock_parent_sync.assert_called_once_with(plan_id=test_identifiers["plan_id"])
|
|
1185
|
+
assert result == 0
|
|
1186
|
+
|
|
1187
|
+
def test_sync_assets_asset_processing_statistics(self, mocker, test_identifiers):
|
|
1188
|
+
"""Test sync_assets processes all returned assets."""
|
|
1189
|
+
# Mock parent class sync_assets to return count of processed assets
|
|
1190
|
+
asset_count = 3
|
|
1191
|
+
mock_parent_sync = mocker.patch(
|
|
1192
|
+
"regscale.integrations.scanner_integration.ScannerIntegration.sync_assets", return_value=asset_count
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
result = GCPScannerIntegration.sync_assets(plan_id=test_identifiers["plan_id"])
|
|
1196
|
+
|
|
1197
|
+
# Should call parent method and return asset count
|
|
1198
|
+
mock_parent_sync.assert_called_once_with(plan_id=test_identifiers["plan_id"])
|
|
1199
|
+
assert result == asset_count
|
|
1200
|
+
|
|
1201
|
+
def test_sync_assets_kwargs_parameter_handling(self, mocker, test_identifiers):
|
|
1202
|
+
"""Test sync_assets accepts and handles kwargs properly."""
|
|
1203
|
+
# Mock parent class sync_assets to accept kwargs
|
|
1204
|
+
mock_parent_sync = mocker.patch(
|
|
1205
|
+
"regscale.integrations.scanner_integration.ScannerIntegration.sync_assets", return_value=0
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
# Should not raise error when called with extra kwargs
|
|
1209
|
+
result = GCPScannerIntegration.sync_assets(
|
|
1210
|
+
plan_id=test_identifiers["plan_id"], extra_param="test", another_param=123
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
# Should call parent method with kwargs
|
|
1214
|
+
mock_parent_sync.assert_called_once_with(
|
|
1215
|
+
plan_id=test_identifiers["plan_id"], extra_param="test", another_param=123
|
|
1216
|
+
)
|
|
1217
|
+
assert result == 0
|
|
1218
|
+
|
|
1219
|
+
def test_sync_assets_integration_with_parent_class(self, mocker, test_identifiers):
|
|
1220
|
+
"""Test sync_assets properly inherits from parent ScannerIntegration."""
|
|
1221
|
+
# Mock the parent class method to return a simple value
|
|
1222
|
+
mock_parent_sync = mocker.patch(
|
|
1223
|
+
"regscale.integrations.scanner_integration.ScannerIntegration.sync_assets", return_value=0
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
# Call the classmethod properly (not instance method)
|
|
1227
|
+
result = GCPScannerIntegration.sync_assets(plan_id=test_identifiers["plan_id"])
|
|
1228
|
+
|
|
1229
|
+
# Should call parent class sync_assets method
|
|
1230
|
+
mock_parent_sync.assert_called_once_with(plan_id=test_identifiers["plan_id"])
|
|
1231
|
+
assert result == 0
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
class TestGCPConfiguration:
|
|
1235
|
+
"""Test suite for GCP configuration and class-level attributes."""
|
|
1236
|
+
|
|
1237
|
+
def test_finding_severity_map_all_mappings(self):
|
|
1238
|
+
"""Test finding_severity_map contains all expected mappings."""
|
|
1239
|
+
expected_mappings = {
|
|
1240
|
+
0: regscale_models.IssueSeverity.Low,
|
|
1241
|
+
1: regscale_models.IssueSeverity.Critical,
|
|
1242
|
+
2: regscale_models.IssueSeverity.High,
|
|
1243
|
+
3: regscale_models.IssueSeverity.Moderate,
|
|
1244
|
+
4: regscale_models.IssueSeverity.Low,
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
assert GCPScannerIntegration.finding_severity_map == expected_mappings
|
|
1248
|
+
|
|
1249
|
+
def test_finding_severity_map_unknown_values(self):
|
|
1250
|
+
"""Test finding_severity_map handles unknown severity values."""
|
|
1251
|
+
# The .get() method should default to Low for unknown values
|
|
1252
|
+
default_severity = GCPScannerIntegration.finding_severity_map.get(999, regscale_models.IssueSeverity.Low)
|
|
1253
|
+
assert default_severity == regscale_models.IssueSeverity.Low
|
|
1254
|
+
|
|
1255
|
+
def test_asset_identifier_field_value(self):
|
|
1256
|
+
"""Test asset_identifier_field is set correctly."""
|
|
1257
|
+
assert GCPScannerIntegration.asset_identifier_field == "googleIdentifier"
|
|
1258
|
+
|
|
1259
|
+
def test_title_property_value(self):
|
|
1260
|
+
"""Test title property value is correct."""
|
|
1261
|
+
assert GCPScannerIntegration.title == "GCP Scanner Integration"
|
|
1262
|
+
|
|
1263
|
+
def test_gcp_control_tests_initialization(self):
|
|
1264
|
+
"""Test gcp_control_tests is properly initialized."""
|
|
1265
|
+
# Should be empty dict by default (gets populated during runtime)
|
|
1266
|
+
assert isinstance(GCPScannerIntegration.gcp_control_tests, dict)
|
|
1267
|
+
|
|
1268
|
+
def test_class_inheritance(self):
|
|
1269
|
+
"""Test GCPScannerIntegration properly inherits from ScannerIntegration."""
|
|
1270
|
+
from regscale.integrations.scanner_integration import ScannerIntegration
|
|
1271
|
+
|
|
1272
|
+
assert issubclass(GCPScannerIntegration, ScannerIntegration)
|
|
1273
|
+
|
|
1274
|
+
# Test that it inherits key methods
|
|
1275
|
+
assert hasattr(GCPScannerIntegration, "sync_findings")
|
|
1276
|
+
assert hasattr(GCPScannerIntegration, "sync_assets")
|
|
1277
|
+
assert hasattr(GCPScannerIntegration, "process_asset")
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
class TestGCPAuthentication:
|
|
1281
|
+
"""Test suite for GCP authentication functions."""
|
|
1282
|
+
|
|
1283
|
+
@pytest.fixture(autouse=True)
|
|
1284
|
+
def setup_auth_test_isolation(self, mocker):
|
|
1285
|
+
"""Ensure each auth test runs in isolation with mocked dependencies."""
|
|
1286
|
+
# Mock GCP variables to avoid external dependencies
|
|
1287
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", "test-project-123")
|
|
1288
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpOrganizationId", "test-org-12345")
|
|
1289
|
+
mocker.patch(
|
|
1290
|
+
"regscale.integrations.commercial.gcp.variables.GcpVariables.gcpCredentials", "test/path/credentials.json"
|
|
1291
|
+
)
|
|
1292
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpScanType", "project")
|
|
1293
|
+
|
|
1294
|
+
def test_ensure_gcp_credentials_sets_environment(self, mocker):
|
|
1295
|
+
"""Test ensure_gcp_credentials sets environment variable when not present."""
|
|
1296
|
+
|
|
1297
|
+
# Mock environment without GOOGLE_APPLICATION_CREDENTIALS
|
|
1298
|
+
mock_environ = {}
|
|
1299
|
+
mocker.patch.dict("os.environ", mock_environ, clear=True)
|
|
1300
|
+
|
|
1301
|
+
# Mock the entire ensure_gcp_credentials function to simulate its behavior
|
|
1302
|
+
def mock_ensure_credentials():
|
|
1303
|
+
if not mock_environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
|
|
1304
|
+
mock_environ["GOOGLE_APPLICATION_CREDENTIALS"] = "test/path/creds.json"
|
|
1305
|
+
|
|
1306
|
+
mocker.patch(
|
|
1307
|
+
"regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials", side_effect=mock_ensure_credentials
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_credentials
|
|
1311
|
+
|
|
1312
|
+
# Verify initial state - env var should not exist
|
|
1313
|
+
assert "GOOGLE_APPLICATION_CREDENTIALS" not in mock_environ
|
|
1314
|
+
|
|
1315
|
+
ensure_gcp_credentials()
|
|
1316
|
+
|
|
1317
|
+
# Now it should be set in our mock environment
|
|
1318
|
+
assert "GOOGLE_APPLICATION_CREDENTIALS" in mock_environ
|
|
1319
|
+
assert mock_environ["GOOGLE_APPLICATION_CREDENTIALS"] == "test/path/creds.json"
|
|
1320
|
+
|
|
1321
|
+
def test_ensure_gcp_credentials_existing_environment(self, mocker):
|
|
1322
|
+
"""Test ensure_gcp_credentials doesn't override existing credentials."""
|
|
1323
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_credentials
|
|
1324
|
+
|
|
1325
|
+
existing_path = "/existing/path/credentials.json"
|
|
1326
|
+
mock_environ = {"GOOGLE_APPLICATION_CREDENTIALS": existing_path}
|
|
1327
|
+
mocker.patch.dict("os.environ", mock_environ, clear=True)
|
|
1328
|
+
|
|
1329
|
+
ensure_gcp_credentials()
|
|
1330
|
+
|
|
1331
|
+
# Should not change existing value
|
|
1332
|
+
assert mock_environ["GOOGLE_APPLICATION_CREDENTIALS"] == existing_path
|
|
1333
|
+
|
|
1334
|
+
def test_ensure_gcp_api_enabled_success(self, mocker):
|
|
1335
|
+
"""Test ensure_gcp_api_enabled with enabled API."""
|
|
1336
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_api_enabled
|
|
1337
|
+
|
|
1338
|
+
# Mock successful API check
|
|
1339
|
+
mock_service = Mock()
|
|
1340
|
+
mock_response = {"state": "ENABLED"}
|
|
1341
|
+
mock_request = Mock()
|
|
1342
|
+
mock_request.execute.return_value = mock_response
|
|
1343
|
+
mock_service.services.return_value.get.return_value = mock_request
|
|
1344
|
+
|
|
1345
|
+
mock_build = mocker.patch("googleapiclient.discovery.build", return_value=mock_service)
|
|
1346
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials")
|
|
1347
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", "test-project")
|
|
1348
|
+
|
|
1349
|
+
# Should not raise exception
|
|
1350
|
+
ensure_gcp_api_enabled("test-service.googleapis.com")
|
|
1351
|
+
|
|
1352
|
+
mock_build.assert_called_once_with("serviceusage", "v1")
|
|
1353
|
+
|
|
1354
|
+
def test_ensure_gcp_api_enabled_api_disabled_exits(self, mocker):
|
|
1355
|
+
"""Test ensure_gcp_api_enabled exits when API is disabled."""
|
|
1356
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_api_enabled
|
|
1357
|
+
|
|
1358
|
+
# Mock disabled API response
|
|
1359
|
+
mock_service = Mock()
|
|
1360
|
+
mock_response = {"state": "DISABLED"}
|
|
1361
|
+
mock_request = Mock()
|
|
1362
|
+
mock_request.execute.return_value = mock_response
|
|
1363
|
+
mock_service.services.return_value.get.return_value = mock_request
|
|
1364
|
+
|
|
1365
|
+
mocker.patch("googleapiclient.discovery.build", return_value=mock_service)
|
|
1366
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials")
|
|
1367
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", "test-project")
|
|
1368
|
+
mock_exit = mocker.patch("regscale.integrations.commercial.gcp.auth.error_and_exit")
|
|
1369
|
+
|
|
1370
|
+
ensure_gcp_api_enabled("test-service.googleapis.com")
|
|
1371
|
+
|
|
1372
|
+
mock_exit.assert_called_once()
|
|
1373
|
+
|
|
1374
|
+
def test_ensure_gcp_api_enabled_authentication_error(self, mocker):
|
|
1375
|
+
"""Test ensure_gcp_api_enabled handles authentication errors."""
|
|
1376
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_api_enabled
|
|
1377
|
+
from google.auth.exceptions import GoogleAuthError
|
|
1378
|
+
|
|
1379
|
+
# Mock authentication error
|
|
1380
|
+
mocker.patch("googleapiclient.discovery.build", side_effect=GoogleAuthError("Auth failed"))
|
|
1381
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials")
|
|
1382
|
+
|
|
1383
|
+
with pytest.raises(RuntimeError, match="Authentication error"):
|
|
1384
|
+
ensure_gcp_api_enabled("test-service.googleapis.com")
|
|
1385
|
+
|
|
1386
|
+
def test_ensure_gcp_api_enabled_general_exception(self, mocker):
|
|
1387
|
+
"""Test ensure_gcp_api_enabled handles general exceptions."""
|
|
1388
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_api_enabled
|
|
1389
|
+
|
|
1390
|
+
# Mock general exception
|
|
1391
|
+
mocker.patch("googleapiclient.discovery.build", side_effect=Exception("General error"))
|
|
1392
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials")
|
|
1393
|
+
|
|
1394
|
+
with pytest.raises(RuntimeError, match="An error occurred"):
|
|
1395
|
+
ensure_gcp_api_enabled("test-service.googleapis.com")
|
|
1396
|
+
|
|
1397
|
+
def test_get_gcp_security_center_client_success(self, mocker):
|
|
1398
|
+
"""Test get_gcp_security_center_client returns client successfully."""
|
|
1399
|
+
from regscale.integrations.commercial.gcp.auth import get_gcp_security_center_client
|
|
1400
|
+
|
|
1401
|
+
# Mock successful client creation
|
|
1402
|
+
mock_client = Mock()
|
|
1403
|
+
mock_security_center = mocker.patch(
|
|
1404
|
+
"google.cloud.securitycenter.SecurityCenterClient", return_value=mock_client
|
|
1405
|
+
)
|
|
1406
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_api_enabled")
|
|
1407
|
+
|
|
1408
|
+
result = get_gcp_security_center_client()
|
|
1409
|
+
|
|
1410
|
+
assert result == mock_client
|
|
1411
|
+
mock_security_center.assert_called_once()
|
|
1412
|
+
|
|
1413
|
+
def test_get_gcp_security_center_client_auth_error(self, mocker):
|
|
1414
|
+
"""Test get_gcp_security_center_client handles authentication errors."""
|
|
1415
|
+
from regscale.integrations.commercial.gcp.auth import get_gcp_security_center_client
|
|
1416
|
+
from google.auth.exceptions import DefaultCredentialsError
|
|
1417
|
+
|
|
1418
|
+
# Mock authentication error during client creation
|
|
1419
|
+
mocker.patch(
|
|
1420
|
+
"google.cloud.securitycenter.SecurityCenterClient", side_effect=DefaultCredentialsError("No creds")
|
|
1421
|
+
)
|
|
1422
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_api_enabled")
|
|
1423
|
+
|
|
1424
|
+
with pytest.raises(DefaultCredentialsError):
|
|
1425
|
+
get_gcp_security_center_client()
|
|
1426
|
+
|
|
1427
|
+
def test_get_gcp_asset_service_client_success(self, mocker):
|
|
1428
|
+
"""Test get_gcp_asset_service_client returns client successfully."""
|
|
1429
|
+
from regscale.integrations.commercial.gcp.auth import get_gcp_asset_service_client
|
|
1430
|
+
|
|
1431
|
+
# Mock successful client creation
|
|
1432
|
+
mock_client = Mock()
|
|
1433
|
+
mock_asset_client = mocker.patch("google.cloud.asset_v1.AssetServiceClient", return_value=mock_client)
|
|
1434
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_api_enabled")
|
|
1435
|
+
|
|
1436
|
+
result = get_gcp_asset_service_client()
|
|
1437
|
+
|
|
1438
|
+
assert result == mock_client
|
|
1439
|
+
mock_asset_client.assert_called_once()
|
|
1440
|
+
|
|
1441
|
+
def test_get_gcp_asset_service_client_auth_error(self, mocker):
|
|
1442
|
+
"""Test get_gcp_asset_service_client handles authentication errors."""
|
|
1443
|
+
from regscale.integrations.commercial.gcp.auth import get_gcp_asset_service_client
|
|
1444
|
+
from google.auth.exceptions import DefaultCredentialsError
|
|
1445
|
+
|
|
1446
|
+
# Mock authentication error during client creation
|
|
1447
|
+
mocker.patch("google.cloud.asset_v1.AssetServiceClient", side_effect=DefaultCredentialsError("No creds"))
|
|
1448
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_api_enabled")
|
|
1449
|
+
|
|
1450
|
+
with pytest.raises(DefaultCredentialsError):
|
|
1451
|
+
get_gcp_asset_service_client()
|
|
1452
|
+
|
|
1453
|
+
def test_ensure_security_center_api_enabled_system_call(self, mocker):
|
|
1454
|
+
"""Test ensure_security_center_api_enabled makes correct system call."""
|
|
1455
|
+
from regscale.integrations.commercial.gcp.auth import ensure_security_center_api_enabled
|
|
1456
|
+
|
|
1457
|
+
mock_system = mocker.patch("os.system")
|
|
1458
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials")
|
|
1459
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", "test-project-123")
|
|
1460
|
+
|
|
1461
|
+
ensure_security_center_api_enabled()
|
|
1462
|
+
|
|
1463
|
+
expected_command = "gcloud services enable securitycenter.googleapis.com --project test-project-123"
|
|
1464
|
+
mock_system.assert_called_once_with(expected_command)
|
|
1465
|
+
|
|
1466
|
+
def test_api_enablement_project_id_usage(self, mocker):
|
|
1467
|
+
"""Test API enablement functions use correct project ID."""
|
|
1468
|
+
from regscale.integrations.commercial.gcp.auth import ensure_gcp_api_enabled
|
|
1469
|
+
|
|
1470
|
+
test_project_id = "custom-test-project-456"
|
|
1471
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", test_project_id)
|
|
1472
|
+
mocker.patch("regscale.integrations.commercial.gcp.auth.ensure_gcp_credentials")
|
|
1473
|
+
|
|
1474
|
+
# Mock service to capture project ID usage
|
|
1475
|
+
mock_service = Mock()
|
|
1476
|
+
mock_response = {"state": "ENABLED"}
|
|
1477
|
+
mock_request = Mock()
|
|
1478
|
+
mock_request.execute.return_value = mock_response
|
|
1479
|
+
mock_service.services.return_value.get.return_value = mock_request
|
|
1480
|
+
mocker.patch("googleapiclient.discovery.build", return_value=mock_service)
|
|
1481
|
+
|
|
1482
|
+
ensure_gcp_api_enabled("test-service.googleapis.com")
|
|
1483
|
+
|
|
1484
|
+
# Verify project ID was used in the API call
|
|
1485
|
+
mock_service.services.return_value.get.assert_called_once_with(
|
|
1486
|
+
name=f"projects/{test_project_id}/services/test-service.googleapis.com"
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
class TestGCPVariables:
|
|
1491
|
+
"""Test suite for GCP variables configuration."""
|
|
1492
|
+
|
|
1493
|
+
@pytest.fixture(autouse=True)
|
|
1494
|
+
def setup_variables_test_isolation(self, mocker):
|
|
1495
|
+
"""Ensure each variables test runs in isolation with mocked dependencies."""
|
|
1496
|
+
# Mock GCP variables to avoid external dependencies
|
|
1497
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpProjectId", "test-project-123")
|
|
1498
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpOrganizationId", "test-org-12345")
|
|
1499
|
+
mocker.patch(
|
|
1500
|
+
"regscale.integrations.commercial.gcp.variables.GcpVariables.gcpCredentials", "test/path/credentials.json"
|
|
1501
|
+
)
|
|
1502
|
+
mocker.patch("regscale.integrations.commercial.gcp.variables.GcpVariables.gcpScanType", "project")
|
|
1503
|
+
|
|
1504
|
+
def test_gcp_variables_class_structure(self):
|
|
1505
|
+
"""Test GcpVariables class has correct structure."""
|
|
1506
|
+
from regscale.integrations.commercial.gcp.variables import GcpVariables
|
|
1507
|
+
|
|
1508
|
+
# Should have class-level attributes
|
|
1509
|
+
assert hasattr(GcpVariables, "gcpProjectId")
|
|
1510
|
+
assert hasattr(GcpVariables, "gcpOrganizationId")
|
|
1511
|
+
assert hasattr(GcpVariables, "gcpScanType")
|
|
1512
|
+
assert hasattr(GcpVariables, "gcpCredentials")
|
|
1513
|
+
|
|
1514
|
+
def test_gcp_variables_required_fields(self):
|
|
1515
|
+
"""Test GcpVariables has all required configuration fields."""
|
|
1516
|
+
from regscale.integrations.commercial.gcp.variables import GcpVariables
|
|
1517
|
+
|
|
1518
|
+
# Verify key variables exist and are accessible
|
|
1519
|
+
required_vars = ["gcpProjectId", "gcpOrganizationId", "gcpScanType", "gcpCredentials"]
|
|
1520
|
+
|
|
1521
|
+
for var_name in required_vars:
|
|
1522
|
+
assert hasattr(GcpVariables, var_name), f"Missing required variable: {var_name}"
|
|
1523
|
+
|
|
1524
|
+
def test_gcp_variables_default_values(self):
|
|
1525
|
+
"""Test GcpVariables provides sensible default/example values."""
|
|
1526
|
+
from regscale.integrations.commercial.gcp.variables import GcpVariables
|
|
1527
|
+
|
|
1528
|
+
# These should be example values, not None
|
|
1529
|
+
# Note: We can't directly access the values due to the metaclass,
|
|
1530
|
+
# but we can verify the attributes exist
|
|
1531
|
+
assert hasattr(GcpVariables, "gcpProjectId")
|
|
1532
|
+
assert hasattr(GcpVariables, "gcpOrganizationId")
|
|
1533
|
+
assert hasattr(GcpVariables, "gcpScanType")
|
|
1534
|
+
assert hasattr(GcpVariables, "gcpCredentials")
|
|
1535
|
+
|
|
1536
|
+
def test_gcp_variables_type_annotations(self):
|
|
1537
|
+
"""Test GcpVariables uses RsVariableType for type annotations."""
|
|
1538
|
+
from regscale.integrations.commercial.gcp.variables import GcpVariables
|
|
1539
|
+
from regscale.core.app.utils.variables import RsVariablesMeta
|
|
1540
|
+
|
|
1541
|
+
# Should use RsVariablesMeta metaclass
|
|
1542
|
+
assert isinstance(GcpVariables, type)
|
|
1543
|
+
assert isinstance(GcpVariables, RsVariablesMeta)
|