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,1517 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Tests for Microsoft Defender integration in RegScale CLI"""
|
|
4
|
+
# standard python imports
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from regscale.integrations.commercial.microsoft_defender.defender import (
|
|
14
|
+
authenticate,
|
|
15
|
+
change_issue_status,
|
|
16
|
+
collect_and_upload_entra_evidence,
|
|
17
|
+
collect_specific_evidence_type,
|
|
18
|
+
compare_defender_and_regscale,
|
|
19
|
+
create_html_table,
|
|
20
|
+
create_payload,
|
|
21
|
+
evaluate_open_issues,
|
|
22
|
+
export_resources,
|
|
23
|
+
fetch_save_and_upload_query,
|
|
24
|
+
format_description,
|
|
25
|
+
get_control_implementations_map,
|
|
26
|
+
get_defender_url,
|
|
27
|
+
get_due_date,
|
|
28
|
+
import_defender_alerts,
|
|
29
|
+
map_365_alert_to_issue,
|
|
30
|
+
map_365_recommendation_to_issue,
|
|
31
|
+
map_cloud_alert_to_issue,
|
|
32
|
+
map_cloud_recommendation_to_issue,
|
|
33
|
+
prep_issues_for_creation,
|
|
34
|
+
process_list_value,
|
|
35
|
+
prompt_user_for_query_selection,
|
|
36
|
+
show_entra_mappings,
|
|
37
|
+
sync_defender_and_regscale,
|
|
38
|
+
upload_evidence_files,
|
|
39
|
+
upload_evidence_to_controls,
|
|
40
|
+
)
|
|
41
|
+
from regscale.integrations.commercial.microsoft_defender.defender_api import DefenderApi
|
|
42
|
+
|
|
43
|
+
from regscale.core.app.application import Application
|
|
44
|
+
from regscale.models import Issue, IssueSeverity
|
|
45
|
+
from regscale.models.integration_models.defender_data import DefenderData
|
|
46
|
+
from tests import CLITestFixture
|
|
47
|
+
|
|
48
|
+
PATH = "regscale.integrations.commercial.microsoft_defender.defender"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.no_parallel
|
|
52
|
+
class TestDefender(CLITestFixture):
|
|
53
|
+
security_plan = None
|
|
54
|
+
|
|
55
|
+
@pytest.fixture(scope="class")
|
|
56
|
+
def create_security_plan(self, request, generate_uuid):
|
|
57
|
+
"""Mock create_security_plan fixture to avoid real API calls in parallel tests"""
|
|
58
|
+
# Create a mock security plan that doesn't require API calls
|
|
59
|
+
mock_security_plan = MagicMock()
|
|
60
|
+
mock_security_plan.id = 12345 # Mock ID
|
|
61
|
+
mock_security_plan.get_module_string.return_value = "security_plans" # Mock module
|
|
62
|
+
yield mock_security_plan
|
|
63
|
+
|
|
64
|
+
@pytest.fixture(autouse=True)
|
|
65
|
+
def setup_ssp(self, create_security_plan):
|
|
66
|
+
self.security_plan = create_security_plan
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def PARENT_ID(self):
|
|
70
|
+
"""Get the parent ID from the existing SSP"""
|
|
71
|
+
return self.security_plan.id
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def PARENT_MODULE(self):
|
|
75
|
+
"""Get the parent module from the existing SSP"""
|
|
76
|
+
return self.security_plan.get_module_string()
|
|
77
|
+
|
|
78
|
+
def test_init(self):
|
|
79
|
+
"""Test init file and config"""
|
|
80
|
+
self.verify_config(
|
|
81
|
+
[
|
|
82
|
+
"azure365TenantId",
|
|
83
|
+
"azure365ClientId",
|
|
84
|
+
"azure365Secret",
|
|
85
|
+
"azureCloudTenantId",
|
|
86
|
+
"azureCloudClientId",
|
|
87
|
+
"azureCloudSecret",
|
|
88
|
+
"azureCloudSubscriptionId",
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
93
|
+
@patch(f"{PATH}.DefenderApi")
|
|
94
|
+
def test_authenticate_365(self, mock_defender_api, mock_check_license):
|
|
95
|
+
"""Test authenticating with Microsoft Defender 365"""
|
|
96
|
+
mock_api_instance = MagicMock()
|
|
97
|
+
mock_defender_api.return_value = mock_api_instance
|
|
98
|
+
|
|
99
|
+
authenticate(system="365")
|
|
100
|
+
|
|
101
|
+
mock_check_license.assert_called_once()
|
|
102
|
+
mock_defender_api.assert_called_once_with(system="365")
|
|
103
|
+
mock_api_instance.get_token.assert_called_once()
|
|
104
|
+
|
|
105
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
106
|
+
@patch(f"{PATH}.DefenderApi")
|
|
107
|
+
def test_authenticate_cloud(self, mock_defender_api, mock_check_license):
|
|
108
|
+
"""Test authenticating with Microsoft Defender for Cloud"""
|
|
109
|
+
mock_api_instance = MagicMock()
|
|
110
|
+
mock_defender_api.return_value = mock_api_instance
|
|
111
|
+
|
|
112
|
+
authenticate(system="cloud")
|
|
113
|
+
|
|
114
|
+
mock_check_license.assert_called_once()
|
|
115
|
+
mock_defender_api.assert_called_once_with(system="cloud")
|
|
116
|
+
mock_api_instance.get_token.assert_called_once()
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize(
|
|
119
|
+
"score,expected_days",
|
|
120
|
+
[
|
|
121
|
+
(9, 7), # high score
|
|
122
|
+
(5, 14), # moderate score
|
|
123
|
+
(2, 30), # low score
|
|
124
|
+
(None, 30), # None score
|
|
125
|
+
("high", 7), # string high
|
|
126
|
+
("medium", 14), # string medium
|
|
127
|
+
("low", 30), # string low
|
|
128
|
+
("unknown", 30), # unknown string
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
def test_get_due_date(self, score, expected_days):
|
|
132
|
+
"""Test getting due date based on severity score"""
|
|
133
|
+
config = {"issues": {"defender365": {"high": 7, "moderate": 14, "low": 30}}}
|
|
134
|
+
|
|
135
|
+
result = get_due_date(score=score, config=config, key="defender365")
|
|
136
|
+
|
|
137
|
+
# Parse the result date and calculate expected date
|
|
138
|
+
today = datetime.now().strftime("%m/%d/%y")
|
|
139
|
+
expected_date = datetime.strptime(today, "%m/%d/%y") + timedelta(days=expected_days)
|
|
140
|
+
expected_date_str = expected_date.strftime("%Y-%m-%dT%H:%M:%S")
|
|
141
|
+
|
|
142
|
+
assert result == expected_date_str
|
|
143
|
+
|
|
144
|
+
def test_format_description(self):
|
|
145
|
+
"""Test formatting description with HTML table"""
|
|
146
|
+
defender_data = {
|
|
147
|
+
"id": "test-id",
|
|
148
|
+
"properties": {"alertUri": "https://example.com/alert", "name": "Test Alert", "severity": "High"},
|
|
149
|
+
}
|
|
150
|
+
tenant_id = "test-tenant-id"
|
|
151
|
+
|
|
152
|
+
result = format_description(defender_data=defender_data, tenant_id=tenant_id)
|
|
153
|
+
|
|
154
|
+
assert '<table style="border: 1px solid;">' in result
|
|
155
|
+
assert "test-id" in result
|
|
156
|
+
assert "Test Alert" in result
|
|
157
|
+
assert "High" in result
|
|
158
|
+
assert "https://example.com/alert" in result
|
|
159
|
+
|
|
160
|
+
def test_get_defender_url_with_alert_uri(self):
|
|
161
|
+
"""Test getting defender URL when alertUri is present"""
|
|
162
|
+
rec = {"properties": {"alertUri": "https://security.microsoft.com/alerts/test-alert"}}
|
|
163
|
+
tenant_id = "test-tenant-id"
|
|
164
|
+
|
|
165
|
+
result = get_defender_url(rec=rec, tenant_id=tenant_id)
|
|
166
|
+
|
|
167
|
+
expected = '<a href="https://security.microsoft.com/alerts/test-alert">https://security.microsoft.com/alerts/test-alert</a>'
|
|
168
|
+
assert result == expected
|
|
169
|
+
|
|
170
|
+
def test_get_defender_url_without_alert_uri(self):
|
|
171
|
+
"""Test getting defender URL when alertUri is not present"""
|
|
172
|
+
rec = {"properties": {}}
|
|
173
|
+
tenant_id = "test-tenant-id"
|
|
174
|
+
|
|
175
|
+
result = get_defender_url(rec=rec, tenant_id=tenant_id)
|
|
176
|
+
|
|
177
|
+
expected_url = f"https://security.microsoft.com/security-recommendations?tid={tenant_id}"
|
|
178
|
+
expected = f'<a href="{expected_url}">{expected_url}</a>'
|
|
179
|
+
assert result == expected
|
|
180
|
+
|
|
181
|
+
def test_create_payload(self):
|
|
182
|
+
"""Test creating payload from defender data"""
|
|
183
|
+
rec = {
|
|
184
|
+
"name": "Test Alert",
|
|
185
|
+
"severity": "High",
|
|
186
|
+
"alertUri": "https://example.com",
|
|
187
|
+
"associatedThreats": ["threat1"], # should be skipped
|
|
188
|
+
"propertiesExtendedPropertiesCustomField": {"customField": "customValue"},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
result = create_payload(rec=rec)
|
|
192
|
+
|
|
193
|
+
assert "Name" in result
|
|
194
|
+
assert "Severity" in result
|
|
195
|
+
assert "Custom Field" in result # uncamel_case applied
|
|
196
|
+
assert "customField" in result["Custom Field"]
|
|
197
|
+
assert "Associated Threats" not in result # should be skipped
|
|
198
|
+
assert "Alert Uri" not in result # should be skipped
|
|
199
|
+
|
|
200
|
+
def test_process_list_value_with_dict_list(self):
|
|
201
|
+
"""Test processing list value with dictionary items"""
|
|
202
|
+
value = [{"key1": "value1", "key2": "value2"}, {"key3": "value3"}]
|
|
203
|
+
|
|
204
|
+
result = process_list_value(value=value)
|
|
205
|
+
|
|
206
|
+
assert "</br>key1: value1</br>key2: value2</br>key3: value3" == result
|
|
207
|
+
|
|
208
|
+
def test_process_list_value_with_nested_list(self):
|
|
209
|
+
"""Test processing list value with nested lists"""
|
|
210
|
+
value = [["item1", "item2"], ["item3", "item4"]]
|
|
211
|
+
|
|
212
|
+
result = process_list_value(value=value)
|
|
213
|
+
|
|
214
|
+
assert "item1</br>item2item3</br>item4" == result
|
|
215
|
+
|
|
216
|
+
def test_process_list_value_with_string_list(self):
|
|
217
|
+
"""Test processing list value with string items"""
|
|
218
|
+
value = ["item1", "item2", "item3"]
|
|
219
|
+
|
|
220
|
+
result = process_list_value(value=value)
|
|
221
|
+
|
|
222
|
+
assert "item1</br>item2</br>item3" == result
|
|
223
|
+
|
|
224
|
+
def test_create_html_table(self):
|
|
225
|
+
"""Test creating HTML table"""
|
|
226
|
+
payload = {
|
|
227
|
+
"name": "Test Alert",
|
|
228
|
+
"severity": "High",
|
|
229
|
+
"created_time": "2023-01-01T12:00:00Z",
|
|
230
|
+
"empty_field": None,
|
|
231
|
+
}
|
|
232
|
+
url = '<a href="https://example.com">https://example.com</a>'
|
|
233
|
+
|
|
234
|
+
result = create_html_table(payload=payload, url=url)
|
|
235
|
+
|
|
236
|
+
assert '<table style="border: 1px solid;">' in result
|
|
237
|
+
assert "Test Alert" in result
|
|
238
|
+
assert "High" in result
|
|
239
|
+
assert "Jan 01, 2023" in result # time formatted
|
|
240
|
+
assert "empty_field" not in result # empty fields excluded
|
|
241
|
+
assert "View in Defender" in result
|
|
242
|
+
assert "</table>" in result
|
|
243
|
+
|
|
244
|
+
def test_compare_defender_and_regscale_new_recommendation(self):
|
|
245
|
+
"""Test comparing when defender has new recommendation"""
|
|
246
|
+
def_data = DefenderData(
|
|
247
|
+
id="test-id", data={"id": "test-id", "name": "Test Rec"}, system="365", object="recommendations"
|
|
248
|
+
)
|
|
249
|
+
issues = []
|
|
250
|
+
|
|
251
|
+
# Mock global variables
|
|
252
|
+
with patch(f"{PATH}.unique_recs", []) as mock_unique_recs, patch(f"{PATH}.job_progress") as mock_job_progress:
|
|
253
|
+
|
|
254
|
+
mock_task = MagicMock()
|
|
255
|
+
args = (MagicMock(), issues, "id", mock_task)
|
|
256
|
+
|
|
257
|
+
compare_defender_and_regscale(def_data=def_data, args=args)
|
|
258
|
+
|
|
259
|
+
assert def_data.analyzed is True
|
|
260
|
+
assert len(mock_unique_recs) == 1
|
|
261
|
+
mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
|
|
262
|
+
|
|
263
|
+
def test_compare_defender_and_regscale_existing_open_issue(self):
|
|
264
|
+
"""Test comparing when issue exists and is open"""
|
|
265
|
+
def_data = DefenderData(
|
|
266
|
+
id="test-id", data={"id": "test-id", "name": "Test Rec"}, system="365", object="recommendations"
|
|
267
|
+
)
|
|
268
|
+
mock_issue = DefenderData(
|
|
269
|
+
id="test-id",
|
|
270
|
+
data={"status": "Open", def_data.integration_field: "test-id"},
|
|
271
|
+
system="365",
|
|
272
|
+
object="recommendations",
|
|
273
|
+
)
|
|
274
|
+
issues = [mock_issue]
|
|
275
|
+
|
|
276
|
+
with patch(f"{PATH}.unique_recs", []) as mock_unique_recs, patch(f"{PATH}.job_progress") as mock_job_progress:
|
|
277
|
+
|
|
278
|
+
mock_task = MagicMock()
|
|
279
|
+
args = (MagicMock(), issues, "id", mock_task)
|
|
280
|
+
|
|
281
|
+
compare_defender_and_regscale(def_data=def_data, args=args)
|
|
282
|
+
|
|
283
|
+
assert def_data.analyzed is True
|
|
284
|
+
assert len(mock_unique_recs) == 0 # Should be empty as it's a duplicate
|
|
285
|
+
mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
|
|
286
|
+
|
|
287
|
+
@patch(f"{PATH}.change_issue_status")
|
|
288
|
+
def test_compare_defender_and_regscale_closed_issue_reopen(self, mock_change_status):
|
|
289
|
+
"""Test comparing when issue exists but is closed - should reopen"""
|
|
290
|
+
def_data = DefenderData(
|
|
291
|
+
id="test-id", data={"id": "test-id", "name": "Test Rec"}, system="365", object="recommendations"
|
|
292
|
+
)
|
|
293
|
+
mock_issue = DefenderData(
|
|
294
|
+
id="test-id",
|
|
295
|
+
data={"status": "Closed", def_data.integration_field: "test-id"},
|
|
296
|
+
system="365",
|
|
297
|
+
object="recommendations",
|
|
298
|
+
)
|
|
299
|
+
issues = [mock_issue]
|
|
300
|
+
api = MagicMock()
|
|
301
|
+
api.config = {"issues": {mock_issue.init_key: {"status": "Open"}}}
|
|
302
|
+
|
|
303
|
+
with patch(f"{PATH}.unique_recs", []), patch(f"{PATH}.job_progress"):
|
|
304
|
+
|
|
305
|
+
mock_task = MagicMock()
|
|
306
|
+
args = (api, issues, "id", mock_task)
|
|
307
|
+
|
|
308
|
+
compare_defender_and_regscale(def_data=def_data, args=args)
|
|
309
|
+
|
|
310
|
+
mock_change_status.assert_called_once_with(
|
|
311
|
+
api=api, status="Open", issue=mock_issue.data, rec=def_data, rec_type=mock_issue.init_key
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@patch(f"{PATH}.change_issue_status")
|
|
315
|
+
def test_evaluate_open_issues_close_outdated(self, mock_change_status):
|
|
316
|
+
"""Test evaluating open issues - close when no longer in defender"""
|
|
317
|
+
issue = DefenderData(
|
|
318
|
+
id="outdated-id",
|
|
319
|
+
data={"status": "Open", "defender365Id": "outdated-id"},
|
|
320
|
+
system="365",
|
|
321
|
+
object="recommendations",
|
|
322
|
+
)
|
|
323
|
+
defender_data = [] # Empty list means issue not in current recommendations
|
|
324
|
+
|
|
325
|
+
with patch(f"{PATH}.job_progress") as mock_job_progress:
|
|
326
|
+
mock_task = MagicMock()
|
|
327
|
+
args = (MagicMock(), defender_data, mock_task)
|
|
328
|
+
|
|
329
|
+
evaluate_open_issues(issue=issue, args=args)
|
|
330
|
+
|
|
331
|
+
assert issue.analyzed is True
|
|
332
|
+
mock_change_status.assert_called_once_with(
|
|
333
|
+
api=args[0], status="Closed", issue=issue.data, rec=None, rec_type=issue.init_key
|
|
334
|
+
)
|
|
335
|
+
mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
|
|
336
|
+
|
|
337
|
+
def test_evaluate_open_issues_skip_closed(self):
|
|
338
|
+
"""Test evaluating issues that are already closed"""
|
|
339
|
+
issue = DefenderData(
|
|
340
|
+
id="closed-id",
|
|
341
|
+
data={"status": "Closed", "defender365Id": "closed-id"},
|
|
342
|
+
system="365",
|
|
343
|
+
object="recommendations",
|
|
344
|
+
)
|
|
345
|
+
defender_data = []
|
|
346
|
+
|
|
347
|
+
with patch(f"{PATH}.job_progress"), patch(f"{PATH}.change_issue_status") as mock_change_status:
|
|
348
|
+
|
|
349
|
+
mock_task = MagicMock()
|
|
350
|
+
args = (MagicMock(), defender_data, mock_task)
|
|
351
|
+
|
|
352
|
+
evaluate_open_issues(issue=issue, args=args)
|
|
353
|
+
|
|
354
|
+
# Should not call change_issue_status for already closed issues
|
|
355
|
+
mock_change_status.assert_not_called()
|
|
356
|
+
|
|
357
|
+
@patch(f"{PATH}.Issue")
|
|
358
|
+
def test_change_issue_status_close_365(self, mock_issue_class):
|
|
359
|
+
"""Test changing issue status to closed for Defender 365"""
|
|
360
|
+
api = MagicMock()
|
|
361
|
+
api.config = {**self.config, **{"userId": "test-user"}}
|
|
362
|
+
|
|
363
|
+
issue = {"id": 1, "status": "Open"}
|
|
364
|
+
rec = DefenderData(
|
|
365
|
+
id="test-id",
|
|
366
|
+
data={"recommendationName": "Test Recommendation", "severityScore": 8},
|
|
367
|
+
system="365",
|
|
368
|
+
object="recommendations",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
mock_issue_instance = MagicMock()
|
|
372
|
+
mock_issue_class.return_value = mock_issue_instance
|
|
373
|
+
|
|
374
|
+
with patch(f"{PATH}.get_current_datetime") as mock_datetime, patch(
|
|
375
|
+
f"{PATH}.format_description"
|
|
376
|
+
) as mock_format_desc, patch(f"{PATH}.closed", []) as mock_closed:
|
|
377
|
+
|
|
378
|
+
mock_datetime.return_value = "2023-01-01 12:00:00"
|
|
379
|
+
mock_format_desc.return_value = "Test description"
|
|
380
|
+
|
|
381
|
+
change_issue_status(api=api, status="Closed", issue=issue, rec=rec, rec_type="defender365")
|
|
382
|
+
|
|
383
|
+
assert issue["status"] == "Closed"
|
|
384
|
+
assert issue["lastUpdatedById"] == "test-user"
|
|
385
|
+
assert "No longer reported via Microsoft 365 Defender" in issue["description"]
|
|
386
|
+
assert len(mock_closed) == 1
|
|
387
|
+
mock_issue_instance.save.assert_called_once()
|
|
388
|
+
|
|
389
|
+
@patch(f"{PATH}.Issue")
|
|
390
|
+
def test_change_issue_status_reopen(self, mock_issue_class):
|
|
391
|
+
"""Test changing issue status to reopen"""
|
|
392
|
+
api = MagicMock()
|
|
393
|
+
api.config = {**self.config, **{"userId": "test-user"}}
|
|
394
|
+
|
|
395
|
+
issue = {"id": 1, "status": "Closed"}
|
|
396
|
+
rec = DefenderData(
|
|
397
|
+
id="test-id",
|
|
398
|
+
data={"recommendationName": "Test Recommendation", "severityScore": 8},
|
|
399
|
+
system="365",
|
|
400
|
+
object="recommendations",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
mock_issue_instance = MagicMock()
|
|
404
|
+
mock_issue_class.return_value = mock_issue_instance
|
|
405
|
+
|
|
406
|
+
with patch(f"{PATH}.get_current_datetime") as mock_datetime, patch(
|
|
407
|
+
f"{PATH}.format_description"
|
|
408
|
+
) as mock_format_desc, patch(f"{PATH}.updated", []) as mock_updated:
|
|
409
|
+
|
|
410
|
+
mock_datetime.return_value = "2023-01-01 12:00:00"
|
|
411
|
+
mock_format_desc.return_value = "Test description"
|
|
412
|
+
|
|
413
|
+
change_issue_status(api=api, status="Open", issue=issue, rec=rec, rec_type="defender365")
|
|
414
|
+
|
|
415
|
+
assert issue["status"] == "Open"
|
|
416
|
+
assert issue["dateCompleted"] == ""
|
|
417
|
+
assert len(mock_updated) == 1
|
|
418
|
+
mock_issue_instance.save.assert_called_once()
|
|
419
|
+
|
|
420
|
+
def test_change_issue_status_no_rec(self):
|
|
421
|
+
"""Test changing issue status with no recommendation data"""
|
|
422
|
+
api = MagicMock()
|
|
423
|
+
api.config = {"userId": "test-user"}
|
|
424
|
+
|
|
425
|
+
issue = {"id": 1, "status": "Open"}
|
|
426
|
+
|
|
427
|
+
with patch(f"{PATH}.get_current_datetime") as mock_datetime:
|
|
428
|
+
mock_datetime.return_value = "2023-01-01 12:00:00"
|
|
429
|
+
|
|
430
|
+
result = change_issue_status(api=api, status="Closed", issue=issue, rec=None, rec_type="defender365")
|
|
431
|
+
|
|
432
|
+
# Should return early when rec is None
|
|
433
|
+
assert result is None
|
|
434
|
+
assert issue["lastUpdatedById"] == "test-user"
|
|
435
|
+
assert issue["status"] == "Closed"
|
|
436
|
+
|
|
437
|
+
@patch(f"{PATH}.issues_to_create")
|
|
438
|
+
def test_prep_issues_for_creation(self, mock_issues_to_create):
|
|
439
|
+
"""Test preparing issues for creation"""
|
|
440
|
+
mock_issues_to_create.return_value = []
|
|
441
|
+
def_data = DefenderData(
|
|
442
|
+
id="test-id", data={"id": "test-id", "name": "Test Alert"}, system="365", object="alerts"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
mapping_func = MagicMock()
|
|
446
|
+
mock_issue = MagicMock()
|
|
447
|
+
mapping_func.return_value = mock_issue
|
|
448
|
+
|
|
449
|
+
with patch(f"{PATH}.job_progress") as mock_job_progress, patch(
|
|
450
|
+
f"{PATH}.format_description"
|
|
451
|
+
) as mock_format_desc:
|
|
452
|
+
|
|
453
|
+
mock_format_desc.return_value = "Test description"
|
|
454
|
+
mock_task = MagicMock()
|
|
455
|
+
args = (mapping_func, self.config, "id", 1, "issues", mock_task)
|
|
456
|
+
|
|
457
|
+
prep_issues_for_creation(def_data=def_data, args=args)
|
|
458
|
+
|
|
459
|
+
assert def_data.created is True
|
|
460
|
+
mapping_func.assert_called_once_with(data=def_data, config=self.config, description="Test description")
|
|
461
|
+
assert mock_issue.parentId == 1
|
|
462
|
+
assert mock_issue.parentModule == "issues"
|
|
463
|
+
mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
|
|
464
|
+
|
|
465
|
+
def test_map_365_alert_to_issue(self):
|
|
466
|
+
"""Test mapping 365 alert to RegScale issue"""
|
|
467
|
+
data = DefenderData(
|
|
468
|
+
id="test-id",
|
|
469
|
+
data={
|
|
470
|
+
"title": "Test Alert",
|
|
471
|
+
"severity": "High",
|
|
472
|
+
"machineId": "machine-123",
|
|
473
|
+
"computerDnsName": "test.example.com",
|
|
474
|
+
},
|
|
475
|
+
system="365",
|
|
476
|
+
object="alerts",
|
|
477
|
+
)
|
|
478
|
+
description = "Test alert description"
|
|
479
|
+
|
|
480
|
+
result = map_365_alert_to_issue(data=data, config=self.config, description=description)
|
|
481
|
+
|
|
482
|
+
assert isinstance(result, Issue)
|
|
483
|
+
assert result.title == "Test Alert"
|
|
484
|
+
assert result.description == description
|
|
485
|
+
assert result.severityLevel == IssueSeverity.High
|
|
486
|
+
assert result.status == "Open"
|
|
487
|
+
assert "Machine ID:machine-123" in result.assetIdentifier
|
|
488
|
+
assert "test.example.com" in result.assetIdentifier
|
|
489
|
+
assert result.sourceReport == "Microsoft Defender 365 Alert"
|
|
490
|
+
|
|
491
|
+
def test_map_365_recommendation_to_issue(self):
|
|
492
|
+
"""Test mapping 365 recommendation to RegScale issue"""
|
|
493
|
+
data = DefenderData(
|
|
494
|
+
id="test-id",
|
|
495
|
+
data={"recommendationName": "Test Recommendation", "severityScore": 8, "vendor": "Microsoft"},
|
|
496
|
+
system="365",
|
|
497
|
+
object="recommendations",
|
|
498
|
+
)
|
|
499
|
+
description = "Test recommendation description"
|
|
500
|
+
|
|
501
|
+
result = map_365_recommendation_to_issue(data=data, config=self.config, description=description)
|
|
502
|
+
|
|
503
|
+
assert isinstance(result, Issue)
|
|
504
|
+
assert result.title == "Test Recommendation"
|
|
505
|
+
assert result.description == description
|
|
506
|
+
assert result.vendorName == "Microsoft"
|
|
507
|
+
assert result.sourceReport == "Microsoft Defender 365 Recommendation"
|
|
508
|
+
|
|
509
|
+
def test_map_cloud_alert_to_issue(self):
|
|
510
|
+
"""Test mapping cloud alert to RegScale issue"""
|
|
511
|
+
data = DefenderData(
|
|
512
|
+
id="test-id",
|
|
513
|
+
data={
|
|
514
|
+
"id": "alert-id-123",
|
|
515
|
+
"properties": {
|
|
516
|
+
"productName": "Microsoft Defender",
|
|
517
|
+
"compromisedEntity": "test-server",
|
|
518
|
+
"severity": "High",
|
|
519
|
+
"vendorName": "Microsoft",
|
|
520
|
+
"resourceIdentifiers": [
|
|
521
|
+
{"azureResourceId": "/subscriptions/test/resource1"},
|
|
522
|
+
{"azureResourceId": "/subscriptions/test/resource2"},
|
|
523
|
+
],
|
|
524
|
+
"remediationSteps": ["Step 1", "Step 2"],
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
system="cloud",
|
|
528
|
+
object="alerts",
|
|
529
|
+
)
|
|
530
|
+
description = "Test alert description"
|
|
531
|
+
|
|
532
|
+
result = map_cloud_alert_to_issue(data=data, config=self.config, description=description)
|
|
533
|
+
|
|
534
|
+
assert isinstance(result, Issue)
|
|
535
|
+
assert result.title == "Microsoft Defender Alert - test-server"
|
|
536
|
+
assert result.description == description
|
|
537
|
+
assert result.vendorName == "Microsoft"
|
|
538
|
+
assert "/subscriptions/test/resource1" in result.assetIdentifier
|
|
539
|
+
assert "Step 1" in result.recommendedActions
|
|
540
|
+
assert result.otherIdentifier == "alert-id-123"
|
|
541
|
+
assert result.sourceReport == "Microsoft Defender for Cloud Alert"
|
|
542
|
+
|
|
543
|
+
def test_map_cloud_recommendation_to_issue(self):
|
|
544
|
+
"""Test mapping cloud recommendation to RegScale issue"""
|
|
545
|
+
data = DefenderData(
|
|
546
|
+
id="test-id",
|
|
547
|
+
data={
|
|
548
|
+
"id": "rec-id-123",
|
|
549
|
+
"properties": {
|
|
550
|
+
"metadata": {
|
|
551
|
+
"displayName": "Test Recommendation",
|
|
552
|
+
"severity": "Medium",
|
|
553
|
+
"remediationDescription": "Fix this issue",
|
|
554
|
+
},
|
|
555
|
+
"resourceDetails": {
|
|
556
|
+
"ResourceProvider": "Microsoft.Compute",
|
|
557
|
+
"ResourceType": "virtualMachines",
|
|
558
|
+
"ResourceName": "test-vm",
|
|
559
|
+
"Id": "/subscriptions/test/vm1",
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
system="cloud",
|
|
564
|
+
object="recommendations",
|
|
565
|
+
)
|
|
566
|
+
description = "Test recommendation description"
|
|
567
|
+
|
|
568
|
+
result = map_cloud_recommendation_to_issue(data=data, config=self.config, description=description)
|
|
569
|
+
|
|
570
|
+
assert isinstance(result, Issue)
|
|
571
|
+
assert result.title == "Test Recommendation on Microsoft.Compute/virtualMachines/test-vm"
|
|
572
|
+
assert result.description == description
|
|
573
|
+
assert result.recommendedActions == "Fix this issue"
|
|
574
|
+
assert result.assetIdentifier == "/subscriptions/test/vm1"
|
|
575
|
+
assert result.otherIdentifier == "rec-id-123"
|
|
576
|
+
assert result.manualDetectionId == data.id
|
|
577
|
+
assert "Microsoft Defender for Cloud Recommendation" in result.sourceReport
|
|
578
|
+
|
|
579
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
580
|
+
@patch(f"{PATH}.save_data_to")
|
|
581
|
+
def test_fetch_save_and_upload_query(self, mock_save_data, mock_upload_file):
|
|
582
|
+
"""Test fetching, saving and uploading query results"""
|
|
583
|
+
mock_defender_api = MagicMock(spec=DefenderApi)
|
|
584
|
+
mock_defender_api.fetch_and_run_query.return_value = [{"data": "test"}]
|
|
585
|
+
mock_defender_api.api = MagicMock()
|
|
586
|
+
|
|
587
|
+
query = {"name": "test-query"}
|
|
588
|
+
parent_id = 1
|
|
589
|
+
parent_module = "issues"
|
|
590
|
+
no_upload = False
|
|
591
|
+
|
|
592
|
+
mock_upload_file.return_value = True
|
|
593
|
+
|
|
594
|
+
with patch(f"{PATH}.get_current_datetime") as mock_datetime:
|
|
595
|
+
mock_datetime.return_value = "20230101"
|
|
596
|
+
|
|
597
|
+
fetch_save_and_upload_query(
|
|
598
|
+
defender_api=mock_defender_api,
|
|
599
|
+
query=query,
|
|
600
|
+
parent_id=parent_id,
|
|
601
|
+
parent_module=parent_module,
|
|
602
|
+
no_upload=no_upload,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
mock_defender_api.fetch_and_run_query.assert_called_once_with(query=query)
|
|
606
|
+
mock_save_data.assert_called_once()
|
|
607
|
+
mock_upload_file.assert_called_once()
|
|
608
|
+
|
|
609
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
610
|
+
@patch(f"{PATH}.save_data_to")
|
|
611
|
+
def test_fetch_save_and_upload_query_no_upload(self, mock_save_data, mock_upload_file):
|
|
612
|
+
"""Test fetching and saving query results without uploading"""
|
|
613
|
+
mock_defender_api = MagicMock(spec=DefenderApi)
|
|
614
|
+
mock_defender_api.fetch_and_run_query.return_value = [{"data": "test"}]
|
|
615
|
+
|
|
616
|
+
query = {"name": "test-query"}
|
|
617
|
+
parent_id = 1
|
|
618
|
+
parent_module = "issues"
|
|
619
|
+
no_upload = True
|
|
620
|
+
|
|
621
|
+
with patch(f"{PATH}.get_current_datetime") as mock_datetime:
|
|
622
|
+
mock_datetime.return_value = "20230101"
|
|
623
|
+
|
|
624
|
+
fetch_save_and_upload_query(
|
|
625
|
+
defender_api=mock_defender_api,
|
|
626
|
+
query=query,
|
|
627
|
+
parent_id=parent_id,
|
|
628
|
+
parent_module=parent_module,
|
|
629
|
+
no_upload=no_upload,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
mock_defender_api.fetch_and_run_query.assert_called_once_with(query=query)
|
|
633
|
+
mock_save_data.assert_called_once()
|
|
634
|
+
mock_upload_file.assert_not_called()
|
|
635
|
+
|
|
636
|
+
def test_prompt_user_for_query_selection_by_name(self):
|
|
637
|
+
"""Test prompting user for query selection when name matches"""
|
|
638
|
+
queries = [{"name": "Query1", "id": "1"}, {"name": "Query2", "id": "2"}, {"name": "Query3", "id": "3"}]
|
|
639
|
+
query_name = "query2" # Case insensitive
|
|
640
|
+
|
|
641
|
+
result = prompt_user_for_query_selection(queries=queries, query_name=query_name)
|
|
642
|
+
|
|
643
|
+
assert result["name"] == "Query2"
|
|
644
|
+
assert result["id"] == "2"
|
|
645
|
+
|
|
646
|
+
@patch(f"{PATH}.click.prompt")
|
|
647
|
+
def test_prompt_user_for_query_selection_interactive(self, mock_click_prompt):
|
|
648
|
+
"""Test prompting user for query selection interactively"""
|
|
649
|
+
queries = [{"name": "Query1", "id": "1"}, {"name": "Query2", "id": "2"}]
|
|
650
|
+
mock_click_prompt.return_value = "Query1"
|
|
651
|
+
|
|
652
|
+
result = prompt_user_for_query_selection(queries=queries)
|
|
653
|
+
|
|
654
|
+
assert result["name"] == "Query1"
|
|
655
|
+
assert result["id"] == "1"
|
|
656
|
+
mock_click_prompt.assert_called_once()
|
|
657
|
+
|
|
658
|
+
@patch(f"{PATH}.FlatFileImporter.import_files")
|
|
659
|
+
def test_import_defender_alerts(self, mock_import_files):
|
|
660
|
+
"""Test importing defender alerts from CSV"""
|
|
661
|
+
from regscale.models.integration_models.defenderimport import DefenderImport
|
|
662
|
+
|
|
663
|
+
folder_path = "/path/to/files"
|
|
664
|
+
regscale_ssp_id = 1
|
|
665
|
+
scan_date = datetime(2023, 1, 1)
|
|
666
|
+
mappings_path = Path("/path/to/mappings")
|
|
667
|
+
disable_mapping = False
|
|
668
|
+
s3_bucket = "test-bucket"
|
|
669
|
+
s3_prefix = "test-prefix"
|
|
670
|
+
aws_profile = "test-profile"
|
|
671
|
+
upload_file = True
|
|
672
|
+
|
|
673
|
+
import_defender_alerts(
|
|
674
|
+
folder_path=folder_path,
|
|
675
|
+
regscale_ssp_id=regscale_ssp_id,
|
|
676
|
+
scan_date=scan_date,
|
|
677
|
+
mappings_path=mappings_path,
|
|
678
|
+
disable_mapping=disable_mapping,
|
|
679
|
+
s3_bucket=s3_bucket,
|
|
680
|
+
s3_prefix=s3_prefix,
|
|
681
|
+
aws_profile=aws_profile,
|
|
682
|
+
upload_file=upload_file,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
mock_import_files.assert_called_once_with(
|
|
686
|
+
import_type=DefenderImport,
|
|
687
|
+
import_name="Defender",
|
|
688
|
+
file_types=".csv",
|
|
689
|
+
folder_path=folder_path,
|
|
690
|
+
object_id=regscale_ssp_id,
|
|
691
|
+
scan_date=scan_date,
|
|
692
|
+
mappings_path=mappings_path,
|
|
693
|
+
disable_mapping=disable_mapping,
|
|
694
|
+
s3_bucket=s3_bucket,
|
|
695
|
+
s3_prefix=s3_prefix,
|
|
696
|
+
aws_profile=aws_profile,
|
|
697
|
+
upload_file=upload_file,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
@patch(f"{PATH}.fetch_save_and_upload_query")
|
|
701
|
+
@patch(f"{PATH}.DefenderApi")
|
|
702
|
+
@patch(f"{PATH}.is_valid")
|
|
703
|
+
@patch(f"{PATH}.check_license")
|
|
704
|
+
def test_export_resources_all_queries(self, mock_check_license, mock_is_valid, mock_defender_api, mock_fetch_save):
|
|
705
|
+
"""Test exporting all queries from Defender for Cloud"""
|
|
706
|
+
mock_app = MagicMock(spec=Application)
|
|
707
|
+
mock_check_license.return_value = mock_app
|
|
708
|
+
mock_is_valid.return_value = True
|
|
709
|
+
|
|
710
|
+
mock_api_instance = MagicMock()
|
|
711
|
+
mock_defender_api.return_value = mock_api_instance
|
|
712
|
+
mock_api_instance.fetch_queries_from_azure.return_value = [{"name": "Query1"}, {"name": "Query2"}]
|
|
713
|
+
|
|
714
|
+
export_resources(
|
|
715
|
+
parent_id=1, parent_module="issues", query_name="Test Query", no_upload=False, all_queries=True
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
mock_defender_api.assert_called_once_with(system="cloud")
|
|
719
|
+
mock_api_instance.fetch_queries_from_azure.assert_called_once()
|
|
720
|
+
assert mock_fetch_save.call_count == 2
|
|
721
|
+
|
|
722
|
+
@patch(f"{PATH}.fetch_save_and_upload_query")
|
|
723
|
+
@patch(f"{PATH}.prompt_user_for_query_selection")
|
|
724
|
+
@patch(f"{PATH}.DefenderApi")
|
|
725
|
+
@patch(f"{PATH}.is_valid")
|
|
726
|
+
@patch(f"{PATH}.check_license")
|
|
727
|
+
def test_export_resources_single_query(
|
|
728
|
+
self, mock_check_license, mock_is_valid, mock_defender_api, mock_prompt, mock_fetch_save
|
|
729
|
+
):
|
|
730
|
+
"""Test exporting single query from Defender for Cloud"""
|
|
731
|
+
mock_app = MagicMock(spec=Application)
|
|
732
|
+
mock_check_license.return_value = mock_app
|
|
733
|
+
mock_is_valid.return_value = True
|
|
734
|
+
|
|
735
|
+
mock_api_instance = MagicMock()
|
|
736
|
+
mock_defender_api.return_value = mock_api_instance
|
|
737
|
+
mock_api_instance.fetch_queries_from_azure.return_value = [{"name": "Query1"}]
|
|
738
|
+
|
|
739
|
+
mock_prompt.return_value = {"name": "Query1"}
|
|
740
|
+
|
|
741
|
+
export_resources(parent_id=1, parent_module="issues", query_name="Query1", no_upload=False, all_queries=False)
|
|
742
|
+
|
|
743
|
+
mock_prompt.assert_called_once_with(queries=[{"name": "Query1"}], query_name="Query1")
|
|
744
|
+
mock_fetch_save.assert_called_once()
|
|
745
|
+
|
|
746
|
+
@patch(f"{PATH}.logger")
|
|
747
|
+
@patch(f"{PATH}.DefenderApi")
|
|
748
|
+
@patch(f"{PATH}.is_valid")
|
|
749
|
+
@patch(f"{PATH}.check_license")
|
|
750
|
+
def test_export_resources_no_queries(self, mock_check_license, mock_is_valid, mock_defender_api, mock_logger):
|
|
751
|
+
"""Test exporting when no queries exist"""
|
|
752
|
+
mock_app = MagicMock(spec=Application)
|
|
753
|
+
mock_check_license.return_value = mock_app
|
|
754
|
+
mock_is_valid.return_value = True
|
|
755
|
+
|
|
756
|
+
mock_api_instance = MagicMock()
|
|
757
|
+
mock_defender_api.return_value = mock_api_instance
|
|
758
|
+
mock_api_instance.fetch_queries_from_azure.return_value = []
|
|
759
|
+
|
|
760
|
+
export_resources(parent_id=1, parent_module="issues", query_name="None", no_upload=False, all_queries=True)
|
|
761
|
+
|
|
762
|
+
mock_logger.warning.assert_called_once_with(
|
|
763
|
+
"No saved queries found in Azure. Please create at least one query to use this export function."
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
@patch(f"{PATH}.error_and_exit")
|
|
767
|
+
@patch(f"{PATH}.is_valid")
|
|
768
|
+
@patch(f"{PATH}.check_license")
|
|
769
|
+
def test_export_resources_invalid_login(self, mock_check_license, mock_is_valid, mock_error_exit):
|
|
770
|
+
"""Test exporting with invalid RegScale login"""
|
|
771
|
+
mock_app = MagicMock(spec=Application)
|
|
772
|
+
mock_check_license.return_value = mock_app
|
|
773
|
+
mock_is_valid.return_value = False
|
|
774
|
+
|
|
775
|
+
export_resources(
|
|
776
|
+
parent_id=1, parent_module="issues", query_name="Invalid Login", no_upload=False, all_queries=True
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
mock_error_exit.assert_called_once_with("Login Invalid RegScale Credentials, please login for a new token.")
|
|
780
|
+
|
|
781
|
+
# ==============================
|
|
782
|
+
# NEW TESTS FOR ENTRA FUNCTIONALITY
|
|
783
|
+
# ==============================
|
|
784
|
+
|
|
785
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
786
|
+
@patch(f"{PATH}.DefenderApi")
|
|
787
|
+
def test_authenticate_entra(self, mock_defender_api, mock_check_license):
|
|
788
|
+
"""Test authenticating with Azure Entra"""
|
|
789
|
+
mock_api_instance = MagicMock()
|
|
790
|
+
mock_defender_api.return_value = mock_api_instance
|
|
791
|
+
|
|
792
|
+
authenticate(system="entra")
|
|
793
|
+
|
|
794
|
+
mock_check_license.assert_called_once()
|
|
795
|
+
mock_defender_api.assert_called_once_with(system="entra")
|
|
796
|
+
mock_api_instance.get_token.assert_called_once()
|
|
797
|
+
|
|
798
|
+
@patch(f"{PATH}.upload_evidence_files")
|
|
799
|
+
@patch(f"{PATH}.collect_specific_evidence_type")
|
|
800
|
+
@patch(f"{PATH}.DefenderApi")
|
|
801
|
+
@patch(f"{PATH}.is_valid")
|
|
802
|
+
@patch(f"{PATH}.Api")
|
|
803
|
+
@patch(f"{PATH}.check_license")
|
|
804
|
+
def test_collect_and_upload_entra_evidence_all(
|
|
805
|
+
self,
|
|
806
|
+
mock_check_license,
|
|
807
|
+
mock_api_class,
|
|
808
|
+
mock_is_valid,
|
|
809
|
+
mock_defender_api,
|
|
810
|
+
mock_collect_specific,
|
|
811
|
+
mock_upload_evidence,
|
|
812
|
+
):
|
|
813
|
+
"""Test collect_and_upload_entra_evidence with all evidence types"""
|
|
814
|
+
mock_app = MagicMock(spec=Application)
|
|
815
|
+
mock_check_license.return_value = mock_app
|
|
816
|
+
mock_is_valid.return_value = True
|
|
817
|
+
|
|
818
|
+
mock_api = MagicMock()
|
|
819
|
+
mock_api_class.return_value = mock_api
|
|
820
|
+
|
|
821
|
+
mock_defender_api_instance = MagicMock()
|
|
822
|
+
mock_defender_api.return_value = mock_defender_api_instance
|
|
823
|
+
mock_defender_api_instance.collect_all_entra_evidence.return_value = {
|
|
824
|
+
"users": [Path("/test/users.csv")],
|
|
825
|
+
"sign_in_logs": [Path("/test/logs.csv")],
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans", days_back=30, evidence_type="all")
|
|
829
|
+
|
|
830
|
+
mock_defender_api.assert_called_once_with(system="entra")
|
|
831
|
+
mock_defender_api_instance.collect_all_entra_evidence.assert_called_once_with(days_back=30)
|
|
832
|
+
mock_upload_evidence.assert_called_once()
|
|
833
|
+
|
|
834
|
+
@patch(f"{PATH}.upload_evidence_files")
|
|
835
|
+
@patch(f"{PATH}.collect_specific_evidence_type")
|
|
836
|
+
@patch(f"{PATH}.DefenderApi")
|
|
837
|
+
@patch(f"{PATH}.is_valid")
|
|
838
|
+
@patch(f"{PATH}.Api")
|
|
839
|
+
@patch(f"{PATH}.check_license")
|
|
840
|
+
def test_collect_and_upload_entra_evidence_specific_type(
|
|
841
|
+
self,
|
|
842
|
+
mock_check_license,
|
|
843
|
+
mock_api_class,
|
|
844
|
+
mock_is_valid,
|
|
845
|
+
mock_defender_api,
|
|
846
|
+
mock_collect_specific,
|
|
847
|
+
mock_upload_evidence,
|
|
848
|
+
):
|
|
849
|
+
"""Test collect_and_upload_entra_evidence with specific evidence type"""
|
|
850
|
+
mock_app = MagicMock(spec=Application)
|
|
851
|
+
mock_check_license.return_value = mock_app
|
|
852
|
+
mock_is_valid.return_value = True
|
|
853
|
+
|
|
854
|
+
mock_api = MagicMock()
|
|
855
|
+
mock_api_class.return_value = mock_api
|
|
856
|
+
|
|
857
|
+
mock_defender_api_instance = MagicMock()
|
|
858
|
+
mock_defender_api.return_value = mock_defender_api_instance
|
|
859
|
+
|
|
860
|
+
mock_collect_specific.return_value = {"users": [Path("/test/users.csv")]}
|
|
861
|
+
|
|
862
|
+
collect_and_upload_entra_evidence(
|
|
863
|
+
parent_id=1, parent_module="securityplans", days_back=30, evidence_type="users_groups"
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
mock_defender_api.assert_called_once_with(system="entra")
|
|
867
|
+
mock_collect_specific.assert_called_once_with(mock_defender_api_instance, "users_groups", 30)
|
|
868
|
+
mock_upload_evidence.assert_called_once()
|
|
869
|
+
|
|
870
|
+
@patch(f"{PATH}.error_and_exit")
|
|
871
|
+
@patch(f"{PATH}.is_valid")
|
|
872
|
+
@patch(f"{PATH}.Api")
|
|
873
|
+
@patch(f"{PATH}.check_license")
|
|
874
|
+
def test_collect_and_upload_entra_evidence_invalid_login(
|
|
875
|
+
self, mock_check_license, mock_api_class, mock_is_valid, mock_error_exit
|
|
876
|
+
):
|
|
877
|
+
"""Test collect_and_upload_entra_evidence with invalid login"""
|
|
878
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
879
|
+
mock_app = MagicMock(spec=Application)
|
|
880
|
+
mock_check_license.return_value = mock_app
|
|
881
|
+
mock_is_valid.return_value = False
|
|
882
|
+
|
|
883
|
+
with pytest.raises(SystemExit):
|
|
884
|
+
collect_and_upload_entra_evidence(
|
|
885
|
+
parent_id=1, parent_module="securityplans", days_back=30, evidence_type="all"
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
mock_error_exit.assert_called_once_with("Login Invalid RegScale Credentials, please login for a new token.")
|
|
889
|
+
|
|
890
|
+
@patch(f"{PATH}.error_and_exit")
|
|
891
|
+
@patch(f"{PATH}.DefenderApi")
|
|
892
|
+
@patch(f"{PATH}.is_valid")
|
|
893
|
+
@patch(f"{PATH}.Api")
|
|
894
|
+
@patch(f"{PATH}.check_license")
|
|
895
|
+
def test_collect_and_upload_entra_evidence_with_exception(
|
|
896
|
+
self, mock_check_license, mock_api_class, mock_is_valid, mock_defender_api, mock_error_exit
|
|
897
|
+
):
|
|
898
|
+
"""Test collect_and_upload_entra_evidence with exception"""
|
|
899
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
900
|
+
mock_app = MagicMock(spec=Application)
|
|
901
|
+
mock_check_license.return_value = mock_app
|
|
902
|
+
mock_is_valid.return_value = True
|
|
903
|
+
|
|
904
|
+
mock_api = MagicMock()
|
|
905
|
+
mock_api_class.return_value = mock_api
|
|
906
|
+
|
|
907
|
+
mock_defender_api_instance = MagicMock()
|
|
908
|
+
mock_defender_api.return_value = mock_defender_api_instance
|
|
909
|
+
mock_defender_api_instance.collect_all_entra_evidence.side_effect = Exception("API Error")
|
|
910
|
+
|
|
911
|
+
with pytest.raises(SystemExit):
|
|
912
|
+
collect_and_upload_entra_evidence(
|
|
913
|
+
parent_id=1, parent_module="securityplans", days_back=30, evidence_type="all"
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
mock_error_exit.assert_called_once_with("Error collecting Azure Entra evidence: API Error")
|
|
917
|
+
|
|
918
|
+
def test_collect_specific_evidence_type_users_groups(self):
|
|
919
|
+
"""Test collect_specific_evidence_type for users_groups"""
|
|
920
|
+
mock_defender_api = MagicMock()
|
|
921
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
922
|
+
|
|
923
|
+
result = collect_specific_evidence_type(mock_defender_api, "users_groups", 30)
|
|
924
|
+
|
|
925
|
+
expected_calls = [
|
|
926
|
+
("users",),
|
|
927
|
+
("guest_users",),
|
|
928
|
+
("groups_and_members",),
|
|
929
|
+
("security_groups",),
|
|
930
|
+
]
|
|
931
|
+
actual_calls = [call[0] for call in mock_defender_api.get_and_save_entra_evidence.call_args_list]
|
|
932
|
+
for expected_call in expected_calls:
|
|
933
|
+
assert expected_call in actual_calls
|
|
934
|
+
|
|
935
|
+
assert "users" in result
|
|
936
|
+
assert "guest_users" in result
|
|
937
|
+
assert "security_groups" in result
|
|
938
|
+
assert "groups_and_members" in result
|
|
939
|
+
|
|
940
|
+
def test_collect_specific_evidence_type_rbac_pim(self):
|
|
941
|
+
"""Test collect_specific_evidence_type for rbac_pim"""
|
|
942
|
+
mock_defender_api = MagicMock()
|
|
943
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
944
|
+
|
|
945
|
+
result = collect_specific_evidence_type(mock_defender_api, "rbac_pim", 30)
|
|
946
|
+
|
|
947
|
+
expected_calls = [
|
|
948
|
+
("role_assignments",),
|
|
949
|
+
("role_definitions",),
|
|
950
|
+
("pim_assignments",),
|
|
951
|
+
("pim_eligibility",),
|
|
952
|
+
]
|
|
953
|
+
actual_calls = [call[0] for call in mock_defender_api.get_and_save_entra_evidence.call_args_list]
|
|
954
|
+
for expected_call in expected_calls:
|
|
955
|
+
assert expected_call in actual_calls
|
|
956
|
+
|
|
957
|
+
assert "role_assignments" in result
|
|
958
|
+
assert "role_definitions" in result
|
|
959
|
+
assert "pim_assignments" in result
|
|
960
|
+
assert "pim_eligibility" in result
|
|
961
|
+
|
|
962
|
+
def test_collect_specific_evidence_type_audit_logs(self):
|
|
963
|
+
"""Test collect_specific_evidence_type for audit_logs"""
|
|
964
|
+
mock_defender_api = MagicMock()
|
|
965
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
966
|
+
|
|
967
|
+
result = collect_specific_evidence_type(mock_defender_api, "audit_logs", 30)
|
|
968
|
+
|
|
969
|
+
# Verify start_date parameter is passed for audit log endpoints
|
|
970
|
+
expected_calls_with_start_date = [
|
|
971
|
+
"sign_in_logs",
|
|
972
|
+
"directory_audits",
|
|
973
|
+
"provisioning_logs",
|
|
974
|
+
]
|
|
975
|
+
for call in mock_defender_api.get_and_save_entra_evidence.call_args_list:
|
|
976
|
+
endpoint_key = call[0][0]
|
|
977
|
+
if endpoint_key in expected_calls_with_start_date:
|
|
978
|
+
assert "start_date" in call[1]
|
|
979
|
+
|
|
980
|
+
assert "sign_in_logs" in result
|
|
981
|
+
assert "directory_audits" in result
|
|
982
|
+
assert "provisioning_logs" in result
|
|
983
|
+
|
|
984
|
+
def test_collect_specific_evidence_type_access_reviews(self):
|
|
985
|
+
"""Test collect_specific_evidence_type for access_reviews"""
|
|
986
|
+
mock_defender_api = MagicMock()
|
|
987
|
+
mock_defender_api.collect_entra_access_reviews.return_value = [Path("/test/file.csv")]
|
|
988
|
+
|
|
989
|
+
result = collect_specific_evidence_type(mock_defender_api, "access_reviews", 30)
|
|
990
|
+
|
|
991
|
+
mock_defender_api.collect_entra_access_reviews.assert_called_once()
|
|
992
|
+
assert "access_review_definitions" in result
|
|
993
|
+
|
|
994
|
+
@patch("regscale.models.ControlImplementation.get_list_by_parent")
|
|
995
|
+
def test_get_control_implementations_map_success(self, mock_get_list):
|
|
996
|
+
"""Test get_control_implementations_map with successful result"""
|
|
997
|
+
from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
|
|
998
|
+
|
|
999
|
+
# Mock control implementations
|
|
1000
|
+
mock_controls = [
|
|
1001
|
+
{"id": 1, "controlId": "AC-1"},
|
|
1002
|
+
{"id": 2, "controlId": "AC-2"},
|
|
1003
|
+
{"id": 3, "controlId": "IA-2"},
|
|
1004
|
+
]
|
|
1005
|
+
mock_get_list.return_value = mock_controls
|
|
1006
|
+
|
|
1007
|
+
result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
|
|
1008
|
+
|
|
1009
|
+
mock_get_list.assert_called_once_with(1, "securityplans")
|
|
1010
|
+
expected = {"AC-1": 1, "AC-2": 2, "IA-2": 3}
|
|
1011
|
+
assert result == expected
|
|
1012
|
+
|
|
1013
|
+
@patch("regscale.models.ControlImplementation.get_list_by_parent")
|
|
1014
|
+
def test_get_control_implementations_map_empty(self, mock_get_list):
|
|
1015
|
+
"""Test get_control_implementations_map with empty result"""
|
|
1016
|
+
from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
|
|
1017
|
+
|
|
1018
|
+
mock_get_list.return_value = []
|
|
1019
|
+
|
|
1020
|
+
result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
|
|
1021
|
+
|
|
1022
|
+
assert result == {}
|
|
1023
|
+
|
|
1024
|
+
@patch("regscale.models.ControlImplementation.get_list_by_parent")
|
|
1025
|
+
def test_get_control_implementations_map_with_exception(self, mock_get_list):
|
|
1026
|
+
"""Test get_control_implementations_map with exception"""
|
|
1027
|
+
from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
|
|
1028
|
+
|
|
1029
|
+
mock_get_list.side_effect = Exception("Database error")
|
|
1030
|
+
|
|
1031
|
+
result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
|
|
1032
|
+
|
|
1033
|
+
assert result == {}
|
|
1034
|
+
|
|
1035
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
1036
|
+
def test_upload_evidence_to_controls_success(self, mock_upload_file):
|
|
1037
|
+
"""Test upload_evidence_to_controls with successful uploads"""
|
|
1038
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
|
|
1039
|
+
|
|
1040
|
+
mock_upload_file.return_value = True
|
|
1041
|
+
evidence_files = [Path("/test/users.csv"), Path("/test/logs.csv")]
|
|
1042
|
+
control_map = {"AC-1": 1, "AC-2": 2, "IA-2": 3}
|
|
1043
|
+
mock_api = MagicMock()
|
|
1044
|
+
|
|
1045
|
+
result = upload_evidence_to_controls(
|
|
1046
|
+
evidence_key="users",
|
|
1047
|
+
evidence_file_list=evidence_files,
|
|
1048
|
+
control_implementations_map=control_map,
|
|
1049
|
+
api=mock_api,
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
# For users evidence, it should upload to AC-1, AC-2, and other user-related controls
|
|
1053
|
+
# Each file should be uploaded to multiple controls
|
|
1054
|
+
assert mock_upload_file.call_count > 0
|
|
1055
|
+
assert result > 0
|
|
1056
|
+
|
|
1057
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
1058
|
+
def test_upload_evidence_to_controls_no_mapping(self, mock_upload_file):
|
|
1059
|
+
"""Test upload_evidence_to_controls with unknown evidence key"""
|
|
1060
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
|
|
1061
|
+
|
|
1062
|
+
evidence_files = [Path("/test/unknown.csv")]
|
|
1063
|
+
control_map = {"AC-1": 1}
|
|
1064
|
+
mock_api = MagicMock()
|
|
1065
|
+
|
|
1066
|
+
result = upload_evidence_to_controls(
|
|
1067
|
+
evidence_key="unknown_evidence",
|
|
1068
|
+
evidence_file_list=evidence_files,
|
|
1069
|
+
control_implementations_map=control_map,
|
|
1070
|
+
api=mock_api,
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
mock_upload_file.assert_not_called()
|
|
1074
|
+
assert result == 0
|
|
1075
|
+
|
|
1076
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
1077
|
+
def test_upload_evidence_to_controls_upload_failure(self, mock_upload_file):
|
|
1078
|
+
"""Test upload_evidence_to_controls with upload failures"""
|
|
1079
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
|
|
1080
|
+
|
|
1081
|
+
mock_upload_file.return_value = False
|
|
1082
|
+
evidence_files = [Path("/test/users.csv")]
|
|
1083
|
+
control_map = {"AC-1": 1, "AC-2": 2}
|
|
1084
|
+
mock_api = MagicMock()
|
|
1085
|
+
|
|
1086
|
+
result = upload_evidence_to_controls(
|
|
1087
|
+
evidence_key="users",
|
|
1088
|
+
evidence_file_list=evidence_files,
|
|
1089
|
+
control_implementations_map=control_map,
|
|
1090
|
+
api=mock_api,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
assert mock_upload_file.call_count > 0
|
|
1094
|
+
assert result == 0
|
|
1095
|
+
|
|
1096
|
+
@patch(f"{PATH}.get_control_implementations_map")
|
|
1097
|
+
@patch(f"{PATH}.upload_evidence_to_controls")
|
|
1098
|
+
def test_upload_evidence_files_success(self, mock_upload_to_controls, mock_get_control_map):
|
|
1099
|
+
"""Test upload_evidence_files with successful uploads"""
|
|
1100
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
|
|
1101
|
+
|
|
1102
|
+
mock_get_control_map.return_value = {"AC-1": 1, "AC-2": 2}
|
|
1103
|
+
mock_upload_to_controls.return_value = 3
|
|
1104
|
+
|
|
1105
|
+
evidence_data = {
|
|
1106
|
+
"users": [Path("/test/users.csv")],
|
|
1107
|
+
"sign_in_logs": [Path("/test/logs.csv")],
|
|
1108
|
+
}
|
|
1109
|
+
mock_api = MagicMock()
|
|
1110
|
+
|
|
1111
|
+
upload_evidence_files(
|
|
1112
|
+
evidence_data=evidence_data, parent_id=1, parent_module="securityplans", api=mock_api, evidence_type="all"
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
mock_get_control_map.assert_called_once_with(1, "securityplans")
|
|
1116
|
+
assert mock_upload_to_controls.call_count == 2 # Called for each evidence type
|
|
1117
|
+
|
|
1118
|
+
@patch(f"{PATH}.get_control_implementations_map")
|
|
1119
|
+
def test_upload_evidence_files_no_controls(self, mock_get_control_map):
|
|
1120
|
+
"""Test upload_evidence_files with no control implementations"""
|
|
1121
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
|
|
1122
|
+
|
|
1123
|
+
mock_get_control_map.return_value = {}
|
|
1124
|
+
|
|
1125
|
+
evidence_data = {"users": [Path("/test/users.csv")]}
|
|
1126
|
+
mock_api = MagicMock()
|
|
1127
|
+
|
|
1128
|
+
# Should return early when no control implementations are found
|
|
1129
|
+
upload_evidence_files(
|
|
1130
|
+
evidence_data=evidence_data, parent_id=1, parent_module="securityplans", api=mock_api, evidence_type="all"
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
mock_get_control_map.assert_called_once()
|
|
1134
|
+
|
|
1135
|
+
@patch(f"{PATH}.console")
|
|
1136
|
+
def test_show_entra_mappings_all(self, mock_console):
|
|
1137
|
+
"""Test show_entra_mappings displaying all mappings"""
|
|
1138
|
+
from click.testing import CliRunner
|
|
1139
|
+
from regscale.integrations.commercial.microsoft_defender.defender import show_entra_mappings
|
|
1140
|
+
|
|
1141
|
+
# Mock the console.print calls
|
|
1142
|
+
mock_console.print.return_value = None
|
|
1143
|
+
|
|
1144
|
+
# Use Click's test runner to invoke the command
|
|
1145
|
+
runner = CliRunner()
|
|
1146
|
+
result = runner.invoke(show_entra_mappings, ["--evidence_type", "all"])
|
|
1147
|
+
|
|
1148
|
+
# Verify the command executed successfully
|
|
1149
|
+
assert result.exit_code == 0
|
|
1150
|
+
# Verify console.print was called (table creation and final message)
|
|
1151
|
+
assert mock_console.print.call_count == 2
|
|
1152
|
+
|
|
1153
|
+
@patch(f"{PATH}.console")
|
|
1154
|
+
def test_show_entra_mappings_specific_type(self, mock_console):
|
|
1155
|
+
"""Test show_entra_mappings displaying specific type"""
|
|
1156
|
+
from click.testing import CliRunner
|
|
1157
|
+
from regscale.integrations.commercial.microsoft_defender.defender import show_entra_mappings
|
|
1158
|
+
|
|
1159
|
+
mock_console.print.return_value = None
|
|
1160
|
+
|
|
1161
|
+
# Use Click's test runner to invoke the command
|
|
1162
|
+
runner = CliRunner()
|
|
1163
|
+
result = runner.invoke(show_entra_mappings, ["--evidence_type", "users_groups"])
|
|
1164
|
+
|
|
1165
|
+
# Verify the command executed successfully
|
|
1166
|
+
assert result.exit_code == 0
|
|
1167
|
+
# Verify console.print was called
|
|
1168
|
+
assert mock_console.print.call_count == 2
|
|
1169
|
+
|
|
1170
|
+
# ==============================
|
|
1171
|
+
# COMPREHENSIVE ENTRA FUNCTIONALITY TESTS
|
|
1172
|
+
# ==============================
|
|
1173
|
+
|
|
1174
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
1175
|
+
@patch(f"{PATH}.DefenderApi")
|
|
1176
|
+
def test_authenticate_entra(self, mock_defender_api, mock_check_license):
|
|
1177
|
+
"""Test authenticating with Azure Entra"""
|
|
1178
|
+
mock_api_instance = MagicMock()
|
|
1179
|
+
mock_defender_api.return_value = mock_api_instance
|
|
1180
|
+
|
|
1181
|
+
authenticate(system="entra")
|
|
1182
|
+
|
|
1183
|
+
mock_check_license.assert_called_once()
|
|
1184
|
+
mock_defender_api.assert_called_once_with(system="entra")
|
|
1185
|
+
mock_api_instance.get_token.assert_called_once()
|
|
1186
|
+
|
|
1187
|
+
@patch(f"{PATH}.error_and_exit")
|
|
1188
|
+
@patch(f"{PATH}.is_valid")
|
|
1189
|
+
@patch(f"{PATH}.check_license")
|
|
1190
|
+
@patch(f"{PATH}.Api")
|
|
1191
|
+
@patch(f"{PATH}.DefenderApi")
|
|
1192
|
+
def test_collect_and_upload_entra_evidence_invalid_auth(
|
|
1193
|
+
self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_error_exit
|
|
1194
|
+
):
|
|
1195
|
+
"""Test collect_and_upload_entra_evidence with invalid authentication"""
|
|
1196
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
1197
|
+
mock_check_license.return_value = MagicMock()
|
|
1198
|
+
mock_is_valid.return_value = False
|
|
1199
|
+
|
|
1200
|
+
with pytest.raises(SystemExit):
|
|
1201
|
+
collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans")
|
|
1202
|
+
|
|
1203
|
+
mock_error_exit.assert_called_once()
|
|
1204
|
+
|
|
1205
|
+
@patch(f"{PATH}.upload_evidence_files")
|
|
1206
|
+
@patch(f"{PATH}.collect_specific_evidence_type")
|
|
1207
|
+
@patch(f"{PATH}.is_valid")
|
|
1208
|
+
@patch(f"{PATH}.check_license")
|
|
1209
|
+
@patch(f"{PATH}.Api")
|
|
1210
|
+
@patch(f"{PATH}.DefenderApi")
|
|
1211
|
+
def test_collect_and_upload_entra_evidence_specific_type(
|
|
1212
|
+
self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_collect_specific, mock_upload
|
|
1213
|
+
):
|
|
1214
|
+
"""Test collect_and_upload_entra_evidence with specific evidence type"""
|
|
1215
|
+
# Setup mocks
|
|
1216
|
+
mock_check_license.return_value = MagicMock()
|
|
1217
|
+
mock_is_valid.return_value = True
|
|
1218
|
+
mock_collect_specific.return_value = {"users": [Path("/test/users.csv")]}
|
|
1219
|
+
mock_upload.return_value = None
|
|
1220
|
+
|
|
1221
|
+
# Call function with specific evidence type
|
|
1222
|
+
collect_and_upload_entra_evidence(
|
|
1223
|
+
parent_id=1, parent_module="securityplans", days_back=30, evidence_type="users_groups"
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
# Verify specific evidence collection was called
|
|
1227
|
+
mock_collect_specific.assert_called_once_with(mock_defender_api.return_value, "users_groups", 30)
|
|
1228
|
+
mock_upload.assert_called_once()
|
|
1229
|
+
|
|
1230
|
+
@patch(f"{PATH}.upload_evidence_files")
|
|
1231
|
+
@patch(f"{PATH}.is_valid")
|
|
1232
|
+
@patch(f"{PATH}.check_license")
|
|
1233
|
+
@patch(f"{PATH}.Api")
|
|
1234
|
+
@patch(f"{PATH}.DefenderApi")
|
|
1235
|
+
def test_collect_and_upload_entra_evidence_all(
|
|
1236
|
+
self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_upload
|
|
1237
|
+
):
|
|
1238
|
+
"""Test collect_and_upload_entra_evidence collecting all evidence"""
|
|
1239
|
+
# Setup mocks
|
|
1240
|
+
mock_check_license.return_value = MagicMock()
|
|
1241
|
+
mock_is_valid.return_value = True
|
|
1242
|
+
mock_api_instance = MagicMock()
|
|
1243
|
+
mock_defender_api.return_value = mock_api_instance
|
|
1244
|
+
mock_api_instance.collect_all_entra_evidence.return_value = {
|
|
1245
|
+
"users": [Path("/test/users.csv")],
|
|
1246
|
+
"sign_in_logs": [Path("/test/signin.csv")],
|
|
1247
|
+
}
|
|
1248
|
+
mock_upload.return_value = None
|
|
1249
|
+
|
|
1250
|
+
# Call function with evidence_type="all"
|
|
1251
|
+
collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans", evidence_type="all")
|
|
1252
|
+
|
|
1253
|
+
# Verify all evidence collection was called
|
|
1254
|
+
mock_api_instance.collect_all_entra_evidence.assert_called_once_with(days_back=30)
|
|
1255
|
+
mock_upload.assert_called_once()
|
|
1256
|
+
|
|
1257
|
+
@patch(f"{PATH}.error_and_exit")
|
|
1258
|
+
@patch(f"{PATH}.is_valid")
|
|
1259
|
+
@patch(f"{PATH}.check_license")
|
|
1260
|
+
@patch(f"{PATH}.Api")
|
|
1261
|
+
@patch(f"{PATH}.DefenderApi")
|
|
1262
|
+
def test_collect_and_upload_entra_evidence_exception(
|
|
1263
|
+
self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_error_exit
|
|
1264
|
+
):
|
|
1265
|
+
"""Test collect_and_upload_entra_evidence handles exceptions"""
|
|
1266
|
+
mock_error_exit.side_effect = SystemExit(1)
|
|
1267
|
+
mock_check_license.return_value = MagicMock()
|
|
1268
|
+
mock_is_valid.return_value = True
|
|
1269
|
+
mock_api_instance = MagicMock()
|
|
1270
|
+
mock_defender_api.return_value = mock_api_instance
|
|
1271
|
+
mock_api_instance.collect_all_entra_evidence.side_effect = Exception("API Error")
|
|
1272
|
+
|
|
1273
|
+
with pytest.raises(SystemExit):
|
|
1274
|
+
collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans")
|
|
1275
|
+
|
|
1276
|
+
mock_error_exit.assert_called_once()
|
|
1277
|
+
error_message = mock_error_exit.call_args[0][0]
|
|
1278
|
+
assert "Error collecting Azure Entra evidence" in error_message
|
|
1279
|
+
|
|
1280
|
+
def test_collect_specific_evidence_type_users_groups(self):
|
|
1281
|
+
"""Test collect_specific_evidence_type for users_groups"""
|
|
1282
|
+
mock_defender_api = MagicMock()
|
|
1283
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
1284
|
+
|
|
1285
|
+
result = collect_specific_evidence_type(mock_defender_api, "users_groups", 30)
|
|
1286
|
+
|
|
1287
|
+
# Verify all expected user/group endpoints were called
|
|
1288
|
+
expected_calls = ["users", "guest_users", "groups_and_members", "security_groups"]
|
|
1289
|
+
assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
|
|
1290
|
+
|
|
1291
|
+
for call in mock_defender_api.get_and_save_entra_evidence.call_args_list:
|
|
1292
|
+
endpoint = call[0][0] # First positional argument
|
|
1293
|
+
assert endpoint in expected_calls
|
|
1294
|
+
|
|
1295
|
+
# Verify all evidence types are in result
|
|
1296
|
+
for expected_type in expected_calls:
|
|
1297
|
+
assert expected_type in result
|
|
1298
|
+
|
|
1299
|
+
def test_collect_specific_evidence_type_rbac_pim(self):
|
|
1300
|
+
"""Test collect_specific_evidence_type for rbac_pim"""
|
|
1301
|
+
mock_defender_api = MagicMock()
|
|
1302
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
1303
|
+
|
|
1304
|
+
result = collect_specific_evidence_type(mock_defender_api, "rbac_pim", 30)
|
|
1305
|
+
|
|
1306
|
+
expected_calls = ["role_assignments", "role_definitions", "pim_assignments", "pim_eligibility"]
|
|
1307
|
+
assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
|
|
1308
|
+
|
|
1309
|
+
for expected_type in expected_calls:
|
|
1310
|
+
assert expected_type in result
|
|
1311
|
+
|
|
1312
|
+
def test_collect_specific_evidence_type_conditional_access(self):
|
|
1313
|
+
"""Test collect_specific_evidence_type for conditional_access"""
|
|
1314
|
+
mock_defender_api = MagicMock()
|
|
1315
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
1316
|
+
|
|
1317
|
+
result = collect_specific_evidence_type(mock_defender_api, "conditional_access", 30)
|
|
1318
|
+
|
|
1319
|
+
mock_defender_api.get_and_save_entra_evidence.assert_called_once_with("conditional_access")
|
|
1320
|
+
assert "conditional_access" in result
|
|
1321
|
+
|
|
1322
|
+
def test_collect_specific_evidence_type_authentication(self):
|
|
1323
|
+
"""Test collect_specific_evidence_type for authentication"""
|
|
1324
|
+
mock_defender_api = MagicMock()
|
|
1325
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
1326
|
+
|
|
1327
|
+
result = collect_specific_evidence_type(mock_defender_api, "authentication", 30)
|
|
1328
|
+
|
|
1329
|
+
expected_calls = ["auth_methods_policy", "user_mfa_registration", "mfa_registered_users"]
|
|
1330
|
+
assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
|
|
1331
|
+
|
|
1332
|
+
for expected_type in expected_calls:
|
|
1333
|
+
assert expected_type in result
|
|
1334
|
+
|
|
1335
|
+
def test_collect_specific_evidence_type_audit_logs(self):
|
|
1336
|
+
"""Test collect_specific_evidence_type for audit_logs with start_date"""
|
|
1337
|
+
mock_defender_api = MagicMock()
|
|
1338
|
+
mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
|
|
1339
|
+
|
|
1340
|
+
result = collect_specific_evidence_type(mock_defender_api, "audit_logs", 60)
|
|
1341
|
+
|
|
1342
|
+
expected_calls = ["sign_in_logs", "directory_audits", "provisioning_logs"]
|
|
1343
|
+
assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
|
|
1344
|
+
|
|
1345
|
+
# Verify start_date parameter was passed for audit logs
|
|
1346
|
+
for call in mock_defender_api.get_and_save_entra_evidence.call_args_list:
|
|
1347
|
+
kwargs = call[1] # Keyword arguments
|
|
1348
|
+
assert "start_date" in kwargs
|
|
1349
|
+
# Verify it's a valid date string for 60 days back
|
|
1350
|
+
start_date = kwargs["start_date"]
|
|
1351
|
+
assert start_date.endswith("T00:00:00Z")
|
|
1352
|
+
|
|
1353
|
+
for expected_type in expected_calls:
|
|
1354
|
+
assert expected_type in result
|
|
1355
|
+
|
|
1356
|
+
def test_collect_specific_evidence_type_access_reviews(self):
|
|
1357
|
+
"""Test collect_specific_evidence_type for access_reviews"""
|
|
1358
|
+
mock_defender_api = MagicMock()
|
|
1359
|
+
mock_defender_api.collect_entra_access_reviews.return_value = [Path("/test/file.csv")]
|
|
1360
|
+
|
|
1361
|
+
result = collect_specific_evidence_type(mock_defender_api, "access_reviews", 30)
|
|
1362
|
+
|
|
1363
|
+
mock_defender_api.collect_entra_access_reviews.assert_called_once()
|
|
1364
|
+
assert "access_review_definitions" in result
|
|
1365
|
+
|
|
1366
|
+
@patch("regscale.models.ControlImplementation")
|
|
1367
|
+
def test_get_control_implementations_map_success(self, mock_control_impl):
|
|
1368
|
+
"""Test get_control_implementations_map with successful retrieval"""
|
|
1369
|
+
from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
|
|
1370
|
+
|
|
1371
|
+
# Mock control implementations
|
|
1372
|
+
mock_controls = [
|
|
1373
|
+
{"id": 1, "controlId": "AC-2"},
|
|
1374
|
+
{"id": 2, "controlId": "AU-3"},
|
|
1375
|
+
{"id": 3, "controlId": "IA-2"},
|
|
1376
|
+
]
|
|
1377
|
+
mock_control_impl.get_list_by_parent.return_value = mock_controls
|
|
1378
|
+
|
|
1379
|
+
result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
|
|
1380
|
+
|
|
1381
|
+
expected = {"AC-2": 1, "AU-3": 2, "IA-2": 3}
|
|
1382
|
+
assert result == expected
|
|
1383
|
+
mock_control_impl.get_list_by_parent.assert_called_once_with(1, "securityplans")
|
|
1384
|
+
|
|
1385
|
+
@patch("regscale.models.ControlImplementation")
|
|
1386
|
+
def test_get_control_implementations_map_empty(self, mock_control_impl):
|
|
1387
|
+
"""Test get_control_implementations_map with no control implementations"""
|
|
1388
|
+
from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
|
|
1389
|
+
|
|
1390
|
+
mock_control_impl.get_list_by_parent.return_value = []
|
|
1391
|
+
|
|
1392
|
+
result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
|
|
1393
|
+
|
|
1394
|
+
assert result == {}
|
|
1395
|
+
|
|
1396
|
+
@patch("regscale.models.ControlImplementation")
|
|
1397
|
+
def test_get_control_implementations_map_exception(self, mock_control_impl):
|
|
1398
|
+
"""Test get_control_implementations_map handles exceptions"""
|
|
1399
|
+
from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
|
|
1400
|
+
|
|
1401
|
+
mock_control_impl.get_list_by_parent.side_effect = Exception("API Error")
|
|
1402
|
+
|
|
1403
|
+
result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
|
|
1404
|
+
|
|
1405
|
+
assert result == {}
|
|
1406
|
+
|
|
1407
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
1408
|
+
def test_upload_evidence_to_controls_success(self, mock_upload):
|
|
1409
|
+
"""Test upload_evidence_to_controls with successful uploads"""
|
|
1410
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
|
|
1411
|
+
|
|
1412
|
+
mock_upload.return_value = True
|
|
1413
|
+
evidence_files = [Path("/test/users.csv"), Path("/test/users2.csv")]
|
|
1414
|
+
# users evidence maps to: [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12]
|
|
1415
|
+
control_map = {"AC-1": 1, "AC-2": 2, "AC-2(1)": 3, "AC-2(3)": 4, "AC-2(5)": 5, "AC-2(7)": 6, "AC-2(12)": 7}
|
|
1416
|
+
api = MagicMock()
|
|
1417
|
+
|
|
1418
|
+
result = upload_evidence_to_controls("users", evidence_files, control_map, api)
|
|
1419
|
+
|
|
1420
|
+
# users evidence maps to 7 controls × 2 files = 14 expected uploads
|
|
1421
|
+
assert result == 14
|
|
1422
|
+
assert mock_upload.call_count == 14
|
|
1423
|
+
|
|
1424
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
1425
|
+
def test_upload_evidence_to_controls_no_mapping(self, mock_upload):
|
|
1426
|
+
"""Test upload_evidence_to_controls with no control mapping"""
|
|
1427
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
|
|
1428
|
+
|
|
1429
|
+
evidence_files = [Path("/test/unknown.csv")]
|
|
1430
|
+
control_map = {"AC-2": 1}
|
|
1431
|
+
api = MagicMock()
|
|
1432
|
+
|
|
1433
|
+
result = upload_evidence_to_controls("unknown_evidence", evidence_files, control_map, api)
|
|
1434
|
+
|
|
1435
|
+
assert result == 0
|
|
1436
|
+
mock_upload.assert_not_called()
|
|
1437
|
+
|
|
1438
|
+
@patch(f"{PATH}.File.upload_file_to_regscale")
|
|
1439
|
+
def test_upload_evidence_to_controls_partial_failure(self, mock_upload):
|
|
1440
|
+
"""Test upload_evidence_to_controls with partial upload failures"""
|
|
1441
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
|
|
1442
|
+
|
|
1443
|
+
# Mock alternating success/failure
|
|
1444
|
+
mock_upload.side_effect = [True, False, True, False, True, False, True]
|
|
1445
|
+
evidence_files = [Path("/test/users.csv")]
|
|
1446
|
+
control_map = {"AC-2": 1, "AC-2(1)": 2, "AC-2(3)": 3, "AC-2(5)": 4, "AC-2(7)": 5, "AC-2(12)": 6, "AC-1": 7}
|
|
1447
|
+
api = MagicMock()
|
|
1448
|
+
|
|
1449
|
+
result = upload_evidence_to_controls("users", evidence_files, control_map, api)
|
|
1450
|
+
|
|
1451
|
+
# Should return count of successful uploads (4 out of 7)
|
|
1452
|
+
assert result == 4
|
|
1453
|
+
|
|
1454
|
+
@patch(f"{PATH}.upload_evidence_to_controls")
|
|
1455
|
+
@patch(f"{PATH}.get_control_implementations_map")
|
|
1456
|
+
@patch(f"{PATH}.Path")
|
|
1457
|
+
def test_upload_evidence_files_success(self, mock_path, mock_get_control_map, mock_upload_evidence):
|
|
1458
|
+
"""Test upload_evidence_files with successful uploads"""
|
|
1459
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
|
|
1460
|
+
|
|
1461
|
+
mock_get_control_map.return_value = {"AC-2": 1, "AU-3": 2}
|
|
1462
|
+
mock_upload_evidence.return_value = 5
|
|
1463
|
+
mock_path.return_value.mkdir.return_value = None
|
|
1464
|
+
|
|
1465
|
+
evidence_data = {
|
|
1466
|
+
"users": [Path("/test/users.csv")],
|
|
1467
|
+
"sign_in_logs": [Path("/test/signin.csv")],
|
|
1468
|
+
}
|
|
1469
|
+
api = MagicMock()
|
|
1470
|
+
|
|
1471
|
+
upload_evidence_files(evidence_data, 1, "securityplans", api, "all")
|
|
1472
|
+
|
|
1473
|
+
# Verify evidence upload was called for each evidence type
|
|
1474
|
+
assert mock_upload_evidence.call_count == 2
|
|
1475
|
+
mock_get_control_map.assert_called_once_with(1, "securityplans")
|
|
1476
|
+
|
|
1477
|
+
@patch(f"{PATH}.get_control_implementations_map")
|
|
1478
|
+
def test_upload_evidence_files_no_control_implementations(self, mock_get_control_map):
|
|
1479
|
+
"""Test upload_evidence_files when no control implementations exist"""
|
|
1480
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
|
|
1481
|
+
|
|
1482
|
+
mock_get_control_map.return_value = {}
|
|
1483
|
+
|
|
1484
|
+
evidence_data = {"users": [Path("/test/users.csv")]}
|
|
1485
|
+
api = MagicMock()
|
|
1486
|
+
|
|
1487
|
+
# Should return early when no control implementations are found
|
|
1488
|
+
upload_evidence_files(evidence_data, 1, "securityplans", api, "all")
|
|
1489
|
+
|
|
1490
|
+
mock_get_control_map.assert_called_once()
|
|
1491
|
+
|
|
1492
|
+
@patch(f"{PATH}.upload_evidence_to_controls")
|
|
1493
|
+
@patch(f"{PATH}.get_control_implementations_map")
|
|
1494
|
+
@patch(f"{PATH}.Path")
|
|
1495
|
+
def test_upload_evidence_files_empty_evidence_lists(self, mock_path, mock_get_control_map, mock_upload_evidence):
|
|
1496
|
+
"""Test upload_evidence_files handles empty evidence lists"""
|
|
1497
|
+
from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
|
|
1498
|
+
|
|
1499
|
+
mock_get_control_map.return_value = {"AC-2": 1}
|
|
1500
|
+
mock_path.return_value.mkdir.return_value = None
|
|
1501
|
+
|
|
1502
|
+
evidence_data = {
|
|
1503
|
+
"users": [], # Empty list
|
|
1504
|
+
"guest_users": [Path("/test/guests.csv")], # Non-empty list
|
|
1505
|
+
}
|
|
1506
|
+
api = MagicMock()
|
|
1507
|
+
|
|
1508
|
+
upload_evidence_files(evidence_data, 1, "securityplans", api, "users_groups")
|
|
1509
|
+
|
|
1510
|
+
# Should only call upload_evidence_to_controls for non-empty evidence lists
|
|
1511
|
+
mock_upload_evidence.assert_called_once()
|
|
1512
|
+
|
|
1513
|
+
@staticmethod
|
|
1514
|
+
def teardown_class(cls):
|
|
1515
|
+
"""Remove test data"""
|
|
1516
|
+
with contextlib.suppress(FileNotFoundError):
|
|
1517
|
+
shutil.rmtree("./artifacts")
|