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.

Files changed (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ """Test control matching for Wiz compliance report against catalog 3."""
3
+
4
+ import logging
5
+ from regscale.integrations.control_matcher import ControlMatcher
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Sample control IDs from Wiz report
12
+ wiz_control_samples = [
13
+ "AC-2(4)",
14
+ "AC-6(9)",
15
+ "AU-12",
16
+ "AU-3(1)",
17
+ "CM-3",
18
+ "SI-4(20)",
19
+ "AC-4(21)",
20
+ "AC-3",
21
+ "AC-17(1)",
22
+ "AC-17(2)",
23
+ "SC-8(1)",
24
+ "SC-28",
25
+ "SC-28(1)",
26
+ "SI-7(12)",
27
+ "PE-19",
28
+ "PS-6",
29
+ "SA-9(5)",
30
+ "SC-7",
31
+ "SI-4",
32
+ "AU-13",
33
+ ]
34
+
35
+
36
+ def test_individual_controls(matcher: ControlMatcher, catalog_id: int) -> tuple:
37
+ """
38
+ Test individual control matching.
39
+
40
+ :param ControlMatcher matcher: Control matcher instance
41
+ :param int catalog_id: Catalog ID to search
42
+ :return: Tuple of (found_controls, not_found_controls)
43
+ :rtype: tuple
44
+ """
45
+ found_controls = []
46
+ not_found_controls = []
47
+
48
+ for control_id in wiz_control_samples:
49
+ logger.info(f"\nTesting: {control_id}")
50
+
51
+ parsed = matcher.parse_control_id(control_id)
52
+ logger.info(f" Parsed as: {parsed}")
53
+
54
+ control = matcher.find_control_in_catalog(control_id, catalog_id)
55
+
56
+ if control:
57
+ logger.info(f" ✓ FOUND in catalog: {control.controlId} (ID: {control.id})")
58
+ found_controls.append(control_id)
59
+ else:
60
+ logger.warning(" ✗ NOT FOUND in catalog")
61
+ not_found_controls.append(control_id)
62
+
63
+ return found_controls, not_found_controls
64
+
65
+
66
+ def log_summary(found_controls: list, not_found_controls: list) -> None:
67
+ """
68
+ Log summary of control matching results.
69
+
70
+ :param list found_controls: List of found controls
71
+ :param list not_found_controls: List of not found controls
72
+ :return: None
73
+ :rtype: None
74
+ """
75
+ logger.info("\n" + "=" * 60)
76
+ logger.info("SUMMARY:")
77
+ logger.info(f"Total tested: {len(wiz_control_samples)}")
78
+ logger.info(f"Found: {len(found_controls)} ({len(found_controls) / len(wiz_control_samples) * 100:.1f}%)")
79
+ logger.info(
80
+ f"Not found: {len(not_found_controls)} ({len(not_found_controls) / len(wiz_control_samples) * 100:.1f}%)"
81
+ )
82
+
83
+ if not_found_controls:
84
+ logger.info("\nControls not found in catalog:")
85
+ for control in not_found_controls:
86
+ logger.info(f" - {control}")
87
+
88
+
89
+ def test_multi_control_parsing() -> None:
90
+ """Test multi-control parsing from Wiz data format."""
91
+ logger.info("\n" + "=" * 60)
92
+ logger.info("Testing multi-control parsing:")
93
+
94
+ multi_control_string = "AC-2(4) Account Management | Automated Audit Actions, AC-6(9) Least Privilege | Log Use of Privileged Functions, AU-12 Audit Record Generation"
95
+ logger.info(f"\nInput: {multi_control_string}")
96
+
97
+ import re
98
+
99
+ control_id_pattern = r"([A-Za-z]{2}-\d+)(?:\s*\(\s*(\d+)\s*\))?"
100
+ matches = []
101
+ for part in multi_control_string.split(", "):
102
+ found = re.findall(control_id_pattern, part.strip())
103
+ for match in found:
104
+ base_control, enhancement = match
105
+ if enhancement:
106
+ matches.append(f"{base_control}({enhancement})")
107
+ else:
108
+ matches.append(base_control)
109
+
110
+ logger.info(f"Extracted control IDs: {matches}")
111
+
112
+
113
+ def test_normalization(matcher: ControlMatcher) -> None:
114
+ """
115
+ Test control ID normalization.
116
+
117
+ :param ControlMatcher matcher: Control matcher instance
118
+ :return: None
119
+ :rtype: None
120
+ """
121
+ logger.info("\n" + "=" * 60)
122
+ logger.info("Testing control ID normalization:")
123
+
124
+ test_cases = [
125
+ ("AC-01", "AC-1"),
126
+ ("AC-1(01)", "AC-1.1"),
127
+ ("AC-2 (4)", "AC-2.4"),
128
+ ("ac-3", "AC-3"),
129
+ ]
130
+
131
+ for original, expected in test_cases:
132
+ parsed = matcher.parse_control_id(original)
133
+ logger.info(f" {original} -> {parsed} (expected: {expected})")
134
+ if parsed == expected:
135
+ logger.info(" ✓ Correct")
136
+ else:
137
+ logger.warning(f" ✗ Mismatch (got {parsed}, expected {expected})")
138
+
139
+
140
+ def test_control_matching():
141
+ """Test control matching against catalog 3."""
142
+ matcher = ControlMatcher()
143
+ catalog_id = 3
144
+
145
+ logger.info(f"Testing control matching for catalog {catalog_id}")
146
+ logger.info("=" * 60)
147
+
148
+ found_controls, not_found_controls = test_individual_controls(matcher, catalog_id)
149
+ log_summary(found_controls, not_found_controls)
150
+ test_multi_control_parsing()
151
+ test_normalization(matcher)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ test_control_matching()
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests for MilestoneManager class."""
4
+ import logging
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from regscale.integrations.milestone_manager import MilestoneManager
10
+ from regscale.integrations.scanner_integration import IntegrationFinding
11
+ from regscale.models import regscale_models
12
+
13
+
14
+ logger = logging.getLogger("regscale")
15
+
16
+
17
+ @pytest.fixture
18
+ def milestone_manager():
19
+ """Create a MilestoneManager instance for testing."""
20
+ return MilestoneManager(
21
+ integration_title="Test Integration",
22
+ assessor_id="test-assessor-id",
23
+ scan_date="2024-01-15T10:00:00Z",
24
+ )
25
+
26
+
27
+ @pytest.fixture
28
+ def mock_issue():
29
+ """Create a mock issue for testing."""
30
+ issue = MagicMock(spec=regscale_models.Issue)
31
+ issue.id = 123
32
+ issue.status = regscale_models.IssueStatus.Open
33
+ issue.dateCompleted = "2024-01-15T12:00:00Z"
34
+ return issue
35
+
36
+
37
+ @pytest.fixture
38
+ def mock_finding():
39
+ """Create a mock finding for testing."""
40
+ finding = MagicMock(spec=IntegrationFinding)
41
+ finding.external_id = "test-external-id-123"
42
+ return finding
43
+
44
+
45
+ class TestMilestoneManagerInitialization:
46
+ """Test MilestoneManager initialization."""
47
+
48
+ def test_initialization(self):
49
+ """Test that MilestoneManager initializes correctly."""
50
+ manager = MilestoneManager(
51
+ integration_title="Test Integration",
52
+ assessor_id="assessor-123",
53
+ scan_date="2024-01-15T10:00:00Z",
54
+ )
55
+
56
+ assert manager.integration_title == "Test Integration"
57
+ assert manager.assessor_id == "assessor-123"
58
+ assert manager.scan_date == "2024-01-15T10:00:00Z"
59
+
60
+
61
+ class TestMilestoneShouldCreateChecks:
62
+ """Test the milestone creation decision logic."""
63
+
64
+ def test_should_create_milestones_enabled(self, milestone_manager, mock_issue):
65
+ """Test that milestones should be created when enabled and issue has ID."""
66
+ with patch("regscale.integrations.milestone_manager.ScannerVariables") as mock_vars:
67
+ mock_vars.useMilestones = True
68
+ assert milestone_manager._should_create_milestones(mock_issue) is True
69
+
70
+ def test_should_not_create_milestones_disabled(self, milestone_manager, mock_issue):
71
+ """Test that milestones should not be created when disabled."""
72
+ with patch("regscale.integrations.milestone_manager.ScannerVariables") as mock_vars:
73
+ mock_vars.useMilestones = False
74
+ assert milestone_manager._should_create_milestones(mock_issue) is False
75
+
76
+ def test_should_not_create_milestones_no_id(self, milestone_manager):
77
+ """Test that milestones should not be created when issue has no ID."""
78
+ issue = MagicMock(spec=regscale_models.Issue)
79
+ issue.id = None
80
+
81
+ with patch("regscale.integrations.milestone_manager.ScannerVariables") as mock_vars:
82
+ mock_vars.useMilestones = True
83
+ assert milestone_manager._should_create_milestones(issue) is False
84
+
85
+
86
+ class TestMilestoneTransitionDetection:
87
+ """Test detection of status transitions."""
88
+
89
+ def test_should_create_reopened_milestone(self, milestone_manager, mock_issue):
90
+ """Test detection of issue reopening (Closed -> Open)."""
91
+ existing_issue = MagicMock(spec=regscale_models.Issue)
92
+ existing_issue.status = regscale_models.IssueStatus.Closed
93
+
94
+ current_issue = MagicMock(spec=regscale_models.Issue)
95
+ current_issue.status = regscale_models.IssueStatus.Open
96
+
97
+ assert milestone_manager._should_create_reopened_milestone(existing_issue, current_issue) is True
98
+
99
+ def test_should_not_create_reopened_milestone_same_status(self, milestone_manager):
100
+ """Test that reopened milestone is not created when status doesn't change."""
101
+ existing_issue = MagicMock(spec=regscale_models.Issue)
102
+ existing_issue.status = regscale_models.IssueStatus.Open
103
+
104
+ current_issue = MagicMock(spec=regscale_models.Issue)
105
+ current_issue.status = regscale_models.IssueStatus.Open
106
+
107
+ assert milestone_manager._should_create_reopened_milestone(existing_issue, current_issue) is False
108
+
109
+ def test_should_create_closed_milestone(self, milestone_manager):
110
+ """Test detection of issue closing (Open -> Closed)."""
111
+ existing_issue = MagicMock(spec=regscale_models.Issue)
112
+ existing_issue.status = regscale_models.IssueStatus.Open
113
+
114
+ current_issue = MagicMock(spec=regscale_models.Issue)
115
+ current_issue.status = regscale_models.IssueStatus.Closed
116
+
117
+ assert milestone_manager._should_create_closed_milestone(existing_issue, current_issue) is True
118
+
119
+ def test_should_not_create_closed_milestone_same_status(self, milestone_manager):
120
+ """Test that closed milestone is not created when status doesn't change."""
121
+ existing_issue = MagicMock(spec=regscale_models.Issue)
122
+ existing_issue.status = regscale_models.IssueStatus.Closed
123
+
124
+ current_issue = MagicMock(spec=regscale_models.Issue)
125
+ current_issue.status = regscale_models.IssueStatus.Closed
126
+
127
+ assert milestone_manager._should_create_closed_milestone(existing_issue, current_issue) is False
128
+
129
+
130
+ class TestMilestoneCreation:
131
+ """Test actual milestone creation."""
132
+
133
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
134
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
135
+ def test_create_new_issue_milestone(
136
+ self, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding
137
+ ):
138
+ """Test creation of milestone for new issue."""
139
+ mock_vars.useMilestones = True
140
+ mock_milestone = MagicMock()
141
+ mock_milestone_class.return_value = mock_milestone
142
+
143
+ milestone_manager.create_milestones_for_issue(
144
+ issue=mock_issue,
145
+ finding=mock_finding,
146
+ existing_issue=None,
147
+ )
148
+
149
+ # Verify Milestone was created with correct parameters
150
+ mock_milestone_class.assert_called_once_with(
151
+ title="Issue created from Test Integration scan",
152
+ milestoneDate=milestone_manager.scan_date,
153
+ responsiblePersonId=milestone_manager.assessor_id,
154
+ parentID=mock_issue.id,
155
+ parentModule="issues",
156
+ )
157
+ mock_milestone.create_or_update.assert_called_once()
158
+
159
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
160
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
161
+ @patch("regscale.integrations.milestone_manager.get_current_datetime")
162
+ def test_create_reopened_milestone(
163
+ self, mock_datetime, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding
164
+ ):
165
+ """Test creation of milestone for reopened issue."""
166
+ mock_vars.useMilestones = True
167
+ mock_datetime.return_value = "2024-01-15T14:00:00Z"
168
+ mock_milestone = MagicMock()
169
+ mock_milestone_class.return_value = mock_milestone
170
+
171
+ existing_issue = MagicMock(spec=regscale_models.Issue)
172
+ existing_issue.status = regscale_models.IssueStatus.Closed
173
+
174
+ current_issue = MagicMock(spec=regscale_models.Issue)
175
+ current_issue.id = 123
176
+ current_issue.status = regscale_models.IssueStatus.Open
177
+
178
+ milestone_manager.create_milestones_for_issue(
179
+ issue=current_issue,
180
+ finding=mock_finding,
181
+ existing_issue=existing_issue,
182
+ )
183
+
184
+ # Verify Milestone was created with correct parameters
185
+ mock_milestone_class.assert_called_once_with(
186
+ title="Issue reopened from Test Integration scan",
187
+ milestoneDate="2024-01-15T14:00:00Z",
188
+ responsiblePersonId=milestone_manager.assessor_id,
189
+ parentID=current_issue.id,
190
+ parentModule="issues",
191
+ )
192
+ mock_milestone.create_or_update.assert_called_once()
193
+
194
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
195
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
196
+ def test_create_closed_milestone(self, mock_vars, mock_milestone_class, milestone_manager, mock_finding):
197
+ """Test creation of milestone for closed issue."""
198
+ mock_vars.useMilestones = True
199
+ mock_milestone = MagicMock()
200
+ mock_milestone_class.return_value = mock_milestone
201
+
202
+ existing_issue = MagicMock(spec=regscale_models.Issue)
203
+ existing_issue.status = regscale_models.IssueStatus.Open
204
+
205
+ current_issue = MagicMock(spec=regscale_models.Issue)
206
+ current_issue.id = 123
207
+ current_issue.status = regscale_models.IssueStatus.Closed
208
+ current_issue.dateCompleted = "2024-01-15T12:00:00Z"
209
+
210
+ milestone_manager.create_milestones_for_issue(
211
+ issue=current_issue,
212
+ finding=mock_finding,
213
+ existing_issue=existing_issue,
214
+ )
215
+
216
+ # Verify Milestone was created with correct parameters
217
+ mock_milestone_class.assert_called_once_with(
218
+ title="Issue closed from Test Integration scan",
219
+ milestoneDate=current_issue.dateCompleted,
220
+ responsiblePersonId=milestone_manager.assessor_id,
221
+ parentID=current_issue.id,
222
+ parentModule="issues",
223
+ )
224
+ mock_milestone.create_or_update.assert_called_once()
225
+
226
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
227
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
228
+ def test_no_milestone_for_existing_open_issue(
229
+ self, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding
230
+ ):
231
+ """Test that no milestone is created when existing issue remains open."""
232
+ mock_vars.useMilestones = True
233
+
234
+ existing_issue = MagicMock(spec=regscale_models.Issue)
235
+ existing_issue.status = regscale_models.IssueStatus.Open
236
+
237
+ current_issue = MagicMock(spec=regscale_models.Issue)
238
+ current_issue.id = 123
239
+ current_issue.status = regscale_models.IssueStatus.Open
240
+
241
+ milestone_manager.create_milestones_for_issue(
242
+ issue=current_issue,
243
+ finding=mock_finding,
244
+ existing_issue=existing_issue,
245
+ )
246
+
247
+ # Verify no Milestone was created
248
+ mock_milestone_class.assert_not_called()
249
+
250
+
251
+ class TestMilestoneCreationErrors:
252
+ """Test error handling in milestone creation."""
253
+
254
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
255
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
256
+ def test_milestone_creation_error_handled(
257
+ self, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding, caplog
258
+ ):
259
+ """Test that milestone creation errors are handled gracefully."""
260
+ mock_vars.useMilestones = True
261
+ mock_milestone = MagicMock()
262
+ mock_milestone_class.return_value = mock_milestone
263
+ mock_milestone.create_or_update.side_effect = Exception("Database error")
264
+
265
+ with caplog.at_level(logging.WARNING):
266
+ milestone_manager.create_milestones_for_issue(
267
+ issue=mock_issue,
268
+ finding=mock_finding,
269
+ existing_issue=None,
270
+ )
271
+
272
+ # Verify error was logged
273
+ assert "Failed to create new milestone" in caplog.text
274
+ assert "Database error" in caplog.text
275
+
276
+
277
+ class TestMilestoneBackfilling:
278
+ """Test milestone backfilling functionality."""
279
+
280
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
281
+ def test_get_existing_milestones(self, mock_milestone_class, milestone_manager, mock_issue):
282
+ """Test retrieval of existing milestones for an issue."""
283
+ # Mock milestones
284
+ mock_milestone1 = MagicMock()
285
+ mock_milestone1.title = "Issue created from Test Integration scan"
286
+ mock_milestone2 = MagicMock()
287
+ mock_milestone2.title = "Some other milestone"
288
+
289
+ mock_milestone_class.get_by_parent.return_value = [mock_milestone1, mock_milestone2]
290
+
291
+ milestones = milestone_manager.get_existing_milestones(mock_issue)
292
+
293
+ assert len(milestones) == 2
294
+ mock_milestone_class.get_by_parent.assert_called_once_with(parent_id=mock_issue.id, parent_module="issues")
295
+
296
+ def test_get_existing_milestones_no_id(self, milestone_manager):
297
+ """Test that get_existing_milestones returns empty list when issue has no ID."""
298
+ issue = MagicMock(spec=regscale_models.Issue)
299
+ issue.id = None
300
+
301
+ milestones = milestone_manager.get_existing_milestones(issue)
302
+
303
+ assert milestones == []
304
+
305
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
306
+ def test_has_creation_milestone_true(self, mock_milestone_class, milestone_manager, mock_issue):
307
+ """Test detection of existing creation milestone."""
308
+ mock_milestone = MagicMock()
309
+ mock_milestone.title = "Issue created from Test Integration scan"
310
+
311
+ mock_milestone_class.get_by_parent.return_value = [mock_milestone]
312
+
313
+ assert milestone_manager.has_creation_milestone(mock_issue) is True
314
+
315
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
316
+ def test_has_creation_milestone_false(self, mock_milestone_class, milestone_manager, mock_issue):
317
+ """Test detection when no creation milestone exists."""
318
+ mock_milestone = MagicMock()
319
+ mock_milestone.title = "Issue closed from Test Integration scan"
320
+
321
+ mock_milestone_class.get_by_parent.return_value = [mock_milestone]
322
+
323
+ assert milestone_manager.has_creation_milestone(mock_issue) is False
324
+
325
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
326
+ def test_has_creation_milestone_empty(self, mock_milestone_class, milestone_manager, mock_issue):
327
+ """Test detection when no milestones exist."""
328
+ mock_milestone_class.get_by_parent.return_value = []
329
+
330
+ assert milestone_manager.has_creation_milestone(mock_issue) is False
331
+
332
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
333
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
334
+ def test_ensure_creation_milestone_exists_backfill(
335
+ self, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding, caplog
336
+ ):
337
+ """Test backfilling of missing creation milestone."""
338
+ mock_vars.useMilestones = True
339
+
340
+ # Mock no existing creation milestone
341
+ mock_milestone_class.get_by_parent.return_value = []
342
+
343
+ # Mock Milestone creation
344
+ mock_milestone = MagicMock()
345
+ mock_milestone_class.return_value = mock_milestone
346
+
347
+ # Set issue dateCreated
348
+ mock_issue.dateCreated = "2024-01-10T10:00:00Z"
349
+
350
+ with caplog.at_level(logging.INFO):
351
+ milestone_manager.ensure_creation_milestone_exists(issue=mock_issue, finding=mock_finding)
352
+
353
+ # Verify milestone was created with issue's dateCreated
354
+ mock_milestone_class.assert_called_once_with(
355
+ title="Issue created from Test Integration scan",
356
+ milestoneDate=mock_issue.dateCreated,
357
+ responsiblePersonId=milestone_manager.assessor_id,
358
+ parentID=mock_issue.id,
359
+ parentModule="issues",
360
+ )
361
+ mock_milestone.create_or_update.assert_called_once()
362
+
363
+ assert "Backfilling missing creation milestone" in caplog.text
364
+
365
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
366
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
367
+ def test_ensure_creation_milestone_exists_no_backfill_exists(
368
+ self, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding
369
+ ):
370
+ """Test that no milestone is created when one already exists."""
371
+ mock_vars.useMilestones = True
372
+
373
+ # Mock has_creation_milestone to return True (issue already has creation milestone)
374
+ with patch.object(milestone_manager, "has_creation_milestone", return_value=True):
375
+ milestone_manager.ensure_creation_milestone_exists(issue=mock_issue, finding=mock_finding)
376
+
377
+ # Verify no new milestone was created (constructor not called)
378
+ mock_milestone_class.assert_not_called()
379
+
380
+ @patch("regscale.integrations.milestone_manager.regscale_models.Milestone")
381
+ @patch("regscale.integrations.milestone_manager.ScannerVariables")
382
+ def test_ensure_creation_milestone_exists_uses_scan_date_fallback(
383
+ self, mock_vars, mock_milestone_class, milestone_manager, mock_issue, mock_finding
384
+ ):
385
+ """Test that scan_date is used when issue.dateCreated is None."""
386
+ mock_vars.useMilestones = True
387
+
388
+ # Mock no existing creation milestone
389
+ mock_milestone_class.get_by_parent.return_value = []
390
+
391
+ # Mock Milestone creation
392
+ mock_milestone = MagicMock()
393
+ mock_milestone_class.return_value = mock_milestone
394
+
395
+ # Set issue dateCreated to None
396
+ mock_issue.dateCreated = None
397
+
398
+ milestone_manager.ensure_creation_milestone_exists(issue=mock_issue, finding=mock_finding)
399
+
400
+ # Verify milestone was created with scan_date as fallback
401
+ mock_milestone_class.assert_called_once_with(
402
+ title="Issue created from Test Integration scan",
403
+ milestoneDate=milestone_manager.scan_date,
404
+ responsiblePersonId=milestone_manager.assessor_id,
405
+ parentID=mock_issue.id,
406
+ parentModule="issues",
407
+ )
408
+ mock_milestone.create_or_update.assert_called_once()