regscale-cli 6.26.0.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 (95) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -1
  3. regscale/core/app/internal/evidence.py +419 -2
  4. regscale/dev/code_gen.py +24 -20
  5. regscale/integrations/commercial/jira.py +367 -126
  6. regscale/integrations/commercial/qualys/__init__.py +7 -8
  7. regscale/integrations/commercial/qualys/scanner.py +8 -3
  8. regscale/integrations/commercial/synqly/assets.py +17 -0
  9. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  10. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  11. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  12. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  13. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  14. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  15. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  16. regscale/integrations/commercial/wizv2/click.py +44 -59
  17. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  18. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  19. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  20. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  21. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  22. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  23. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  24. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  25. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  26. regscale/integrations/commercial/wizv2/issue.py +1 -1
  27. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  28. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  29. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  30. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  31. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  32. regscale/integrations/commercial/wizv2/reports.py +1 -1
  33. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  34. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  35. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  36. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  37. regscale/integrations/commercial/wizv2/variables.py +89 -3
  38. regscale/integrations/compliance_integration.py +0 -46
  39. regscale/integrations/control_matcher.py +22 -3
  40. regscale/integrations/due_date_handler.py +14 -8
  41. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  42. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  43. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  44. regscale/integrations/scanner_integration.py +127 -57
  45. regscale/models/integration_models/cisa_kev_data.json +132 -9
  46. regscale/models/integration_models/qualys.py +3 -4
  47. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  48. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  49. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  50. regscale/models/regscale_models/control_implementation.py +1 -1
  51. regscale/models/regscale_models/issue.py +0 -1
  52. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +93 -60
  54. tests/regscale/integrations/commercial/test_jira.py +481 -91
  55. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  56. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  57. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  58. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  59. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  60. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  63. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  65. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  67. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  69. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  70. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  71. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  72. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  74. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  75. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  76. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  77. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  78. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  79. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  85. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  86. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  87. tests/regscale/integrations/public/test_fedramp.py +301 -0
  88. tests/regscale/integrations/test_control_matcher.py +83 -0
  89. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  90. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  91. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  92. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -31,7 +31,6 @@ from regscale.integrations.commercial.jira import (
31
31
  sync_regscale_objects_to_jira,
32
32
  sync_regscale_to_jira,
33
33
  task_and_attachments_sync,
34
- update_regscale_issues,
35
34
  create_regscale_task_from_jira,
36
35
  create_and_update_regscale_tasks,
37
36
  process_tasks_for_sync,
@@ -39,7 +38,7 @@ from regscale.integrations.commercial.jira import (
39
38
  upload_files_to_regscale,
40
39
  validate_issue_type,
41
40
  )
42
- from regscale.models import File, Issue, SecurityPlan
41
+ from regscale.models import File, Issue
43
42
  from regscale.models.regscale_models.task import Task
44
43
  from tests import CLITestFixture
45
44
 
@@ -184,6 +183,7 @@ class TestJira(CLITestFixture):
184
183
  parentId=self.PARENT_ID,
185
184
  parentModule=self.PARENT_MODULE,
186
185
  dueDate=get_current_datetime(),
186
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
187
187
  status="Open",
188
188
  ).create()
189
189
  if fetch_attachments:
@@ -287,6 +287,8 @@ class TestJira(CLITestFixture):
287
287
  def test_sync_regscale_and_jira(self, mock_check_license, fetch_attachments):
288
288
  """Test the entire Jira & RegScale sync process"""
289
289
  mock_check_license.return_value.config = self.config
290
+ # Add thread_manager to the mock Application
291
+ mock_check_license.return_value.thread_manager = MagicMock()
290
292
  try:
