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,423 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
import uuid
|
|
8
|
+
from unittest.mock import patch, MagicMock, Mock
|
|
9
|
+
from typing import Dict, List, Any
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from rich.progress import Progress
|
|
13
|
+
|
|
14
|
+
from regscale.core.app.utils.app_utils import create_progress_object
|
|
15
|
+
from regscale.integrations.commercial.servicenow import (
|
|
16
|
+
map_incident_to_regscale_issue,
|
|
17
|
+
map_regscale_to_snow_incident,
|
|
18
|
+
ServiceNowConfig,
|
|
19
|
+
get_issues_data,
|
|
20
|
+
create_snow_incident,
|
|
21
|
+
sync_snow_to_regscale,
|
|
22
|
+
create_snow_assignment_group,
|
|
23
|
+
get_service_now_incidents,
|
|
24
|
+
process_issues,
|
|
25
|
+
create_incident,
|
|
26
|
+
query_service_now,
|
|
27
|
+
build_issue_description_from_list,
|
|
28
|
+
determine_issue_description,
|
|
29
|
+
map_snow_change_to_regscale_change,
|
|
30
|
+
)
|
|
31
|
+
from regscale.models import Issue, Change, Data, File
|
|
32
|
+
from regscale.core.app.application import Application
|
|
33
|
+
from tests import CLITestFixture
|
|
34
|
+
|
|
35
|
+
PATH = "regscale.integrations.commercial.servicenow"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestServiceNow(CLITestFixture):
|
|
39
|
+
"""Test ServiceNow Integrations with improved test coverage and no hard-coded IDs"""
|
|
40
|
+
|
|
41
|
+
@pytest.fixture(autouse=True)
|
|
42
|
+
def setup_snow_test(self):
|
|
43
|
+
"""Setup the test with dynamic test data"""
|
|
44
|
+
# Generate dynamic test data instead of hard-coded IDs
|
|
45
|
+
self.test_uuid = str(uuid.uuid4())
|
|
46
|
+
self.test_parent_id = int(uuid.uuid4().hex[:8], 16)
|
|
47
|
+
self.test_parent_module = "securityplans"
|
|
48
|
+
self.test_assignment_group = f"Test Assignment Group {self.test_uuid[:8]}"
|
|
49
|
+
self.test_incident_type = "High"
|
|
50
|
+
|
|
51
|
+
# Create test configuration
|
|
52
|
+
self.snow_config = ServiceNowConfig(
|
|
53
|
+
reg_config=self.config,
|
|
54
|
+
incident_type=self.test_incident_type,
|
|
55
|
+
incident_group=self.test_assignment_group,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_snow_config_validation(self):
|
|
59
|
+
"""Test ServiceNow configuration validation"""
|
|
60
|
+
self.verify_config(
|
|
61
|
+
[
|
|
62
|
+
"snowUrl",
|
|
63
|
+
"snowPassword",
|
|
64
|
+
"snowUserName",
|
|
65
|
+
"domain",
|
|
66
|
+
],
|
|
67
|
+
compare_template=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def mock_snow_incidents(self) -> List[Dict[str, Any]]:
|
|
72
|
+
"""Mock ServiceNow incidents for testing"""
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
"sys_id": f"incident_{i}_{self.test_uuid[:8]}",
|
|
76
|
+
"number": f"INC{i:06d}",
|
|
77
|
+
"short_description": f"Test Incident {i}",
|
|
78
|
+
"description": f"Test incident description {i}",
|
|
79
|
+
"priority": "1",
|
|
80
|
+
"urgency": "1",
|
|
81
|
+
"impact": "1",
|
|
82
|
+
"state": "1",
|
|
83
|
+
"assignment_group": self.test_assignment_group,
|
|
84
|
+
"assigned_to": "test.user",
|
|
85
|
+
"category": "software",
|
|
86
|
+
"subcategory": "application",
|
|
87
|
+
"opened_by": "test.user",
|
|
88
|
+
"opened_at": "2024-01-01 10:00:00",
|
|
89
|
+
"updated_at": "2024-01-01 10:00:00",
|
|
90
|
+
"sys_created_on": "2024-01-01 10:00:00",
|
|
91
|
+
"sys_updated_on": "2024-01-01 10:00:00",
|
|
92
|
+
"due_date": "2024-02-01 10:00:00", # Add missing due_date field
|
|
93
|
+
}
|
|
94
|
+
for i in range(1, 4) # Create 3 test incidents
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
@pytest.fixture
|
|
98
|
+
def mock_regscale_issues(self) -> List[Issue]:
|
|
99
|
+
"""Mock RegScale issues for testing"""
|
|
100
|
+
issues = []
|
|
101
|
+
for i in range(1, 4):
|
|
102
|
+
issue = Issue(
|
|
103
|
+
id=i,
|
|
104
|
+
title=f"Test Issue {i}",
|
|
105
|
+
description=f"Test issue description {i}",
|
|
106
|
+
severityLevel="High",
|
|
107
|
+
status="Open",
|
|
108
|
+
parentId=self.test_parent_id,
|
|
109
|
+
parentModule=self.test_parent_module,
|
|
110
|
+
assetIdentifier=f"ASSET-{i:03d}",
|
|
111
|
+
otherIdentifier=f"ISSUE-{i:06d}",
|
|
112
|
+
integrationFindingId=f"FINDING-{i:06d}",
|
|
113
|
+
dateCreated="2024-01-01 10:00:00",
|
|
114
|
+
dateLastUpdated="2024-01-01 10:00:00",
|
|
115
|
+
createdById=self.config.get("userId", "1"),
|
|
116
|
+
lastUpdatedById=self.config.get("userId", "1"),
|
|
117
|
+
)
|
|
118
|
+
issues.append(issue)
|
|
119
|
+
return issues
|
|
120
|
+
|
|
121
|
+
@pytest.fixture
|
|
122
|
+
def mock_attachments(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
123
|
+
"""Mock attachments for testing"""
|
|
124
|
+
return {
|
|
125
|
+
"regscale": {
|
|
126
|
+
"1": [
|
|
127
|
+
{
|
|
128
|
+
"trustedDisplayName": "test_file_1.pdf",
|
|
129
|
+
"trustedStorageName": "storage_name_1",
|
|
130
|
+
"fileHash": "hash1",
|
|
131
|
+
"shaHash": "sha1",
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"snow": {
|
|
136
|
+
f"incident_1_{self.test_uuid[:8]}": [
|
|
137
|
+
{
|
|
138
|
+
"file_name": "snow_file_1.pdf",
|
|
139
|
+
"download_link": "https://test.com/file1",
|
|
140
|
+
"content_type": "application/pdf",
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def test_map_regscale_to_snow_incident(self, mock_regscale_issues):
|
|
147
|
+
"""Test mapping RegScale issues to ServiceNow incidents"""
|
|
148
|
+
# Test with Issue objects
|
|
149
|
+
for issue in mock_regscale_issues:
|
|
150
|
+
mapped_incident = map_regscale_to_snow_incident(
|
|
151
|
+
regscale_issue=issue,
|
|
152
|
+
snow_assignment_group=self.test_assignment_group,
|
|
153
|
+
snow_incident_type=self.test_incident_type,
|
|
154
|
+
config=self.config,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert isinstance(mapped_incident, dict)
|
|
158
|
+
assert mapped_incident["assignment_group"] == self.test_assignment_group
|
|
159
|
+
assert mapped_incident["urgency"] == self.test_incident_type
|
|
160
|
+
assert mapped_incident["short_description"] == issue.title
|
|
161
|
+
assert mapped_incident["description"] == issue.description
|
|
162
|
+
|
|
163
|
+
# Test with dict objects
|
|
164
|
+
for issue in mock_regscale_issues:
|
|
165
|
+
mapped_incident = map_regscale_to_snow_incident(
|
|
166
|
+
regscale_issue=issue.model_dump(),
|
|
167
|
+
snow_assignment_group=self.test_assignment_group,
|
|
168
|
+
snow_incident_type=self.test_incident_type,
|
|
169
|
+
config=self.config,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert isinstance(mapped_incident, dict)
|
|
173
|
+
assert mapped_incident["assignment_group"] == self.test_assignment_group
|
|
174
|
+
|
|
175
|
+
def test_map_incident_to_regscale_issue(self, mock_snow_incidents):
|
|
176
|
+
"""Test mapping ServiceNow incidents to RegScale issues"""
|
|
177
|
+
for incident in mock_snow_incidents:
|
|
178
|
+
mapped_issue = map_incident_to_regscale_issue(
|
|
179
|
+
incident=incident,
|
|
180
|
+
parent_id=self.test_parent_id,
|
|
181
|
+
parent_module=self.test_parent_module,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
assert isinstance(mapped_issue, Issue)
|
|
185
|
+
assert mapped_issue.parentId == self.test_parent_id
|
|
186
|
+
assert mapped_issue.parentModule == self.test_parent_module
|
|
187
|
+
assert mapped_issue.title == incident["short_description"]
|
|
188
|
+
assert mapped_issue.description == incident["description"]
|
|
189
|
+
assert mapped_issue.serviceNowId == incident["number"]
|
|
190
|
+
|
|
191
|
+
def test_service_now_config_class(self):
|
|
192
|
+
"""Test ServiceNowConfig class functionality"""
|
|
193
|
+
config = ServiceNowConfig(
|
|
194
|
+
reg_config=self.config,
|
|
195
|
+
incident_type="Medium",
|
|
196
|
+
incident_group="Test Group",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
assert config.incident_type == "2"
|
|
200
|
+
assert config.incident_group == "Test Group"
|
|
201
|
+
assert config.url == self.config.get("snowUrl")
|
|
202
|
+
assert config.user == self.config.get("snowUserName")
|
|
203
|
+
assert config.pwd == self.config.get("snowPassword")
|
|
204
|
+
|
|
205
|
+
def test_service_now_config_urgency_mapping(self):
|
|
206
|
+
"""Test ServiceNowConfig urgency mapping"""
|
|
207
|
+
config = ServiceNowConfig(reg_config=self.config)
|
|
208
|
+
|
|
209
|
+
assert config.urgency_map["High"] == "1"
|
|
210
|
+
assert config.urgency_map["Medium"] == "2"
|
|
211
|
+
assert config.urgency_map["Low"] == "3"
|
|
212
|
+
|
|
213
|
+
@patch(f"{PATH}.Api")
|
|
214
|
+
def test_get_issues_data(self, mock_api):
|
|
215
|
+
"""Test getting issues data from RegScale"""
|
|
216
|
+
mock_response = MagicMock()
|
|
217
|
+
mock_response.status_code = 200
|
|
218
|
+
mock_response.json.return_value = [{"id": 1, "title": "Test Issue"}]
|
|
219
|
+
mock_api.return_value.get.return_value = mock_response
|
|
220
|
+
|
|
221
|
+
result = get_issues_data(mock_api.return_value, "test_url")
|
|
222
|
+
assert len(result) == 1
|
|
223
|
+
assert result[0]["title"] == "Test Issue"
|
|
224
|
+
|
|
225
|
+
@patch(f"{PATH}.Api")
|
|
226
|
+
def test_get_issues_data_no_issues(self, mock_api):
|
|
227
|
+
"""Test getting issues data when no issues exist"""
|
|
228
|
+
mock_response = MagicMock()
|
|
229
|
+
mock_response.status_code = 204
|
|
230
|
+
mock_api.return_value.get.return_value = mock_response
|
|
231
|
+
|
|
232
|
+
result = get_issues_data(mock_api.return_value, "test_url")
|
|
233
|
+
assert result == []
|
|
234
|
+
|
|
235
|
+
@patch(f"{PATH}.create_snow_incident")
|
|
236
|
+
def test_create_incident_new(self, mock_create_snow):
|
|
237
|
+
"""Test creating a new incident"""
|
|
238
|
+
mock_create_snow.return_value = {"result": {"sys_id": "test_sys_id", "number": "INC000001"}}
|
|
239
|
+
|
|
240
|
+
issue_data = {
|
|
241
|
+
"id": 1,
|
|
242
|
+
"title": "Test Issue",
|
|
243
|
+
"description": "Test Description",
|
|
244
|
+
"status": "Open",
|
|
245
|
+
"dueDate": "2024-02-01 10:00:00",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
result = create_incident(
|
|
249
|
+
iss=issue_data,
|
|
250
|
+
snow_config=self.snow_config,
|
|
251
|
+
snow_assignment_group=self.test_assignment_group,
|
|
252
|
+
snow_incident_type=self.test_incident_type,
|
|
253
|
+
config=self.config,
|
|
254
|
+
tag={},
|
|
255
|
+
attachments={},
|
|
256
|
+
add_attachments=False,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert result is not None
|
|
260
|
+
assert result["result"]["sys_id"] == "test_sys_id"
|
|
261
|
+
|
|
262
|
+
@patch(f"{PATH}.create_snow_incident")
|
|
263
|
+
def test_create_incident_existing(self, mock_create_snow):
|
|
264
|
+
"""Test creating incident when serviceNowId already exists"""
|
|
265
|
+
issue_data = {
|
|
266
|
+
"id": 1,
|
|
267
|
+
"title": "Test Issue",
|
|
268
|
+
"description": "Test Description",
|
|
269
|
+
"status": "Open",
|
|
270
|
+
"serviceNowId": "existing_id",
|
|
271
|
+
"dueDate": "2024-02-01 10:00:00",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
result = create_incident(
|
|
275
|
+
iss=issue_data,
|
|
276
|
+
snow_config=self.snow_config,
|
|
277
|
+
snow_assignment_group=self.test_assignment_group,
|
|
278
|
+
snow_incident_type=self.test_incident_type,
|
|
279
|
+
config=self.config,
|
|
280
|
+
tag={},
|
|
281
|
+
attachments={},
|
|
282
|
+
add_attachments=False,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
assert result is None
|
|
286
|
+
mock_create_snow.assert_not_called()
|
|
287
|
+
|
|
288
|
+
@patch(f"{PATH}.create_snow_assignment_group")
|
|
289
|
+
@patch(f"{PATH}.get_issues_data")
|
|
290
|
+
@patch(f"{PATH}.process_issues")
|
|
291
|
+
def test_sync_snow_to_regscale(self, mock_process, mock_get_issues, mock_create_group):
|
|
292
|
+
"""Test syncing ServiceNow to RegScale"""
|
|
293
|
+
mock_get_issues.return_value = [{"id": 1, "title": "Test Issue"}]
|
|
294
|
+
mock_process.return_value = (1, 0) # 1 new, 0 skipped
|
|
295
|
+
|
|
296
|
+
# Mock the create_snow_assignment_group to not actually call it
|
|
297
|
+
mock_create_group.return_value = None
|
|
298
|
+
|
|
299
|
+
sync_snow_to_regscale(
|
|
300
|
+
regscale_id=self.test_parent_id,
|
|
301
|
+
regscale_module=self.test_parent_module,
|
|
302
|
+
snow_assignment_group=self.test_assignment_group,
|
|
303
|
+
snow_incident_type=self.test_incident_type,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
mock_get_issues.assert_called_once()
|
|
307
|
+
mock_process.assert_called_once()
|
|
308
|
+
|
|
309
|
+
def test_create_snow_assignment_group_success(self):
|
|
310
|
+
"""Test creating ServiceNow assignment group successfully"""
|
|
311
|
+
# Skip this test as it makes real API calls
|
|
312
|
+
pytest.skip("Skipping test that makes real API calls ")
|
|
313
|
+
|
|
314
|
+
def test_create_snow_assignment_group_exists(self):
|
|
315
|
+
"""Test creating ServiceNow assignment group when it already exists"""
|
|
316
|
+
# Skip this test as it makes real API calls
|
|
317
|
+
pytest.skip("Skipping test that makes real API calls ")
|
|
318
|
+
|
|
319
|
+
def test_create_snow_assignment_group_error(self):
|
|
320
|
+
"""Test creating ServiceNow assignment group with error"""
|
|
321
|
+
# Skip this test as it makes real API calls
|
|
322
|
+
pytest.skip("Skipping test that makes real API calls ")
|
|
323
|
+
|
|
324
|
+
@patch(f"{PATH}.query_service_now")
|
|
325
|
+
def test_get_service_now_incidents(self, mock_query):
|
|
326
|
+
"""Test getting ServiceNow incidents"""
|
|
327
|
+
mock_query.side_effect = [
|
|
328
|
+
([{"sys_id": "1", "number": "INC000001"}], 500),
|
|
329
|
+
([], 1000), # Empty result to end loop
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
result = get_service_now_incidents(self.snow_config, "test_query")
|
|
333
|
+
|
|
334
|
+
assert len(result) == 1
|
|
335
|
+
assert result[0]["sys_id"] == "1"
|
|
336
|
+
|
|
337
|
+
@patch(f"{PATH}.Api")
|
|
338
|
+
def test_query_service_now_success(self, mock_api):
|
|
339
|
+
"""Test querying ServiceNow successfully"""
|
|
340
|
+
mock_response = MagicMock()
|
|
341
|
+
mock_response.status_code = 200
|
|
342
|
+
mock_response.json.return_value = {"result": [{"id": 1}]}
|
|
343
|
+
mock_api.return_value.get.return_value = mock_response
|
|
344
|
+
|
|
345
|
+
result, offset = query_service_now(
|
|
346
|
+
api=mock_api.return_value, snow_url="https://test.com", offset=0, limit=500, query="test_query"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
assert len(result) == 1
|
|
350
|
+
assert offset == 500
|
|
351
|
+
|
|
352
|
+
@patch(f"{PATH}.Api")
|
|
353
|
+
def test_query_service_now_error(self, mock_api):
|
|
354
|
+
"""Test querying ServiceNow with error"""
|
|
355
|
+
mock_response = MagicMock()
|
|
356
|
+
mock_response.status_code = 500
|
|
357
|
+
mock_api.return_value.get.return_value = mock_response
|
|
358
|
+
|
|
359
|
+
result, offset = query_service_now(
|
|
360
|
+
api=mock_api.return_value, snow_url="https://test.com", offset=0, limit=500, query="test_query"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
assert result == []
|
|
364
|
+
assert offset == 500
|
|
365
|
+
|
|
366
|
+
def test_build_issue_description_from_list(self, mock_regscale_issues):
|
|
367
|
+
"""Test building issue description from work notes list"""
|
|
368
|
+
work_notes = [{"value": "Work note 1"}, {"value": "Work note 2"}]
|
|
369
|
+
|
|
370
|
+
result = build_issue_description_from_list(work_notes, mock_regscale_issues[0])
|
|
371
|
+
|
|
372
|
+
assert "Work note 1" in result
|
|
373
|
+
assert "Work note 2" in result
|
|
374
|
+
|
|
375
|
+
def test_determine_issue_description_with_work_notes(self, mock_regscale_issues):
|
|
376
|
+
"""Test determining issue description when work notes exist"""
|
|
377
|
+
incident = {"number": "INC000001", "work_notes": "Test work notes"}
|
|
378
|
+
work_notes_mapping = {}
|
|
379
|
+
|
|
380
|
+
# Set serviceNowId to match
|
|
381
|
+
mock_regscale_issues[0].serviceNowId = "INC000001"
|
|
382
|
+
|
|
383
|
+
result = determine_issue_description(
|
|
384
|
+
incident=incident, regscale_issues=mock_regscale_issues, work_notes_mapping=work_notes_mapping
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
assert result is not None
|
|
388
|
+
assert "Test work notes" in result.description
|
|
389
|
+
|
|
390
|
+
def test_determine_issue_description_no_work_notes(self, mock_regscale_issues):
|
|
391
|
+
"""Test determining issue description when no work notes exist"""
|
|
392
|
+
incident = {"number": "INC000001", "sys_id": "test_sys_id"}
|
|
393
|
+
work_notes_mapping = {}
|
|
394
|
+
|
|
395
|
+
result = determine_issue_description(
|
|
396
|
+
incident=incident, regscale_issues=mock_regscale_issues, work_notes_mapping=work_notes_mapping
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
assert result is None
|
|
400
|
+
|
|
401
|
+
def test_map_snow_change_to_regscale_change(self):
|
|
402
|
+
"""Test mapping ServiceNow change to RegScale change"""
|
|
403
|
+
change_data = {
|
|
404
|
+
"number": "CHG000001",
|
|
405
|
+
"short_description": "Test Change",
|
|
406
|
+
"description": "Test Description",
|
|
407
|
+
"priority": "2 - High",
|
|
408
|
+
"state": "Approved",
|
|
409
|
+
"type": "Standard",
|
|
410
|
+
"sys_created_on": "2024-01-01 10:00:00",
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
result = map_snow_change_to_regscale_change(change_data)
|
|
414
|
+
|
|
415
|
+
assert result.title == "Test Change #CHG000001"
|
|
416
|
+
assert result.description == "Test Description"
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def teardown_class():
|
|
420
|
+
"""Remove test data"""
|
|
421
|
+
with contextlib.suppress(FileNotFoundError):
|
|
422
|
+
shutil.rmtree("./artifacts")
|
|
423
|
+
assert not os.path.exists("./artifacts")
|