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,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()
|