291
293
  sync_regscale_and_jira(
292
294
  parent_id=self.PARENT_ID,
@@ -302,6 +304,8 @@ class TestJira(CLITestFixture):
302
304
  def test_sync_regscale_and_jira_tasks(self, mock_check_license, fetch_attachments):
303
305
  """Test the entire Jira & RegScale task sync process"""
304
306
  mock_check_license.return_value.config = self.config
307
+ # Add thread_manager to the mock Application
308
+ mock_check_license.return_value.thread_manager = MagicMock()
305
309
  try:
306
310
  sync_regscale_and_jira(
307
311
  parent_id=self.PARENT_ID,
@@ -358,7 +362,7 @@ class TestJira(CLITestFixture):
358
362
  mock_fetch_jira_objects.assert_called_once_with(
359
363
  jira_client=mock_jira_client,
360
364
  jira_project=self.JIRA_PROJECT,
361
- jql_str="project = SNES",
365
+ jql_str="project = 'SNES'",
362
366
  jira_issue_type="Bug",
363
367
  sync_tasks_only=False,
364
368
  )
@@ -390,6 +394,11 @@ class TestJira(CLITestFixture):
390
394
  mock_jira_client = MagicMock()
391
395
  mock_create_jira_client.return_value = mock_jira_client
392
396
 
397
+ # Setup mock config with jiraCustomFields returning empty dict
398
+ mock_config = MagicMock()
399
+ mock_config.get.return_value = {}
400
+ mock_check_license.return_value.config = mock_config
401
+
393
402
  # mock these so that we can control what objects were returned to check later
394
403
  mock_fetch_jira_objects.return_value = MagicMock()
395
404
  mock_get_regscale_data_and_attachments.return_value = (MagicMock(), MagicMock())
@@ -415,7 +424,7 @@ class TestJira(CLITestFixture):
415
424
  mock_fetch_jira_objects.assert_called_once_with(
416
425
  jira_client=mock_jira_client,
417
426
  jira_project=self.JIRA_PROJECT,
418
- jql_str="project = SNES",
427
+ jql_str="project = 'SNES'",
419
428
  jira_issue_type="Bug",
420
429
  sync_tasks_only=False,
421
430
  )
@@ -429,6 +438,7 @@ class TestJira(CLITestFixture):
429
438
  api=mock_api.return_value,
430
439
  sync_attachments=fetch_attachments,
431
440
  attachments=mock_get_regscale_data_and_attachments.return_value[1],
441
+ custom_fields={},
432
442
  )
433
443
  mock_sync_regscale_objects_to_jira.assert_called_once_with(
434
444
  mock_fetch_jira_objects.return_value,
@@ -438,6 +448,7 @@ class TestJira(CLITestFixture):
438
448
  self.PARENT_ID,
439
449
  self.PARENT_MODULE,
440
450
  False,
451
+ False,
441
452
  )
442
453
 
443
454
  @patch(f"{PATH}.sync_regscale_objects_to_jira")
@@ -606,14 +617,12 @@ class TestJira(CLITestFixture):
606
617
  mock_create_and_update_regscale_issues.assert_not_called()
607
618
 
608
619
  @patch(f"{PATH}.create_jira_client")
609
- @patch(f"{PATH}.create_threads")
610
620
  @patch(f"{PATH}.create_and_update_regscale_tasks")
611
621
  @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
612
622
  def test_sync_regscale_objects_to_jira_all(
613
623
  self,
614
624
  mock_check_license,
615
625
  mock_create_and_update_regscale_tasks,
616
- mock_create_threads,
617
626
  mock_create_jira_client,
618
627
  fetch_attachments,
619
628
  ):
@@ -622,18 +631,29 @@ class TestJira(CLITestFixture):
622
631
  mock_jira_client = MagicMock()
623
632
  mock_create_jira_client.return_value = mock_jira_client
624
633
 
634
+ # Create mock application with ThreadManager
635
+ mock_app = MagicMock(spec=Application)
636
+ mock_app.config = self.config
637
+ mock_thread_manager = MagicMock()
638
+ mock_app.thread_manager = mock_thread_manager
639
+
640
+ # Mock Jira issues
641
+ mock_jira_issues = [MagicMock(), MagicMock()]
642
+
625
643
  sync_regscale_objects_to_jira(
626
- MagicMock(),
644
+ mock_jira_issues,
627
645
  MagicMock(),
628
646
  fetch_attachments,
629
- MagicMock(spec=Application),
647
+ mock_app,
630
648
  self.PARENT_ID,
631
649
  self.PARENT_MODULE,
632
650
  False,
633
651
  )
634
652
 
635
653
  mock_create_and_update_regscale_tasks.assert_not_called()
636
- mock_create_threads.assert_called_once()
654
+ # Verify ThreadManager methods were called
655
+ mock_thread_manager.submit_tasks_from_list.assert_called_once()
656
+ mock_thread_manager.execute_and_verify.assert_called_once()
637
657
 
638
658
  @patch(f"{PATH}.JIRA")
639
659
  def test_create_jira_client_basic(self, mock_jira):
@@ -1159,14 +1179,14 @@ class TestJira(CLITestFixture):
1159
1179
  mock_compare_files_for_dupes_and_upload.assert_called_once()
1160
1180
 
1161
1181
  @patch(f"{PATH}.compare_files_for_dupes_and_upload")
1162
- @patch(f"{PATH}.Issue.insert_issue")
1163
- @patch(f"{PATH}.Issue.update_issue")
1182
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
1183
+ @patch(f"{PATH}.Issue.save")
1164
1184
  @patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
1165
1185
  def test_create_and_update_regscale_issues(
1166
1186
  self,
1167
1187
  mock_job_progress_object,
1168
1188
  mock_update_issue,
1169
- mock_insert_issue,
1189
+ mock_map_jira_to_regscale_issue,
1170
1190
  mock_compare_files_for_dupes_and_upload,
1171
1191
  fetch_attachments,
1172
1192
  ):
@@ -1190,60 +1210,58 @@ class TestJira(CLITestFixture):
1190
1210
  lowest_priority.name = "lowest"
1191
1211
 
1192
1212
  # Create mock Jira issues
1193
- jira_issues = [
1194
- MagicMock( # should be skipped (up to date - nothing happens) - counts as updated
1195
- key="JIRA-1",
1196
- fields=MagicMock(
1197
- summary="Skipped issue",
1198
- description="Skipped issue description",
1199
- status=open_status,
1200
- duedate=None,
1201
- priority=highest_priority,
1202
- statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1203
- attachment=MagicMock(),
1204
- ),
1213
+ jira_issue_1 = MagicMock( # should be updated (existing issue)
1214
+ key="JIRA-1",
1215
+ fields=MagicMock(
1216
+ summary="Skipped issue",
1217
+ description="Skipped issue description",
1218
+ status=open_status,
1219
+ duedate=None,
1220
+ priority=highest_priority,
1221
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1222
+ attachment=[MagicMock()], # Has attachments
1205
1223
  ),
1206
- MagicMock( # should be inserted (issue in jira but not regscale)
1207
- key="JIRA-2",
1208
- fields=MagicMock(
1209
- summary="New issue",
1210
- description="New issue description",
1211
- status=open_status,
1212
- duedate=None,
1213
- priority=medium_priority,
1214
- statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1215
- attachment=None,
1216
- ),
1224
+ )
1225
+ jira_issue_2 = MagicMock( # should be inserted (issue in jira but not regscale)
1226
+ key="JIRA-2",
1227
+ fields=MagicMock(
1228
+ summary="New issue",
1229
+ description="New issue description",
1230
+ status=open_status,
1231
+ duedate=None,
1232
+ priority=medium_priority,
1233
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1234
+ attachment=None,
1217
1235
  ),
1218
- MagicMock( # should be updated (issue in both but out of sync)
1219
- key="JIRA-3",
1220
- fields=MagicMock(
1221
- summary="Existing issue",
1222
- description="Existing issue description",
1223
- status=in_progress_status,
1224
- duedate=None,
1225
- priority=low_priority,
1226
- statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1227
- attachment=None, # No attachments
1228
- ),
1236
+ )
1237
+ jira_issue_3 = MagicMock( # should be updated (issue in both but out of sync)
1238
+ key="JIRA-3",
1239
+ fields=MagicMock(
1240
+ summary="Existing issue",
1241
+ description="Existing issue description",
1242
+ status=in_progress_status,
1243
+ duedate=None,
1244
+ priority=low_priority,
1245
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1246
+ attachment=None, # No attachments
1229
1247
  ),
1230
- MagicMock( # should be closed - counts as updated
1231
- key="JIRA-4",
1232
- fields=MagicMock(
1233
- summary="Existing issue",
1234
- description="Existing issue description",
1235
- status=closed_status,
1236
- duedate=None,
1237
- priority=lowest_priority,
1238
- statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1239
- attachment=None, # No attachments
1240
- ),
1248
+ )
1249
+ jira_issue_4 = MagicMock( # should be closed - counts as updated
1250
+ key="JIRA-4",
1251
+ fields=MagicMock(
1252
+ summary="Existing issue",
1253
+ description="Existing issue description",
1254
+ status=closed_status,
1255
+ duedate=None,
1256
+ priority=lowest_priority,
1257
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1258
+ attachment=None, # No attachments
1241
1259
  ),
1242
- ]
1260
+ )
1243
1261
 
1244
1262
  # Create RegScale issues
1245
1263
  regscale_issues = [
1246
- Issue( # matches JIRA-1 (should be skipped - up to date)
1264
+ Issue( # matches JIRA-1 (should be updated)
1247
1265
  id=1,
1248
1266
  title="Skipped issue",
1249
1267
  status="Open",
@@ -1252,6 +1270,7 @@ class TestJira(CLITestFixture):
1252
1270
  parentId=self.PARENT_ID,
1253
1271
  parentModule=self.PARENT_MODULE,
1254
1272
  dueDate=get_current_datetime(),
1273
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1255
1274
  ),
1256
1275
  Issue( # matches JIRA-3 (should be updated - issue in both but out of sync)
1257
1276
  id=3,
@@ -1262,6 +1281,7 @@ class TestJira(CLITestFixture):
1262
1281
  parentId=self.PARENT_ID,
1263
1282
  parentModule=self.PARENT_MODULE,
1264
1283
  dueDate=get_current_datetime(),
1284
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1265
1285
  ),
1266
1286
  Issue( # matches JIRA-4 (should be closed - jira closed but regscale open)
1267
1287
  id=4,
@@ -1272,32 +1292,41 @@ class TestJira(CLITestFixture):
1272
1292
  parentId=self.PARENT_ID,
1273
1293
  parentModule=self.PARENT_MODULE,
1274
1294
  dueDate=get_current_datetime(),
1295
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1275
1296
  ),
1276
1297
  ]
1277
1298
 
1278
1299
  # Create mock config with priority mappings from init.yaml
1279
1300
  config = {
1280
1301
  "issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
1281
- "maxThreads": 4, # Set to match number of issues to process
1282
- "userId": "123e4567-e89b-12d3-a456-426614174000", # Add a fake UUID for userId for issue owner
1302
+ "maxThreads": 4,
1303
+ "userId": "123e4567-e89b-12d3-a456-426614174000",
1283
1304
  }
1284
1305
  app = MagicMock()
1285
1306
  app.config = config
1286
1307
 
1287
1308
  # Setup mock return values
1288
1309
  mock_update_issue.return_value = MagicMock()
1289
- mock_insert_issue.return_value = MagicMock()
1310
+
1311
+ # Mock the creation of a new issue
1312
+ created_issue_mock = MagicMock()
1313
+ created_issue_mock.id = 2
1314
+ created_issue_mock.create.return_value = created_issue_mock
1315
+ mock_map_jira_to_regscale_issue.return_value = created_issue_mock
1290
1316
 
1291
1317
  with mock_job_progress_object as job_progress:
1292
1318
  test_task = job_progress.add_task(
1293
1319
  description="Processing issues",
1294
- total=len(jira_issues),
1320
+ total=4,
1295
1321
  visible=False,
1296
1322
  )
1297
1323
 
1298
- args = (
1299
- jira_issues, # Pass all issues at once
1324
+ # Test the function with each Jira issue individually (as ThreadManager would call it)
1325
+ # JIRA-1: existing issue with matching jiraId and attachments
1326
+ create_and_update_regscale_issues(
1327
+ jira_issue_1,
1300
1328
  regscale_issues,
1329
+ False,
1301
1330
  fetch_attachments,
1302
1331
  MagicMock(),
1303
1332
  app,
@@ -1307,17 +1336,56 @@ class TestJira(CLITestFixture):
1307
1336
  job_progress,
1308
1337
  )
1309
1338
 
1310
- # Test each thread
1311
- for thread in range(4):
1312
- create_and_update_regscale_issues(
1313
- args=args,
1314
- thread=thread,
1315
- )
1339
+ # JIRA-2: new issue (not in regscale_issues)
1340
+ create_and_update_regscale_issues(
1341
+ jira_issue_2,
1342
+ regscale_issues,
1343
+ False,
1344
+ fetch_attachments,
1345
+ MagicMock(),
1346
+ app,
1347
+ self.PARENT_ID,
1348
+ self.PARENT_MODULE,
1349
+ test_task,
1350
+ job_progress,
1351
+ )
1316
1352
 
1317
- assert mock_update_issue.call_count == 3 # JIRA-3, JIRA-4, and JIRA-1
1318
- assert mock_insert_issue.call_count == 1 # JIRA-2
1353
+ # JIRA-3: existing issue to be updated
1354
+ create_and_update_regscale_issues(
1355
+ jira_issue_3,
1356
+ regscale_issues,
1357
+ False,
1358
+ fetch_attachments,
1359
+ MagicMock(),
1360
+ app,
1361
+ self.PARENT_ID,
1362
+ self.PARENT_MODULE,
1363
+ test_task,
1364
+ job_progress,
1365
+ )
1366
+
1367
+ # JIRA-4: existing issue to be closed
1368
+ create_and_update_regscale_issues(
1369
+ jira_issue_4,
1370
+ regscale_issues,
1371
+ False,
1372
+ fetch_attachments,
1373
+ MagicMock(),
1374
+ app,
1375
+ self.PARENT_ID,
1376
+ self.PARENT_MODULE,
1377
+ test_task,
1378
+ job_progress,
1379
+ )
1380
+
1381
+ # Verify update_issue was called 3 times (JIRA-1, JIRA-3, JIRA-4)
1382
+ assert mock_update_issue.call_count == 3
1383
+ # Verify map_jira_to_regscale_issue was called once for JIRA-2 (new issue)
1384
+ assert mock_map_jira_to_regscale_issue.call_count == 1
1385
+ # Verify attachment handling
1319
1386
  if fetch_attachments:
1320
- assert mock_compare_files_for_dupes_and_upload.call_count == 1 # JIRA-2
1387
+ # Only JIRA-1 has attachments in fields.attachment
1388
+ assert mock_compare_files_for_dupes_and_upload.call_count == 1
1321
1389
  else:
1322
1390
  assert mock_compare_files_for_dupes_and_upload.call_count == 0
1323
1391
 
@@ -1335,6 +1403,7 @@ class TestJira(CLITestFixture):
1335
1403
  dueDate=get_current_datetime(),
1336
1404
  parentId=self.PARENT_ID,
1337
1405
  parentModule=self.PARENT_MODULE,
1406
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1338
1407
  ),
1339
1408
  Issue(
1340
1409
  id=3,
@@ -1345,6 +1414,7 @@ class TestJira(CLITestFixture):
1345
1414
  parentId=self.PARENT_ID,
1346
1415
  parentModule=self.PARENT_MODULE,
1347
1416
  jiraId="JIRA-3",
1417
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1348
1418
  ),
1349
1419
  ]
1350
1420
 
@@ -1459,6 +1529,7 @@ class TestJira(CLITestFixture):
1459
1529
  jiraId="JIRA-123",
1460
1530
  severityLevel="High",
1461
1531
  dueDate="2023-12-31",
1532
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1462
1533
  )
1463
1534
 
1464
1535
  comment = _generate_jira_comment(issue)
@@ -1493,6 +1564,7 @@ class TestJira(CLITestFixture):
1493
1564
  otherIdentifier="JIRA-123",
1494
1565
  percentComplete=50,
1495
1566
  dueDate="2023-12-31",
1567
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1496
1568
  )
1497
1569
 
1498
1570
  comment = _generate_jira_comment(task)
@@ -1596,6 +1668,7 @@ class TestJira(CLITestFixture):
1596
1668
  description="Test Description",
1597
1669
  parentId=self.PARENT_ID,
1598
1670
  parentModule=self.PARENT_MODULE,
1671
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1599
1672
  )
1600
1673
 
1601
1674
  # setup file hashes for upload
@@ -1671,6 +1744,7 @@ class TestJira(CLITestFixture):
1671
1744
  parentModule=self.PARENT_MODULE,
1672
1745
  dueDate=get_current_datetime(),
1673
1746
  status="Open",
1747
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1674
1748
  )
1675
1749
  reg_issue = tmp.create()
1676
1750
 
@@ -1782,29 +1856,345 @@ class TestJira(CLITestFixture):
1782
1856
  )
