regscale-cli 6.20.9.1__py3-none-any.whl → 6.20.10.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 (29) hide show
  1. regscale/_version.py +1 -1
  2. regscale/integrations/commercial/defender.py +9 -0
  3. regscale/integrations/commercial/wizv2/async_client.py +325 -0
  4. regscale/integrations/commercial/wizv2/constants.py +756 -0
  5. regscale/integrations/commercial/wizv2/scanner.py +1301 -89
  6. regscale/integrations/commercial/wizv2/utils.py +280 -36
  7. regscale/integrations/commercial/wizv2/variables.py +2 -10
  8. regscale/integrations/scanner_integration.py +58 -2
  9. regscale/integrations/variables.py +1 -0
  10. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  11. regscale/models/regscale_models/__init__.py +13 -0
  12. regscale/models/regscale_models/classification.py +23 -0
  13. regscale/models/regscale_models/cryptography.py +56 -0
  14. regscale/models/regscale_models/deviation.py +4 -4
  15. regscale/models/regscale_models/group.py +3 -2
  16. regscale/models/regscale_models/interconnection.py +1 -1
  17. regscale/models/regscale_models/issue.py +140 -41
  18. regscale/models/regscale_models/milestone.py +40 -0
  19. regscale/models/regscale_models/property.py +0 -1
  20. regscale/models/regscale_models/regscale_model.py +29 -18
  21. regscale/models/regscale_models/team.py +55 -0
  22. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/METADATA +1 -1
  23. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/RECORD +29 -23
  24. tests/regscale/integrations/test_property_and_milestone_creation.py +684 -0
  25. tests/regscale/models/test_report.py +105 -29
  26. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/LICENSE +0 -0
  27. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/WHEEL +0 -0
  28. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/entry_points.txt +0 -0
  29. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,684 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Unit tests for the _handle_property_and_milestone_creation method in ScannerIntegration.
