regscale-cli 6.25.0.1__py3-none-any.whl → 6.26.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (84) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +18 -3
  4. regscale/core/app/internal/login.py +0 -1
  5. regscale/core/app/utils/catalog_utils/common.py +1 -1
  6. regscale/integrations/commercial/sicura/api.py +14 -13
  7. regscale/integrations/commercial/sicura/commands.py +8 -2
  8. regscale/integrations/commercial/sicura/scanner.py +49 -39
  9. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  10. regscale/integrations/commercial/synqly/assets.py +17 -0
  11. regscale/integrations/commercial/wizv2/click.py +26 -26
  12. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  13. regscale/integrations/commercial/wizv2/constants.py +20 -71
  14. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  15. regscale/integrations/compliance_integration.py +67 -2
  16. regscale/integrations/control_matcher.py +358 -0
  17. regscale/integrations/due_date_handler.py +118 -6
  18. regscale/integrations/milestone_manager.py +291 -0
  19. regscale/integrations/public/__init__.py +1 -0
  20. regscale/integrations/public/cci_importer.py +37 -38
  21. regscale/integrations/public/fedramp/click.py +60 -2
  22. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  23. regscale/integrations/scanner_integration.py +199 -130
  24. regscale/models/integration_models/cisa_kev_data.json +199 -4
  25. regscale/models/integration_models/nexpose.py +36 -10
  26. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  27. regscale/models/locking.py +12 -8
  28. regscale/models/platform.py +1 -2
  29. regscale/models/regscale_models/control_implementation.py +46 -21
  30. regscale/models/regscale_models/issue.py +256 -94
  31. regscale/models/regscale_models/milestone.py +1 -1
  32. regscale/models/regscale_models/regscale_model.py +6 -1
  33. regscale/templates/__init__.py +0 -0
  34. regscale/utils/threading/threadhandler.py +20 -15
  35. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  36. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
  37. tests/regscale/integrations/commercial/__init__.py +0 -0
  38. tests/regscale/integrations/commercial/conftest.py +28 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  40. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  41. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  42. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  43. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  44. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  45. tests/regscale/integrations/commercial/test_burp.py +48 -0
  46. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  47. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  48. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  49. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  50. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  51. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  52. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  53. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  54. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  55. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  56. tests/regscale/integrations/commercial/test_snow.py +423 -0
  57. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  58. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  59. tests/regscale/integrations/commercial/test_stig.py +33 -0
  60. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  61. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  62. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  63. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  64. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  65. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  66. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  71. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  72. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  73. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  74. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  75. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  76. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  77. tests/regscale/integrations/test_control_matcher.py +1314 -0
  78. tests/regscale/integrations/test_control_matching.py +155 -0
  79. tests/regscale/integrations/test_milestone_manager.py +408 -0
  80. tests/regscale/models/test_issue.py +378 -1
  81. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  82. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  83. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  84. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.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")