1783
1857
  assert os.path.exists(tmpdir) is True
1784
1858
 
1859
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
1860
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
1861
+ @patch(f"{PATH}.create_jira_client")
1862
+ @patch(f"{PATH}.fetch_jira_objects")
1863
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
1864
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
1865
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1866
+ def test_sync_regscale_and_jira_with_poams_true(
1867
+ self,
1868
+ mock_check_license,
1869
+ mock_api,
1870
+ mock_get_regscale_data_and_attachments,
1871
+ mock_fetch_jira_objects,
1872
+ mock_create_jira_client,
1873
+ mock_sync_regscale_to_jira,
1874
+ mock_sync_regscale_objects_to_jira,
1875
+ fetch_attachments,
1876
+ ):
1877
+ """Test sync_regscale_and_jira with use_poams=True"""
1878
+ # Setup mocks
1879
+ mock_jira_client = MagicMock()
1880
+ mock_create_jira_client.return_value = mock_jira_client
1881
+ mock_fetch_jira_objects.return_value = [MagicMock()]
1882
+ mock_get_regscale_data_and_attachments.return_value = ([MagicMock()], MagicMock())
1883
+
1884
+ # Call function with use_poams=True
1885
+ sync_regscale_and_jira(
1886
+ parent_id=self.PARENT_ID,
1887
+ parent_module=self.PARENT_MODULE,
1888
+ jira_project=self.JIRA_PROJECT,
1889
+ jira_issue_type="Bug",
1890
+ sync_attachments=fetch_attachments,
1891
+ use_poams=True,
1892
+ )
1893
+
1894
+ # Verify sync_regscale_objects_to_jira was called with use_poams=True
1895
+ mock_sync_regscale_objects_to_jira.assert_called_once()
1896
+ call_args = mock_sync_regscale_objects_to_jira.call_args
1897
+ assert call_args[0][7] is True # use_poams is the 8th positional argument
1898
+
1899
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
1900
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
1901
+ @patch(f"{PATH}.create_jira_client")
1902
+ @patch(f"{PATH}.fetch_jira_objects")
1903
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
1904
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
1905
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1906
+ def test_sync_regscale_and_jira_with_poams_false(
1907
+ self,
1908
+ mock_check_license,
1909
+ mock_api,
1910
+ mock_get_regscale_data_and_attachments,
1911
+ mock_fetch_jira_objects,
1912
+ mock_create_jira_client,
1913
+ mock_sync_regscale_to_jira,
1914
+ mock_sync_regscale_objects_to_jira,
1915
+ fetch_attachments,
1916
+ ):
1917
+ """Test sync_regscale_and_jira with use_poams=False (default)"""
1918
+ # Setup mocks
1919
+ mock_jira_client = MagicMock()
1920
+ mock_create_jira_client.return_value = mock_jira_client
1921
+ mock_fetch_jira_objects.return_value = [MagicMock()]
1922
+ mock_get_regscale_data_and_attachments.return_value = ([MagicMock()], MagicMock())
1923
+
1924
+ # Call function with use_poams=False (default)
1925
+ sync_regscale_and_jira(
1926
+ parent_id=self.PARENT_ID,
1927
+ parent_module=self.PARENT_MODULE,
1928
+ jira_project=self.JIRA_PROJECT,
1929
+ jira_issue_type="Bug",
1930
+ sync_attachments=fetch_attachments,
1931
+ use_poams=False,
1932
+ )
1933
+
1934
+ # Verify sync_regscale_objects_to_jira was called with use_poams=False
1935
+ mock_sync_regscale_objects_to_jira.assert_called_once()
1936
+ call_args = mock_sync_regscale_objects_to_jira.call_args
1937
+ assert call_args[0][7] is False # use_poams is the 8th positional argument
1938
+
1939
+ @patch(f"{PATH}.create_jira_client")
1940
+ @patch(f"{PATH}.create_and_update_regscale_issues")
1941
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1942
+ def test_sync_regscale_objects_to_jira_with_poams(
1943
+ self,
1944
+ mock_check_license,
1945
+ mock_create_and_update_regscale_issues,
1946
+ mock_create_jira_client,
1947
+ fetch_attachments,
1948
+ ):
1949
+ """Test sync_regscale_objects_to_jira passes use_poams to create_and_update_regscale_issues"""
1950
+ mock_check_license.return_value.config = self.config
1951
+ mock_jira_client = MagicMock()
1952
+ mock_create_jira_client.return_value = mock_jira_client
1953
+
1954
+ # Create mock application with ThreadManager
1955
+ mock_app = MagicMock(spec=Application)
1956
+ mock_app.config = self.config
1957
+ mock_thread_manager = MagicMock()
1958
+ mock_app.thread_manager = mock_thread_manager
1959
+
1960
+ # Mock Jira issues
1961
+ mock_jira_issues = [MagicMock(), MagicMock()]
1962
+ mock_regscale_objects = [MagicMock()]
1963
+
1964
+ # Test with use_poams=True
1965
+ sync_regscale_objects_to_jira(
1966
+ mock_jira_issues,
1967
+ mock_regscale_objects,
1968
+ fetch_attachments,
1969
+ mock_app,
1970
+ self.PARENT_ID,
1971
+ self.PARENT_MODULE,
1972
+ False, # sync_tasks_only
1973
+ True, # use_poams
1974
+ )
1975
+
1976
+ # Verify ThreadManager was called with correct parameters
1977
+ mock_thread_manager.submit_tasks_from_list.assert_called_once()
1978
+ call_args = mock_thread_manager.submit_tasks_from_list.call_args[0]
1979
+ # use_poams is the 4th argument (index 3) after function, jira_issues, and regscale_objects
1980
+ assert call_args[3] is True
1981
+
1982
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
1983
+ def test_map_jira_to_regscale_issue_with_poam_true(self, mock_map_jira_to_regscale_issue):
1984
+ """Test map_jira_to_regscale_issue sets isPoam=True when is_poam=True"""
1985
+ # Create mock Jira issue
1986
+ mock_issue = MagicMock()
1987
+ mock_issue.fields.summary = "Test Issue"
1988
+ mock_issue.fields.description = "Test Description"
1989
+ mock_issue.fields.status.name = "Open"
1990
+ mock_issue.fields.priority.name = "High"
1991
+ mock_issue.fields.duedate = None
1992
+ mock_issue.key = "TEST-123"
1993
+
1994
+ # Create mock config
1995
+ mock_config = {
1996
+ "userId": "1",
1997
+ "issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
1998
+ }
1999
+
2000
+ # Call the actual function with is_poam=True
2001
+ result = map_jira_to_regscale_issue(
2002
+ jira_issue=mock_issue,
2003
+ config=mock_config,
2004
+ parent_id=self.PARENT_ID,
2005
+ parent_module=self.PARENT_MODULE,
2006
+ is_poam=True,
2007
+ )
2008
+
2009
+ # Verify the Issue object was created with isPoam=True
2010
+ assert isinstance(result, Issue)
2011
+ assert result.isPoam is True
2012
+ assert result.title == "Test Issue"
2013
+ assert result.jiraId == "TEST-123"
2014
+
2015
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
2016
+ def test_map_jira_to_regscale_issue_with_poam_false(self, mock_map_jira_to_regscale_issue):
2017
+ """Test map_jira_to_regscale_issue sets isPoam=False when is_poam=False"""
2018
+ # Create mock Jira issue
2019
+ mock_issue = MagicMock()
2020
+ mock_issue.fields.summary = "Test Issue"
2021
+ mock_issue.fields.description = "Test Description"
2022
+ mock_issue.fields.status.name = "Open"
2023
+ mock_issue.fields.priority.name = "High"
2024
+ mock_issue.fields.duedate = None
2025
+ mock_issue.key = "TEST-123"
2026
+
2027
+ # Create mock config
2028
+ mock_config = {
2029
+ "userId": "1",
2030
+ "issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
2031
+ }
2032
+
2033
+ # Call the actual function with is_poam=False
2034
+ result = map_jira_to_regscale_issue(
2035
+ jira_issue=mock_issue,
2036
+ config=mock_config,
2037
+ parent_id=self.PARENT_ID,
2038
+ parent_module=self.PARENT_MODULE,
2039
+ is_poam=False,
2040
+ )
2041
+
2042
+ # Verify the Issue object was created with isPoam=False
2043
+ assert isinstance(result, Issue)
2044
+ assert result.isPoam is False
2045
+ assert result.title == "Test Issue"
2046
+ assert result.jiraId == "TEST-123"
2047
+
2048
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
2049
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
2050
+ @patch(f"{PATH}.Issue.update_issue")
2051
+ @patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
2052
+ def test_create_and_update_regscale_issues_sets_ispoam_on_new_issue(
2053
+ self,
2054
+ mock_job_progress_object,
2055
+ mock_update_issue,
2056
+ mock_map_jira_to_regscale_issue,
2057
+ mock_compare_files_for_dupes_and_upload,
2058
+ ):
2059
+ """Test that create_and_update_regscale_issues sets isPoam on newly created issues"""
2060
+ # Create mock Jira issue
2061
+ open_status = MagicMock()
2062
+ open_status.name = "open"
2063
+ high_priority = MagicMock()
2064
+ high_priority.name = "high"
2065
+
2066
+ jira_issue = MagicMock(
2067
+ key="JIRA-NEW",
2068
+ fields=MagicMock(
2069
+ summary="New Issue",
2070
+ description="New issue description",
2071
+ status=open_status,
2072
+ duedate=None,
2073
+ priority=high_priority,
2074
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
2075
+ attachment=None,
2076
+ ),
2077
+ )
2078
+
2079
+ # Create empty RegScale issues list (no existing issues)
2080
+ regscale_issues = []
2081
+
2082
+ # Create mock config
2083
+ config = {
2084
+ "issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
2085
+ "maxThreads": 4,
2086
+ "userId": "123e4567-e89b-12d3-a456-426614174000",
2087
+ }
2088
+ app = MagicMock()
2089
+ app.config = config
2090
+
2091
+ # Mock the creation of a new issue
2092
+ created_issue_mock = MagicMock()
2093
+ created_issue_mock.id = 999
2094
+ created_issue_mock.create.return_value = created_issue_mock
2095
+ mock_map_jira_to_regscale_issue.return_value = created_issue_mock
2096
+
2097
+ with mock_job_progress_object as job_progress:
2098
+ test_task = job_progress.add_task(
2099
+ description="Processing issues",
2100
+ total=1,
2101
+ visible=False,
2102
+ )
2103
+
2104
+ # Call with use_poams=True
2105
+ create_and_update_regscale_issues(
2106
+ jira_issue,
2107
+ regscale_issues,
2108
+ True, # use_poams
2109
+ False, # add_attachments
2110
+ MagicMock(),
2111
+ app,
2112
+ self.PARENT_ID,
2113
+ self.PARENT_MODULE,
2114
+ test_task,
2115
+ job_progress,
2116
+ )
2117
+
2118
+ # Verify map_jira_to_regscale_issue was called with is_poam=True
2119
+ mock_map_jira_to_regscale_issue.assert_called_once()
2120
+ call_kwargs = mock_map_jira_to_regscale_issue.call_args[1]
2121
+ assert call_kwargs["is_poam"] is True
2122
+
2123
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
2124
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
2125
+ @patch(f"{PATH}.Issue.save")
1785
2126
  @patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