5
+
6
+ This module provides comprehensive test coverage for the property and milestone creation
7
+ functionality in scanner integrations.
8
+ """
9
+
10
+ import unittest
11
+ from typing import Iterator, Optional
12
+ from unittest.mock import MagicMock, patch, call
13
+
14
+ import pytest
15
+
16
+ from regscale.integrations.scanner_integration import (
17
+ ScannerIntegration,
18
+ ScannerIntegrationType,
19
+ IntegrationFinding,
20
+ )
21
+ from regscale.integrations.variables import ScannerVariables
22
+ from regscale.models import regscale_models
23
+
24
+
25
+ class TestScannerIntegration(ScannerIntegration):
26
+ """
27
+ A concrete implementation of ScannerIntegration for testing purposes.
28
+ """
29
+
30
+ title = "Test Scanner"
31
+ type = ScannerIntegrationType.VULNERABILITY
32
+
33
+ def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
34
+ """Mock implementation for testing."""
35
+ return iter([])
36
+
37
+ def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
38
+ """Mock implementation for testing."""
39
+ return iter([])
40
+
41
+
42
+ class TestPropertyAndMilestoneCreation:
43
+ """
44
+ Test cases for the _handle_property_and_milestone_creation method.
45
+
46
+ This class provides comprehensive test coverage for all code paths in the method,
47
+ including property creation for POC and CWE, and milestone creation for various
48
+ issue status transitions.
49
+ """
50
+
51
+ @pytest.fixture(autouse=True)
52
+ def setup_scanner(self) -> TestScannerIntegration:
53
+ """
54
+ Set up a test scanner instance with mocked dependencies.
55
+
56
+ :return: Configured test scanner instance
57
+ :rtype: TestScannerIntegration
58
+ """
59
+ scanner = TestScannerIntegration(plan_id=1, tenant_id=1)
60
+ scanner.assessor_id = "test_assessor"
61
+ scanner.scan_date = "2024-01-01 00:00:00"
62
+ return scanner
63
+
64
+ @pytest.fixture
65
+ def mock_issue(self) -> MagicMock:
66
+ """
67
+ Create a mock issue for testing.
68
+
69
+ :return: Mock issue object
70
+ :rtype: MagicMock
71
+ """
72
+ issue = MagicMock(spec=regscale_models.Issue)
73
+ issue.id = 123
74
+ issue.status = regscale_models.IssueStatus.Open
75
+ issue.dateCompleted = "2024-01-02 00:00:00"
76
+ return issue
77
+
78
+ @pytest.fixture
79
+ def mock_finding(self) -> MagicMock:
80
+ """
81
+ Create a mock finding for testing.
82
+
83
+ :return: Mock finding object
84
+ :rtype: MagicMock
85
+ """
86
+ finding = MagicMock(spec=IntegrationFinding)
87
+ finding.external_id = "test_external_id"
88
+ finding.point_of_contact = None
89
+ finding.is_cwe = False
90
+ finding.plugin_id = "CWE-123"
91
+ return finding
92
+
93
+ @pytest.fixture
94
+ def mock_property_class(self) -> MagicMock:
95
+ """
96
+ Create a mock Property class for testing.
97
+
98
+ :return: Mock Property class
99
+ :rtype: MagicMock
100
+ """
101
+ return MagicMock(spec=regscale_models.Property)
102
+
103
+ @pytest.fixture
104
+ def mock_milestone_class(self) -> MagicMock:
105
+ """
106
+ Create a mock Milestone class for testing.
107
+
108
+ :return: Mock Milestone class
109
+ :rtype: MagicMock
110
+ """
111
+ return MagicMock(spec=regscale_models.Milestone)
112
+
113
+ def test_handle_property_and_milestone_creation_no_properties_no_milestones(
114
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
115
+ ):
116
+ """
117
+ Test when no properties or milestones should be created.
118
+
119
+ This test covers the case where:
120
+ - finding.point_of_contact is None
121
+ - finding.is_cwe is False
122
+ - ScannerVariables.useMilestones is False
123
+ """
124
+ # Arrange
125
+ scanner = setup_scanner
126
+ issue = mock_issue
127
+ finding = mock_finding
128
+
129
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
130
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
131
+ ), patch.object(ScannerVariables, "useMilestones", False):
132
+
133
+ # Act
134
+ scanner._handle_property_and_milestone_creation(issue, finding)
135
+
136
+ # Assert
137
+ mock_property_class.assert_not_called()
138
+ mock_milestone_class.assert_not_called()
139
+
140
+ def test_handle_property_and_milestone_creation_with_poc_property(
141
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
142
+ ):
143
+ """
144
+ Test creation of POC property when point_of_contact is provided.
145
+
146
+ This test covers the case where:
147
+ - finding.point_of_contact has a value
148
+ - finding.is_cwe is False
149
+ - ScannerVariables.useMilestones is False
150
+ """
151
+ # Arrange
152
+ scanner = setup_scanner
153
+ issue = mock_issue
154
+ finding = mock_finding
155
+ finding.point_of_contact = "test_poc@example.com"
156
+
157
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
158
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
159
+ ), patch.object(ScannerVariables, "useMilestones", False):
160
+
161
+ # Act
162
+ scanner._handle_property_and_milestone_creation(issue, finding)
163
+
164
+ # Assert
165
+ mock_property_class.assert_called_once_with(
166
+ key="POC",
167
+ value="test_poc@example.com",
168
+ parentId=123,
169
+ parentModule="issues",
170
+ )
171
+ mock_property_class.return_value.create_or_update.assert_called_once()
172
+ mock_milestone_class.assert_not_called()
173
+
174
+ def test_handle_property_and_milestone_creation_with_cwe_property(
175
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
176
+ ):
177
+ """
178
+ Test creation of CWE property when is_cwe is True.
179
+
180
+ This test covers the case where:
181
+ - finding.point_of_contact is None
182
+ - finding.is_cwe is True
183
+ - ScannerVariables.useMilestones is False
184
+ """
185
+ # Arrange
186
+ scanner = setup_scanner
187
+ issue = mock_issue
188
+ finding = mock_finding
189
+ finding.is_cwe = True
190
+
191
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
192
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
193
+ ), patch.object(ScannerVariables, "useMilestones", False):
194
+
195
+ # Act
196
+ scanner._handle_property_and_milestone_creation(issue, finding)
197
+
198
+ # Assert
199
+ mock_property_class.assert_called_once_with(
200
+ key="CWE",
201
+ value="CWE-123",
202
+ parentId=123,
203
+ parentModule="issues",
204
+ )
205
+ mock_property_class.return_value.create_or_update.assert_called_once()
206
+ mock_milestone_class.assert_not_called()
207
+
208
+ def test_handle_property_and_milestone_creation_with_both_properties(
209
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
210
+ ):
211
+ """
212
+ Test creation of both POC and CWE properties.
213
+
214
+ This test covers the case where:
215
+ - finding.point_of_contact has a value
216
+ - finding.is_cwe is True
217
+ - ScannerVariables.useMilestones is False
218
+ """
219
+ # Arrange
220
+ scanner = setup_scanner
221
+ issue = mock_issue
222
+ finding = mock_finding
223
+ finding.point_of_contact = "test_poc@example.com"
224
+ finding.is_cwe = True
225
+
226
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
227
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
228
+ ), patch.object(ScannerVariables, "useMilestones", False):
229
+
230
+ # Act
231
+ scanner._handle_property_and_milestone_creation(issue, finding)
232
+
233
+ # Assert
234
+ # Check that Property was called twice (once for each property)
235
+ assert mock_property_class.call_count == 2
236
+
237
+ # Check the first call (POC property)
238
+ first_call = mock_property_class.call_args_list[0]
239
+ assert first_call[1]["key"] == "POC"
240
+ assert first_call[1]["value"] == "test_poc@example.com"
241
+ assert first_call[1]["parentId"] == 123
242
+ assert first_call[1]["parentModule"] == "issues"
243
+
244
+ # Check the second call (CWE property)
245
+ second_call = mock_property_class.call_args_list[1]
246
+ assert second_call[1]["key"] == "CWE"
247
+ assert second_call[1]["value"] == "CWE-123"
248
+ assert second_call[1]["parentId"] == 123
249
+ assert second_call[1]["parentModule"] == "issues"
250
+
251
+ # Verify create_or_update was called for each property
252
+ assert mock_property_class.return_value.create_or_update.call_count == 2
253
+ mock_milestone_class.assert_not_called()
254
+
255
+ def test_handle_property_and_milestone_creation_milestones_disabled(
256
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
257
+ ):
258
+ """
259
+ Test that milestones are not created when useMilestones is False.
260
+
261
+ This test covers the case where:
262
+ - ScannerVariables.useMilestones is False
263
+ - All milestone creation logic should be skipped
264
+ """
265
+ # Arrange
266
+ scanner = setup_scanner
267
+ issue = mock_issue
268
+ finding = mock_finding
269
+
270
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
271
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
272
+ ), patch.object(ScannerVariables, "useMilestones", False):
273
+
274
+ # Act
275
+ scanner._handle_property_and_milestone_creation(issue, finding)
276
+
277
+ # Assert
278
+ mock_milestone_class.assert_not_called()
279
+
280
+ def test_handle_property_and_milestone_creation_issue_reopened_milestone(
281
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
282
+ ):
283
+ """
284
+ Test creation of milestone when issue is reopened.
285
+
286
+ This test covers the case where:
287
+ - ScannerVariables.useMilestones is True
288
+ - existing_issue.status is Closed
289
+ - issue.status is Open
290
+ """
291
+ # Arrange
292
+ scanner = setup_scanner
293
+ issue = mock_issue
294
+ issue.status = regscale_models.IssueStatus.Open
295
+ finding = mock_finding
296
+
297
+ existing_issue = MagicMock(spec=regscale_models.Issue)
298
+ existing_issue.status = regscale_models.IssueStatus.Closed
299
+
300
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
301
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
302
+ ), patch(
303
+ "regscale.integrations.scanner_integration.get_current_datetime", return_value="2024-01-03 00:00:00"
304
+ ), patch.object(
305
+ ScannerVariables, "useMilestones", True
306
+ ):
307
+
308
+ # Act
309
+ scanner._handle_property_and_milestone_creation(issue, finding, existing_issue)
310
+
311
+ # Assert
312
+ mock_milestone_class.assert_called_once_with(
313
+ title="Issue reopened from Test Scanner scan",
314
+ milestoneDate="2024-01-03 00:00:00",
315
+ responsiblePersonId="test_assessor",
316
+ parentID=123,
317
+ parentModule="issues",
318
+ )
319
+ mock_milestone_class.return_value.create_or_update.assert_called_once()
320
+
321
+ def test_handle_property_and_milestone_creation_issue_closed_milestone(
322
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
323
+ ):
324
+ """
325
+ Test creation of milestone when issue is closed.
326
+
327
+ This test covers the case where:
328
+ - ScannerVariables.useMilestones is True
329
+ - existing_issue.status is Open
330
+ - issue.status is Closed
331
+ """
332
+ # Arrange
333
+ scanner = setup_scanner
334
+ issue = mock_issue
335
+ issue.status = regscale_models.IssueStatus.Closed
336
+ issue.dateCompleted = "2024-01-02 00:00:00"
337
+ finding = mock_finding
338
+
339
+ existing_issue = MagicMock(spec=regscale_models.Issue)
340
+ existing_issue.status = regscale_models.IssueStatus.Open
341
+
342
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
343
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
344
+ ), patch.object(ScannerVariables, "useMilestones", True):
345
+
346
+ # Act
347
+ scanner._handle_property_and_milestone_creation(issue, finding, existing_issue)
348
+
349
+ # Assert
350
+ mock_milestone_class.assert_called_once_with(
351
+ title="Issue closed from Test Scanner scan",
352
+ milestoneDate="2024-01-02 00:00:00",
353
+ responsiblePersonId="test_assessor",
354
+ parentID=123,
355
+ parentModule="issues",
356
+ )
357
+ mock_milestone_class.return_value.create_or_update.assert_called_once()
358
+
359
+ def test_handle_property_and_milestone_creation_new_issue_milestone(
360
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
361
+ ):
362
+ """
363
+ Test creation of milestone for a new issue.
364
+
365
+ This test covers the case where:
366
+ - ScannerVariables.useMilestones is True
367
+ - existing_issue is None (new issue)
368
+ """
369
+ # Arrange
370
+ scanner = setup_scanner
371
+ issue = mock_issue
372
+ finding = mock_finding
373
+
374
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
375
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
376
+ ), patch.object(ScannerVariables, "useMilestones", True):
377
+
378
+ # Act
379
+ scanner._handle_property_and_milestone_creation(issue, finding)
380
+
381
+ # Assert
382
+ mock_milestone_class.assert_called_once_with(
383
+ title="Issue created from Test Scanner scan",
384
+ milestoneDate="2024-01-01 00:00:00",
385
+ responsiblePersonId="test_assessor",
386
+ parentID=123,
387
+ parentModule="issues",
388
+ )
389
+ mock_milestone_class.return_value.create_or_update.assert_called_once()
390
+
391
+ def test_handle_property_and_milestone_creation_no_milestone_created(
392
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
393
+ ):
394
+ """
395
+ Test case where no milestone is created due to status conditions.
396
+
397
+ This test covers the case where:
398
+ - ScannerVariables.useMilestones is True
399
+ - existing_issue.status is Open
400
+ - issue.status is Open (no status change)
401
+ """
402
+ # Arrange
403
+ scanner = setup_scanner
404
+ issue = mock_issue
405
+ issue.status = regscale_models.IssueStatus.Open
406
+ finding = mock_finding
407
+
408
+ existing_issue = MagicMock(spec=regscale_models.Issue)
409
+ existing_issue.status = regscale_models.IssueStatus.Open
410
+
411
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
412
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
413
+ ), patch.object(ScannerVariables, "useMilestones", True):
414
+
415
+ # Act
416
+ scanner._handle_property_and_milestone_creation(issue, finding, existing_issue)
417
+
418
+ # Assert
419
+ mock_milestone_class.assert_not_called()
420
+
421
+ def test_handle_property_and_milestone_creation_comprehensive_scenario(
422
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
423
+ ):
424
+ """
425
+ Test comprehensive scenario with all properties and milestone creation.
426
+
427
+ This test covers the case where:
428
+ - finding.point_of_contact has a value
429
+ - finding.is_cwe is True
430
+ - ScannerVariables.useMilestones is True
431
+ - Issue is being reopened (milestone creation)
432
+ """
433
+ # Arrange
434
+ scanner = setup_scanner
435
+ issue = mock_issue
436
+ issue.status = regscale_models.IssueStatus.Open
437
+ finding = mock_finding
438
+ finding.point_of_contact = "test_poc@example.com"
439
+ finding.is_cwe = True
440
+
441
+ existing_issue = MagicMock(spec=regscale_models.Issue)
442
+ existing_issue.status = regscale_models.IssueStatus.Closed
443
+
444
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
445
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
446
+ ), patch(
447
+ "regscale.integrations.scanner_integration.get_current_datetime", return_value="2024-01-03 00:00:00"
448
+ ), patch.object(
449
+ ScannerVariables, "useMilestones", True
450
+ ):
451
+
452
+ # Act
453
+ scanner._handle_property_and_milestone_creation(issue, finding, existing_issue)
454
+
455
+ # Assert
456
+ # Check that Property was called twice (once for each property)
457
+ assert mock_property_class.call_count == 2
458
+
459
+ # Check the first call (POC property)
460
+ first_call = mock_property_class.call_args_list[0]
461
+ assert first_call[1]["key"] == "POC"
462
+ assert first_call[1]["value"] == "test_poc@example.com"
463
+ assert first_call[1]["parentId"] == 123
464
+ assert first_call[1]["parentModule"] == "issues"
465
+
466
+ # Check the second call (CWE property)
467
+ second_call = mock_property_class.call_args_list[1]
468
+ assert second_call[1]["key"] == "CWE"
469
+ assert second_call[1]["value"] == "CWE-123"
470
+ assert second_call[1]["parentId"] == 123
471
+ assert second_call[1]["parentModule"] == "issues"
472
+
473
+ # Check milestone creation call
474
+ mock_milestone_class.assert_called_once_with(
475
+ title="Issue reopened from Test Scanner scan",
476
+ milestoneDate="2024-01-03 00:00:00",
477
+ responsiblePersonId="test_assessor",
478
+ parentID=123,
479
+ parentModule="issues",
480
+ )
481
+
482
+ # Verify create_or_update was called for all created objects
483
+ assert mock_property_class.return_value.create_or_update.call_count == 2
484
+ mock_milestone_class.return_value.create_or_update.assert_called_once()
485
+
486
+ def test_handle_property_and_milestone_creation_with_logging(
487
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
488
+ ):
489
+ """
490
+ Test that logging occurs when properties and milestones are created.
491
+
492
+ This test verifies that debug logging is called appropriately.
493
+ """
494
+ # Arrange
495
+ scanner = setup_scanner
496
+ issue = mock_issue
497
+ finding = mock_finding
498
+ finding.point_of_contact = "test_poc@example.com"
499
+ finding.is_cwe = True
500
+
501
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
502
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
503
+ ), patch("regscale.integrations.scanner_integration.logger") as mock_logger, patch.object(
504
+ ScannerVariables, "useMilestones", True
505
+ ):
506
+
507
+ # Act
508
+ scanner._handle_property_and_milestone_creation(issue, finding)
509
+
510
+ # Assert
511
+ # Verify debug logging calls
512
+ expected_log_calls = [
513
+ call.debug("Added POC property %s to issue %s", "test_poc@example.com", 123),
514
+ call.debug("Added CWE property %s to issue %s", "CWE-123", 123),
515
+ call.debug("Created milestone for issue %s from finding %s", 123, "test_external_id"),
516
+ ]
517
+ mock_logger.assert_has_calls(expected_log_calls)
518
+
519
+ def test_handle_property_and_milestone_creation_no_milestone_logging(
520
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
521
+ ):
522
+ """
523
+ Test logging when no milestone is created.
524
+
525
+ This test verifies that the appropriate debug log is called when no milestone
526
+ is created due to status conditions.
527
+ """
528
+ # Arrange
529
+ scanner = setup_scanner
530
+ issue = mock_issue
531
+ issue.status = regscale_models.IssueStatus.Open
532
+ finding = mock_finding
533
+
534
+ existing_issue = MagicMock(spec=regscale_models.Issue)
535
+ existing_issue.status = regscale_models.IssueStatus.Open
536
+
537
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
538
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
539
+ ), patch("regscale.integrations.scanner_integration.logger") as mock_logger, patch.object(
540
+ ScannerVariables, "useMilestones", True
541
+ ):
542
+
543
+ # Act
544
+ scanner._handle_property_and_milestone_creation(issue, finding, existing_issue)
545
+
546
+ # Assert
547
+ mock_logger.debug.assert_called_with(
548
+ "No milestone created for issue %s from finding %s", 123, "test_external_id"
549
+ )
550
+
551
+ def test_handle_property_and_milestone_creation_edge_cases(
552
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
553
+ ):
554
+ """
555
+ Test edge cases and boundary conditions.
556
+
557
+ This test covers various edge cases to ensure robust behavior.
558
+ """
559
+ # Arrange
560
+ scanner = setup_scanner
561
+ issue = mock_issue
562
+ finding = mock_finding
563
+
564
+ # Test with empty string values
565
+ finding.point_of_contact = ""
566
+ finding.is_cwe = True
567
+
568
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
569
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
570
+ ), patch.object(ScannerVariables, "useMilestones", False):
571
+
572
+ # Act
573
+ scanner._handle_property_and_milestone_creation(issue, finding)
574
+
575
+ # Assert
576
+ # Should only create CWE property (empty string is falsy for POC)
577
+ mock_property_class.assert_called_once_with(
578
+ key="CWE",
579
+ value="CWE-123",
580
+ parentId=123,
581
+ parentModule="issues",
582
+ )
583
+
584
+ def test_handle_property_and_milestone_creation_with_none_values(
585
+ self, setup_scanner, mock_issue, mock_finding, mock_property_class, mock_milestone_class
586
+ ):
587
+ """
588
+ Test behavior with None values for optional fields.
589
+
590
+ This test ensures the method handles None values gracefully.
591
+ """
592
+ # Arrange
593
+ scanner = setup_scanner
594
+ issue = mock_issue
595
+ finding = mock_finding
596
+ finding.point_of_contact = None
597
+ finding.is_cwe = False
598
+ finding.plugin_id = None
599
+
600
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
601
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
602
+ ), patch.object(ScannerVariables, "useMilestones", True):
603
+
604
+ # Act
605
+ scanner._handle_property_and_milestone_creation(issue, finding)
606
+
607
+ # Assert
608
+ # Should only create milestone (no properties due to None values)
609
+ mock_property_class.assert_not_called()
610
+ mock_milestone_class.assert_called_once()
611
+
612
+ @pytest.mark.parametrize(
613
+ "issue_status,existing_issue_status,expected_title,expected_date",
614
+ [
615
+ (
616
+ regscale_models.IssueStatus.Open,
617
+ regscale_models.IssueStatus.Closed,
618
+ "Issue reopened from Test Scanner scan",
619
+ "2024-01-03 00:00:00",
620
+ ),
621
+ (
622
+ regscale_models.IssueStatus.Closed,
623
+ regscale_models.IssueStatus.Open,
624
+ "Issue closed from Test Scanner scan",
625
+ "2024-01-02 00:00:00",
626
+ ),
627
+ (regscale_models.IssueStatus.Open, None, "Issue created from Test Scanner scan", "2024-01-01 00:00:00"),
628
+ ],
629
+ )
630
+ def test_handle_property_and_milestone_creation_milestone_status_transitions(
631
+ self,
632
+ setup_scanner,
633
+ mock_issue,
634
+ mock_finding,
635
+ mock_property_class,
636
+ mock_milestone_class,
637
+ issue_status,
638
+ existing_issue_status,
639
+ expected_title,
640
+ expected_date,
641
+ ):
642
+ """
643
+ Test all possible milestone status transitions.
644
+
645
+ This test ensures all milestone creation scenarios are covered.
646
+ """
647
+ # Arrange
648
+ scanner = setup_scanner
649
+ finding = mock_finding
650
+
651
+ # Reset mocks
652
+ mock_property_class.reset_mock()
653
+ mock_milestone_class.reset_mock()
654
+
655
+ # Setup issue
656
+ issue = mock_issue
657
+ issue.status = issue_status
658
+
659
+ # Setup existing issue if needed
660
+ existing_issue = None
661
+ if existing_issue_status is not None:
662
+ existing_issue = MagicMock(spec=regscale_models.Issue)
663
+ existing_issue.status = existing_issue_status
664
+
665
+ with patch("regscale.integrations.scanner_integration.regscale_models.Property", mock_property_class), patch(
666
+ "regscale.integrations.scanner_integration.regscale_models.Milestone", mock_milestone_class
667
+ ), patch(
668
+ "regscale.integrations.scanner_integration.get_current_datetime", return_value="2024-01-03 00:00:00"
669
+ ), patch.object(
670
+ ScannerVariables, "useMilestones", True
671
+ ):
672
+
673
+ # Act
674
+ scanner._handle_property_and_milestone_creation(issue, finding, existing_issue)
675
+
676
+ # Assert
677
+ mock_milestone_class.assert_called_once_with(
678
+ title=expected_title,
679
+ milestoneDate=expected_date,
680
+ responsiblePersonId="test_assessor",
681
+ parentID=123,
682
+ parentModule="issues",
683
+ )
684
+ mock_milestone_class.return_value.create_or_update.assert_called_once()