regscale-cli 6.25.1.0__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.

Files changed (80) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +18 -3
  4. regscale/core/app/internal/login.py +0 -1
  5. regscale/core/app/utils/catalog_utils/common.py +1 -1
  6. regscale/integrations/commercial/sicura/api.py +14 -13
  7. regscale/integrations/commercial/sicura/commands.py +8 -2
  8. regscale/integrations/commercial/sicura/scanner.py +49 -39
  9. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  10. regscale/integrations/commercial/wizv2/click.py +26 -26
  11. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  12. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  13. regscale/integrations/compliance_integration.py +67 -2
  14. regscale/integrations/control_matcher.py +358 -0
  15. regscale/integrations/milestone_manager.py +291 -0
  16. regscale/integrations/public/__init__.py +1 -0
  17. regscale/integrations/public/cci_importer.py +37 -38
  18. regscale/integrations/public/fedramp/click.py +60 -2
  19. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  20. regscale/integrations/scanner_integration.py +150 -96
  21. regscale/models/integration_models/cisa_kev_data.json +154 -4
  22. regscale/models/integration_models/nexpose.py +36 -10
  23. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  24. regscale/models/locking.py +12 -8
  25. regscale/models/platform.py +1 -2
  26. regscale/models/regscale_models/control_implementation.py +46 -21
  27. regscale/models/regscale_models/issue.py +256 -94
  28. regscale/models/regscale_models/milestone.py +1 -1
  29. regscale/models/regscale_models/regscale_model.py +6 -1
  30. regscale/templates/__init__.py +0 -0
  31. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  32. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
  33. tests/regscale/integrations/commercial/__init__.py +0 -0
  34. tests/regscale/integrations/commercial/conftest.py +28 -0
  35. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  36. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  37. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  38. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  40. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  41. tests/regscale/integrations/commercial/test_burp.py +48 -0
  42. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  43. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  44. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  45. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  46. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  47. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  48. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  49. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  50. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  51. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  52. tests/regscale/integrations/commercial/test_snow.py +423 -0
  53. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  54. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  55. tests/regscale/integrations/commercial/test_stig.py +33 -0
  56. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  57. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  58. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  59. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  60. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  61. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  62. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  63. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  64. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  65. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  66. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  71. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  72. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  73. tests/regscale/integrations/test_control_matcher.py +1314 -0
  74. tests/regscale/integrations/test_control_matching.py +155 -0
  75. tests/regscale/integrations/test_milestone_manager.py +408 -0
  76. tests/regscale/models/test_issue.py +378 -1
  77. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  78. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  79. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  80. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests for Microsoft Defender Scanner integration"""
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from regscale.integrations.commercial.microsoft_defender.defender_api import DefenderApi
7
+ from regscale.integrations.commercial.microsoft_defender.defender_scanner import DefenderScanner
8
+ from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
9
+ from regscale.models import IssueSeverity
10
+ from tests import CLITestFixture
11
+
12
+ PATH = "regscale.integrations.commercial.microsoft_defender.defender_scanner"
13
+
14
+
15
+ class TestDefenderScanner(CLITestFixture):
16
+
17
+ def test_init(self):
18
+ """Test init file and config"""
19
+ self.verify_config(
20
+ [
21
+ "azureCloudSubscriptionId",
22
+ ]
23
+ )
24
+
25
+ @patch(f"{PATH}.DefenderApi")
26
+ def test_defender_scanner_init_default_system(self, mock_defender_api_class):
27
+ """Test DefenderScanner initialization with default system"""
28
+ mock_api = MagicMock(spec=DefenderApi)
29
+ mock_defender_api_class.return_value = mock_api
30
+
31
+ scanner = DefenderScanner(plan_id=1, is_component=False)
32
+
33
+ assert scanner.system == "cloud"
34
+ assert scanner.title == "Microsoft Defender for Cloud"
35
+ assert scanner.asset_identifier_field == "otherTrackingNumber"
36
+ assert scanner.api == mock_api
37
+ mock_defender_api_class.assert_called_once_with(system="cloud")
38
+
39
+ @patch(f"{PATH}.DefenderApi")
40
+ def test_defender_scanner_init_custom_system(self, mock_defender_api_class):
41
+ """Test DefenderScanner initialization with custom system"""
42
+ mock_api = MagicMock(spec=DefenderApi)
43
+ mock_defender_api_class.return_value = mock_api
44
+
45
+ scanner = DefenderScanner(system="365", plan_id=1, is_component=False)
46
+
47
+ assert scanner.system == "365"
48
+ assert scanner.api == mock_api
49
+ mock_defender_api_class.assert_called_once_with(system="365")
50
+
51
+ def test_finding_severity_map(self):
52
+ """Test that severity mapping is correctly defined"""
53
+ scanner = DefenderScanner(plan_id=1, is_component=False)
54
+
55
+ expected_mapping = {
56
+ "Critical": IssueSeverity.Critical,
57
+ "High": IssueSeverity.High,
58
+ "Medium": IssueSeverity.Moderate,
59
+ "Low": IssueSeverity.Low,
60
+ }
61
+
62
+ assert scanner.finding_severity_map == expected_mapping
63
+
64
+ @patch(f"{PATH}.DefenderApi")
65
+ def test_fetch_assets(self, mock_defender_api_class):
66
+ """Test fetching assets from Defender"""
67
+ mock_api = MagicMock(spec=DefenderApi)
68
+ mock_api.config = {"azureCloudSubscriptionId": "test-subscription"}
69
+ mock_api.execute_resource_graph_query.return_value = [
70
+ {
71
+ "resourceId": "/subscriptions/test/vm1",
72
+ "resourceName": "test-vm",
73
+ "resourceType": "microsoft.compute/virtualmachines",
74
+ "resourceLocation": "eastus",
75
+ "resourceGroup": "test-rg",
76
+ "ipAddress": "10.0.0.1",
77
+ "properties": {"osProfile": {"computerName": "test-vm"}},
78
+ },
79
+ {
80
+ "resourceId": "/subscriptions/test/storage1",
81
+ "resourceName": "test-storage",
82
+ "resourceType": "microsoft.storage/storageaccounts",
83
+ "resourceLocation": "westus",
84
+ "resourceGroup": "test-rg",
85
+ "ipAddress": "",
86
+ "properties": {"primaryEndpoints": {"blob": "https://test.blob.core.windows.net"}},
87
+ },
88
+ ]
89
+ mock_defender_api_class.return_value = mock_api
90
+
91
+ scanner = DefenderScanner(plan_id=1, is_component=False)
92
+
93
+ assets = list(scanner.fetch_assets())
94
+
95
+ assert len(assets) == 2
96
+ assert scanner.num_assets_to_process == 2
97
+
98
+ # Check first asset (VM)
99
+ vm_asset = assets[0]
100
+ assert isinstance(vm_asset, IntegrationAsset)
101
+ assert vm_asset.name == "test-vm"
102
+ assert vm_asset.other_tracking_number == "/subscriptions/test/vm1"
103
+ assert vm_asset.ip_address == "10.0.0.1"
104
+
105
+ # Check second asset (Storage)
106
+ storage_asset = assets[1]
107
+ assert isinstance(storage_asset, IntegrationAsset)
108
+ assert storage_asset.name == "test-storage"
109
+ assert storage_asset.other_tracking_number == "/subscriptions/test/storage1"
110
+ assert storage_asset.fqdn == "https://test.blob.core.windows.net"
111
+
112
+ @patch(f"{PATH}.DefenderApi")
113
+ def test_fetch_findings(self, mock_defender_api_class):
114
+ """Test fetching findings"""
115
+ mock_api = MagicMock(spec=DefenderApi)
116
+ mock_defender_api_class.return_value = mock_api
117
+
118
+ scanner = DefenderScanner(plan_id=1, is_component=False)
119
+
120
+ mock_findings = [MagicMock(spec=IntegrationFinding), MagicMock(spec=IntegrationFinding)]
121
+
122
+ findings = list(scanner.fetch_findings(integration_findings=mock_findings))
123
+
124
+ assert len(findings) == 2
125
+ assert all(isinstance(f, IntegrationFinding) for f in findings)
126
+
127
+ @patch(f"{PATH}.DefenderApi")
128
+ def test_parse_asset_vm(self, mock_defender_api_class):
129
+ """Test parsing a virtual machine asset"""
130
+ mock_api = MagicMock(spec=DefenderApi)
131
+ mock_defender_api_class.return_value = mock_api
132
+
133
+ scanner = DefenderScanner(plan_id=1, is_component=False)
134
+
135
+ defender_asset = {
136
+ "resourceId": "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm",
137
+ "resourceName": "test-vm",
138
+ "resourceType": "microsoft.compute/virtualmachines",
139
+ "resourceLocation": "eastus",
140
+ "resourceGroup": "test-rg",
141
+ "ipAddress": "10.0.0.1",
142
+ "properties": {
143
+ "osProfile": {"computerName": "test-vm"},
144
+ "networkProfile": {"networkInterfaces": [{"id": "/subscriptions/test/nic1"}]},
145
+ },
146
+ }
147
+
148
+ asset = scanner.parse_asset(defender_asset)
149
+
150
+ assert isinstance(asset, IntegrationAsset)
151
+ assert asset.name == "test-vm"
152
+ assert (
153
+ asset.other_tracking_number
154
+ == "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm"
155
+ )
156
+ assert (
157
+ asset.azure_identifier
158
+ == "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm"
159
+ )
160
+ assert asset.ip_address == "10.0.0.1"
161
+ assert asset.is_virtual is True
162
+ assert asset.baseline_configuration == "Azure Hardening Guide"
163
+ assert "microsoft.compute/virtualmachines" in asset.component_names
164
+
165
+ @patch(f"{PATH}.DefenderApi")
166
+ def test_parse_asset_storage_account(self, mock_defender_api_class):
167
+ """Test parsing a storage account asset"""
168
+ mock_api = MagicMock(spec=DefenderApi)
169
+ mock_defender_api_class.return_value = mock_api
170
+
171
+ scanner = DefenderScanner(plan_id=1, is_component=False)
172
+
173
+ defender_asset = {
174
+ "resourceId": "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorage",
175
+ "resourceName": "teststorage",
176
+ "resourceType": "microsoft.storage/storageaccounts",
177
+ "resourceLocation": "westus",
178
+ "resourceGroup": "test-rg",
179
+ "ipAddress": "",
180
+ "properties": {"primaryEndpoints": {"blob": "https://teststorage.blob.core.windows.net"}},
181
+ }
182
+
183
+ asset = scanner.parse_asset(defender_asset)
184
+
185
+ assert isinstance(asset, IntegrationAsset)
186
+ assert asset.name == "teststorage"
187
+ assert asset.fqdn == "https://teststorage.blob.core.windows.net"
188
+ assert asset.software_function == "Storage blob to house unstructured files uploaded to the platform"
189
+ assert not asset.is_public_facing # Storage account is not public facing by default
190
+
191
+ @patch(f"{PATH}.DefenderApi")
192
+ def test_parse_asset_cdn_profile(self, mock_defender_api_class):
193
+ """Test parsing a CDN profile asset"""
194
+ mock_api = MagicMock(spec=DefenderApi)
195
+ mock_defender_api_class.return_value = mock_api
196
+
197
+ scanner = DefenderScanner(plan_id=1, is_component=False)
198
+
199
+ defender_asset = {
200
+ "resourceId": "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Cdn/profiles/testcdn",
201
+ "resourceName": "testcdn",
202
+ "resourceType": "microsoft.cdn/profiles",
203
+ "resourceLocation": "global",
204
+ "resourceGroup": "test-rg",
205
+ "ipAddress": "",
206
+ "properties": {},
207
+ }
208
+
209
+ asset = scanner.parse_asset(defender_asset)
210
+
211
+ assert isinstance(asset, IntegrationAsset)
212
+ assert asset.name == "testcdn"
213
+ assert asset.is_public_facing is True # CDN profiles are public facing
214
+ assert asset.software_function.startswith("Monitoring and controlling inbound and outbound traffic")
215
+
216
+ @patch(f"{PATH}.DefenderApi")
217
+ def test_parse_asset_network_security_group(self, mock_defender_api_class):
218
+ """Test parsing a network security group asset"""
219
+ mock_api = MagicMock(spec=DefenderApi)
220
+ mock_defender_api_class.return_value = mock_api
221
+
222
+ scanner = DefenderScanner(plan_id=1, is_component=False)
223
+
224
+ defender_asset = {
225
+ "resourceId": "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg",
226
+ "resourceName": "test-nsg",
227
+ "resourceType": "microsoft.network/networksecuritygroups",
228
+ "resourceLocation": "eastus",
229
+ "resourceGroup": "test-rg",
230
+ "ipAddress": "",
231
+ "properties": {"securityRules": [{"properties": {"destinationAddressPrefix": "10.0.0.0/24"}}]},
232
+ }
233
+
234
+ asset = scanner.parse_asset(defender_asset)
235
+
236
+ assert isinstance(asset, IntegrationAsset)
237
+ assert asset.name == "test-nsg"
238
+ assert asset.ip_address == "10.0.0.0/24"
239
+ assert asset.software_function == "Network protection for internal communications and load balancing"
240
+
241
+ @patch(f"{PATH}.DefenderApi")
242
+ def test_parse_asset_key_vault(self, mock_defender_api_class):
243
+ """Test parsing a key vault asset"""
244
+ mock_api = MagicMock(spec=DefenderApi)
245
+ mock_defender_api_class.return_value = mock_api
246
+
247
+ scanner = DefenderScanner(plan_id=1, is_component=False)
248
+
249
+ defender_asset = {
250
+ "resourceId": "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/testkv",
251
+ "resourceName": "testkv",
252
+ "resourceType": "microsoft.keyvault/vaults",
253
+ "resourceLocation": "eastus",
254
+ "resourceGroup": "test-rg",
255
+ "ipAddress": "",
256
+ "properties": {"vaultUri": "https://testkv.vault.azure.net/"},
257
+ }
258
+
259
+ asset = scanner.parse_asset(defender_asset)
260
+
261
+ assert isinstance(asset, IntegrationAsset)
262
+ assert asset.name == "testkv"
263
+ assert asset.fqdn == "https://testkv.vault.azure.net/"
264
+ assert asset.software_function == "To securely store API keys, passwords, certificates, or cryptographic keys"
265
+
266
+ @patch(f"{PATH}.DefenderApi")
267
+ def test_parse_asset_with_component_parent(self, mock_defender_api_class):
268
+ """Test parsing asset with component as parent"""
269
+ mock_api = MagicMock(spec=DefenderApi)
270
+ mock_defender_api_class.return_value = mock_api
271
+
272
+ scanner = DefenderScanner(plan_id=1, is_component=True)
273
+
274
+ defender_asset = {
275
+ "resourceId": "/subscriptions/test/vm1",
276
+ "resourceName": "test-vm",
277
+ "resourceType": "microsoft.compute/virtualmachines",
278
+ "resourceLocation": "eastus",
279
+ "resourceGroup": "test-rg",
280
+ "ipAddress": "10.0.0.1",
281
+ "properties": {},
282
+ }
283
+
284
+ with patch("regscale.models.regscale_models") as mock_models:
285
+ mock_models.Component.get_module_slug.return_value = "components"
286
+ mock_models.SecurityPlan.get_module_slug.return_value = "securityPlans"
287
+
288
+ asset = scanner.parse_asset(defender_asset)
289
+
290
+ assert asset.parent_module == "components"
291
+ mock_models.Component.get_module_slug.assert_called_once()
292
+
293
+ @patch(f"{PATH}.DefenderApi")
294
+ def test_parse_asset_with_security_plan_parent(self, mock_defender_api_class):
295
+ """Test parsing asset with security plan as parent"""
296
+ mock_api = MagicMock(spec=DefenderApi)
297
+ mock_defender_api_class.return_value = mock_api
298
+
299
+ scanner = DefenderScanner(plan_id=1, is_component=False)
300
+
301
+ defender_asset = {
302
+ "resourceId": "/subscriptions/test/vm1",
303
+ "resourceName": "test-vm",
304
+ "resourceType": "microsoft.compute/virtualmachines",
305
+ "resourceLocation": "eastus",
306
+ "resourceGroup": "test-rg",
307
+ "ipAddress": "10.0.0.1",
308
+ "properties": {},
309
+ }
310
+
311
+ with patch("regscale.models.regscale_models") as mock_models:
312
+ mock_models.Component.get_module_slug.return_value = "components"
313
+ mock_models.SecurityPlan.get_module_slug.return_value = "securityPlans"
314
+
315
+ asset = scanner.parse_asset(defender_asset)
316
+
317
+ assert asset.parent_module == "securityPlans"
318
+ mock_models.SecurityPlan.get_module_slug.assert_called_once()
319
+
320
+ @patch(f"{PATH}.DefenderApi")
321
+ def test_parse_asset_empty_ip_mapping(self, mock_defender_api_class):
322
+ """Test parsing asset when IP mapping fails due to IndexError"""
323
+ mock_api = MagicMock(spec=DefenderApi)
324
+ mock_defender_api_class.return_value = mock_api
325
+
326
+ scanner = DefenderScanner(plan_id=1, is_component=False)
327
+
328
+ defender_asset = {
329
+ "resourceId": "/subscriptions/test/vm1",
330
+ "resourceName": "test-vm",
331
+ "resourceType": "microsoft.network/networksecuritygroups",
332
+ "resourceLocation": "eastus",
333
+ "resourceGroup": "test-rg",
334
+ "ipAddress": "",
335
+ "properties": {"securityRules": []}, # Empty list will cause IndexError
336
+ }
337
+
338
+ asset = scanner.parse_asset(defender_asset)
339
+
340
+ assert isinstance(asset, IntegrationAsset)
341
+ assert asset.name == "test-vm"
342
+ assert asset.ip_address is None
343
+
344
+ @patch(f"{PATH}.DefenderApi")
345
+ def test_parse_asset_empty_fqdn_mapping(self, mock_defender_api_class):
346
+ """Test parsing asset when FQDN mapping fails due to IndexError"""
347
+ mock_api = MagicMock(spec=DefenderApi)
348
+ mock_defender_api_class.return_value = mock_api
349
+
350
+ scanner = DefenderScanner(plan_id=1, is_component=False)
351
+
352
+ defender_asset = {
353
+ "resourceId": "/subscriptions/test/app1",
354
+ "resourceName": "test-app",
355
+ "resourceType": "microsoft.app/containerapps",
356
+ "resourceLocation": "eastus",
357
+ "resourceGroup": "test-rg",
358
+ "ipAddress": "",
359
+ "properties": {"configuration": {"ingress": {}}}, # Missing fqdn will cause IndexError in complex mapping
360
+ }
361
+
362
+ asset = scanner.parse_asset(defender_asset)
363
+
364
+ assert isinstance(asset, IntegrationAsset)
365
+ assert asset.name == "test-app"
366
+ # Should not have fqdn set since mapping failed
367
+ assert not hasattr(asset, "fqdn") or asset.fqdn is None
368
+
369
+ @patch(f"{PATH}.DefenderApi")
370
+ def test_parse_asset_with_custom_description(self, mock_defender_api_class):
371
+ """Test parsing asset with custom description from properties"""
372
+ mock_api = MagicMock(spec=DefenderApi)
373
+ mock_defender_api_class.return_value = mock_api
374
+
375
+ scanner = DefenderScanner(plan_id=1, is_component=False)
376
+
377
+ defender_asset = {
378
+ "resourceId": "/subscriptions/test/custom1",
379
+ "resourceName": "test-custom",
380
+ "resourceType": "custom.provider/resources", # Unknown type
381
+ "resourceLocation": "eastus",
382
+ "resourceGroup": "test-rg",
383
+ "ipAddress": "",
384
+ "properties": {"description": "Custom resource description"},
385
+ }
386
+
387
+ with patch(f"{PATH}.generate_html_table_from_dict") as mock_generate_table:
388
+ mock_generate_table.return_value = "<table>Asset details</table>"
389
+
390
+ asset = scanner.parse_asset(defender_asset)
391
+
392
+ assert asset.software_function == "Custom resource description"
393
+ mock_generate_table.assert_called_once_with(defender_asset)
394
+
395
+ @patch(f"{PATH}.DefenderApi")
396
+ def test_parse_asset_afd_endpoints(self, mock_defender_api_class):
397
+ """Test parsing AFD endpoints asset type"""
398
+ mock_api = MagicMock(spec=DefenderApi)
399
+ mock_defender_api_class.return_value = mock_api
400
+
401
+ scanner = DefenderScanner(plan_id=1, is_component=False)
402
+
403
+ defender_asset = {
404
+ "resourceId": "/subscriptions/test/afd1",
405
+ "resourceName": "test-afd",
406
+ "resourceType": "microsoft.cdn/profiles/afdendpoints",
407
+ "resourceLocation": "global",
408
+ "resourceGroup": "test-rg",
409
+ "ipAddress": "",
410
+ "properties": {"hostName": "test.azurefd.net"},
411
+ }
412
+
413
+ asset = scanner.parse_asset(defender_asset)
414
+
415
+ assert isinstance(asset, IntegrationAsset)
416
+ assert asset.name == "test-afd"
417
+ assert asset.is_public_facing is True # AFD endpoints are public facing
418
+ assert asset.fqdn == "test.azurefd.net"
419
+ assert asset.software_function == "Endpoint that all of the routes attach to"
420
+
421
+ @patch(f"{PATH}.DefenderApi")
422
+ def test_parse_asset_action_rules_not_authenticated(self, mock_defender_api_class):
423
+ """Test parsing assets that are not authenticated scans"""
424
+ mock_api = MagicMock(spec=DefenderApi)
425
+ mock_defender_api_class.return_value = mock_api
426
+
427
+ scanner = DefenderScanner(plan_id=1, is_component=False)
428
+
429
+ # Test action rules
430
+ defender_asset = {
431
+ "resourceId": "/subscriptions/test/action1",
432
+ "resourceName": "test-action",
433
+ "resourceType": "microsoft.alertsmanagement/actionrules",
434
+ "resourceLocation": "global",
435
+ "resourceGroup": "test-rg",
436
+ "ipAddress": "",
437
+ "properties": {},
438
+ }
439
+
440
+ asset = scanner.parse_asset(defender_asset)
441
+
442
+ assert asset.is_authenticated_scan is False
443
+
444
+ @patch(f"{PATH}.DefenderApi")
445
+ def test_parse_asset_smart_detector_not_authenticated(self, mock_defender_api_class):
446
+ """Test parsing smart detector alert rules - not authenticated scans"""
447
+ mock_api = MagicMock(spec=DefenderApi)
448
+ mock_defender_api_class.return_value = mock_api
449
+
450
+ scanner = DefenderScanner(plan_id=1, is_component=False)
451
+
452
+ defender_asset = {
453
+ "resourceId": "/subscriptions/test/smart1",
454
+ "resourceName": "test-smart",
455
+ "resourceType": "microsoft.alertsmanagement/smartdetectoralertrules",
456
+ "resourceLocation": "global",
457
+ "resourceGroup": "test-rg",
458
+ "ipAddress": "",
459
+ "properties": {},
460
+ }
461
+
462
+ asset = scanner.parse_asset(defender_asset)
463
+
464
+ assert asset.is_authenticated_scan is False
465
+
466
+ @patch(f"{PATH}.DefenderApi")
467
+ def test_parse_asset_dns_zone_name_as_fqdn(self, mock_defender_api_class):
468
+ """Test parsing private DNS zone where name is used as FQDN"""
469
+ mock_api = MagicMock(spec=DefenderApi)
470
+ mock_defender_api_class.return_value = mock_api
471
+
472
+ scanner = DefenderScanner(plan_id=1, is_component=False)
473
+
474
+ defender_asset = {
475
+ "resourceId": "/subscriptions/test/dns1",
476
+ "resourceName": "test.private.dns",
477
+ "resourceType": "microsoft.network/privatednszones",
478
+ "resourceLocation": "global",
479
+ "resourceGroup": "test-rg",
480
+ "ipAddress": "",
481
+ "properties": {},
482
+ }
483
+
484
+ asset = scanner.parse_asset(defender_asset)
485
+
486
+ assert asset.fqdn == "test.private.dns"
487
+ assert asset.software_function == "Dns zone that will connect to the private endpoint and network interfaces"