1786
- def test_update_regscale_issue(self, mock_job_progress_object, regscale_issue_and_attachment):
1787
- """Test updating an issue in RegScale"""
1788
- test_issue = regscale_issue_and_attachment
1789
- # update fields in a RegScale issue
1790
- test_issue.assetIdentifier = f"Updated via Test on {get_current_datetime()}"
1791
- test_issue.lastUpdatedById = self.config["userId"]
2127
+ def test_create_and_update_regscale_issues_sets_ispoam_on_existing_issue(
2128
+ self,
2129
+ mock_job_progress_object,
2130
+ mock_save,
2131
+ mock_map_jira_to_regscale_issue,
2132
+ mock_compare_files_for_dupes_and_upload,
2133
+ ):
2134
+ """Test that create_and_update_regscale_issues sets isPoam on existing issues"""
2135
+ # Create mock Jira issue
2136
+ open_status = MagicMock()
2137
+ open_status.name = "open"
2138
+ high_priority = MagicMock()
2139
+ high_priority.name = "high"
2140
+
2141
+ jira_issue = MagicMock(
2142
+ key="JIRA-1",
2143
+ fields=MagicMock(
2144
+ summary="Existing Issue",
2145
+ description="Existing issue description",
2146
+ status=open_status,
2147
+ duedate=None,
2148
+ priority=high_priority,
2149
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
2150
+ attachment=None,
2151
+ ),
2152
+ )
2153
+
2154
+ # Create existing RegScale issue (using MagicMock to avoid actual creation)
2155
+ existing_issue = MagicMock(spec=Issue)
2156
+ existing_issue.jiraId = "JIRA-1"
2157
+ existing_issue.isPoam = False # Initially not a POAM
2158
+ existing_issue.id = 1
2159
+ existing_issue.title = "Existing Issue"
2160
+ regscale_issues = [existing_issue]
2161
+
2162
+ # Create mock config
2163
+ config = {
2164
+ "issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
2165
+ "maxThreads": 4,
2166
+ "userId": "123e4567-e89b-12d3-a456-426614174000",
2167
+ "jiraCustomFields": {},
2168
+ }
2169
+ app = MagicMock()
2170
+ app.config = config
2171
+
2172
+ # Setup mock return values
2173
+ mock_save.return_value = MagicMock()
2174
+
1792
2175
  with mock_job_progress_object as job_progress:
1793
2176
  test_task = job_progress.add_task(
1794
- description="Updating 1 issue(s) in RegScale...",
1795
- total=len([test_issue]),
2177
+ description="Processing issues",
2178
+ total=1,
1796
2179
  visible=False,
1797
2180
  )
1798
- update_regscale_issues(
1799
- args=([test_issue], test_task),
1800
- thread=0,
2181
+
2182
+ # Call with use_poams=True
2183
+ create_and_update_regscale_issues(
2184
+ jira_issue,
2185
+ regscale_issues,
2186
+ True, # use_poams
2187
+ False, # add_attachments
2188
+ MagicMock(),
2189
+ app,
2190
+ self.PARENT_ID,
2191
+ self.PARENT_MODULE,
2192
+ test_task,
2193
+ job_progress,
1801
2194
  )
1802
- # make sure the task was marked complete
1803
- assert test_task.finished
1804
- # make sure the issue was updated
1805
- updated_issue = Issue.fetch_issue_by_id(app=self.app, issue_id=test_issue.id)
1806
- assert updated_issue.assetIdentifier == test_issue.assetIdentifier
1807
- assert updated_issue.lastUpdatedById == test_issue.lastUpdatedById
2195
+
2196
+ # Verify the existing issue had isPoam set to True
2197
+ assert existing_issue.isPoam is True
1808
2198
 
1809
2199
  @staticmethod
1810
2200
  def teardown_class(cls):