regscale-cli 6.26.0.0__py3-none-any.whl → 6.27.0.1__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 (96) 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/__init__.py +0 -1
  6. regscale/integrations/commercial/jira.py +367 -126
  7. regscale/integrations/commercial/qualys/__init__.py +7 -8
  8. regscale/integrations/commercial/qualys/scanner.py +8 -3
  9. regscale/integrations/commercial/synqly/assets.py +17 -0
  10. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  11. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  12. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  13. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  14. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  15. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  16. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  17. regscale/integrations/commercial/wizv2/click.py +44 -59
  18. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  19. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  20. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  21. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  22. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  23. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  24. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  25. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  26. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  27. regscale/integrations/commercial/wizv2/issue.py +1 -1
  28. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  29. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  30. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  31. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  33. regscale/integrations/commercial/wizv2/reports.py +1 -1
  34. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  35. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  36. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  37. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  38. regscale/integrations/commercial/wizv2/variables.py +89 -3
  39. regscale/integrations/compliance_integration.py +0 -46
  40. regscale/integrations/control_matcher.py +22 -3
  41. regscale/integrations/due_date_handler.py +14 -8
  42. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  43. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  44. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  45. regscale/integrations/scanner_integration.py +127 -57
  46. regscale/models/integration_models/cisa_kev_data.json +132 -9
  47. regscale/models/integration_models/qualys.py +3 -4
  48. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  49. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  50. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  51. regscale/models/regscale_models/control_implementation.py +1 -1
  52. regscale/models/regscale_models/issue.py +0 -1
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/METADATA +1 -17
  54. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/RECORD +94 -61
  55. tests/regscale/integrations/commercial/test_jira.py +481 -91
  56. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  57. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  58. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  59. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  60. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  63. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  65. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  67. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  69. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  70. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  71. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  72. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  74. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  75. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  76. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  77. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  78. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  79. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  85. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  86. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  87. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  88. tests/regscale/integrations/public/test_fedramp.py +301 -0
  89. tests/regscale/integrations/test_control_matcher.py +83 -0
  90. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  91. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  92. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/LICENSE +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/WHEEL +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/entry_points.txt +0 -0
  96. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/top_level.txt +0 -0
@@ -8,11 +8,10 @@ import tempfile
8
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
9
  from datetime import datetime, timedelta
10
10
  from io import BytesIO
11
+ from pathlib import Path
11
12
  from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, Literal
12
13
  from urllib.parse import urljoin
13
14
 
14
- from pathlib import Path
15
-
16
15
  if TYPE_CHECKING:
17
16
  from regscale.core.app.application import Application
18
17
 
@@ -40,7 +39,7 @@ from regscale.models import regscale_id, regscale_module
40
39
  from regscale.models.regscale_models.file import File
41
40
  from regscale.models.regscale_models.issue import Issue
42
41
  from regscale.models.regscale_models.task import Task
43
- from regscale.utils.threading.threadhandler import create_threads, thread_assignment
42
+ from regscale.integrations.variables import ScannerVariables
44
43
 
45
44
  job_progress = create_progress_object()
46
45
  logger = create_logger()
@@ -104,6 +103,12 @@ def jira():
104
103
  help="Custom JQL query for filtering Jira issues.",
105
104
  required=False,
106
105
  )
106
+ @click.option(
107
+ "--poams",
108
+ "-p",
109
+ is_flag=True,
110
+ help="Whether to create/update the incoming issues from Jira as POAMs in RegScale.",
111
+ )
107
112
  def issues(
108
113
  regscale_id: int,
109
114
  regscale_module: str,
@@ -112,6 +117,7 @@ def issues(
112
117
  sync_attachments: bool = True,
113
118
  token_auth: bool = False,
114
119
  jql: Optional[str] = None,
120
+ poams: bool = False,
115
121
  ):
116
122
  """Sync issues from Jira into RegScale."""
117
123
  sync_regscale_and_jira(
@@ -122,6 +128,7 @@ def issues(
122
128
  sync_attachments=sync_attachments,
123
129
  token_auth=token_auth,
124
130
  jql=jql,
131
+ use_poams=poams,
125
132
  )
126
133
 
127
134
 
@@ -219,6 +226,7 @@ def sync_regscale_and_jira(
219
226
  sync_tasks_only: bool = False,
220
227
  token_auth: bool = False,
221
228
  jql: Optional[str] = None,
229
+ use_poams: Optional[bool] = False,
222
230
  ) -> None:
223
231
  """
224
232
  Sync issues, bidirectionally, from Jira into RegScale as issues
@@ -231,12 +239,19 @@ def sync_regscale_and_jira(
231
239
  :param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
232
240
  :param bool token_auth: Use token authentication for Jira API, defaults to False
233
241
  :param Optional[str] jql: Custom JQL query for filtering Jira issues/tasks, defaults to None
242
+ :param Optional[bool] use_poams: Whether to mark the incoming issues as POAMs in RegScale, defaults to False
234
243
  :rtype: None
235
244
  """
236
245
  app = check_license()
237
246
  api = Api()
238
247
  config = app.config
239
248
 
249
+ # Load custom fields configuration from init.yaml
250
+ if custom_fields := config.get("jiraCustomFields", {}):
251
+ logger.info("Custom field mappings loaded from config: %s", custom_fields)
252
+ else:
253
+ logger.debug("No custom field mappings found in configuration")
254
+
240
255
  # see if provided RegScale Module is an accepted option
241
256
  verify_provided_module(parent_module)
242
257
 
@@ -296,35 +311,20 @@ def sync_regscale_and_jira(
296
311
  api=api,
297
312
  sync_attachments=sync_attachments,
298
313
  attachments=regscale_attachments,
314
+ custom_fields=custom_fields,
299
315
  ):
300
- with job_progress:
301
- # create task to update RegScale issues
302
- updating_issues = job_progress.add_task(
303
- f"[#f8b737]Updating {len(regscale_objects_to_update)} RegScale {output_str}(s) from Jira...",
304
- total=len(regscale_objects_to_update),
305
- )
306
- # create threads to analyze Jira issues and RegScale issues
307
- create_threads(
308
- process=update_regscale_issues,
309
- args=(
310
- regscale_objects_to_update,
311
- updating_issues,
312
- ),
313
- thread_count=len(regscale_objects_to_update),
314
- )
315
- # output the final result
316
- logger.info(
317
- "%i/%i %s(s) updated in RegScale.",
318
- len(regscale_objects_to_update),
319
- len(update_counter),
320
- output_str,
321
- )
316
+ for regscale_object in regscale_objects_to_update:
317
+ regscale_object.save(bulk=True)
318
+ if isinstance(regscale_objects[0], Issue):
319
+ Issue.bulk_save()
320
+ elif isinstance(regscale_objects[0], Task):
321
+ Task.bulk_save()
322
322
  else:
323
323
  logger.info("No %s(s) need to be updated in RegScale.", output_str)
324
324
 
325
325
  if jira_objects:
326
326
  return sync_regscale_objects_to_jira(
327
- jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only
327
+ jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only, use_poams
328
328
  )
329
329
  logger.info("No %s need to be analyzed from Jira.", output_str)
330
330
 
@@ -337,6 +337,7 @@ def sync_regscale_objects_to_jira(
337
337
  parent_id: int,
338
338
  parent_module: str,
339
339
  sync_tasks_only: bool,
340
+ use_poams: Optional[bool] = False,
340
341
  ):
341
342
  """
342
343
  Sync issues from Jira to RegScale
@@ -348,6 +349,7 @@ def sync_regscale_objects_to_jira(
348
349
  :param int parent_id: Parent record ID in RegScale
349
350
  :param str parent_module: Parent record module in RegScale
350
351
  :param bool sync_tasks_only: Whether to sync only tasks from Jira
352
+ :param bool use_poams: Whether to create/update the incoming issues as POAMs in RegScale, defaults to False
351
353
  """
352
354
  issues_closed = []
353
355
  with job_progress:
@@ -369,21 +371,20 @@ def sync_regscale_objects_to_jira(
369
371
  progress_task=creating_issues,
370
372
  )
371
373
  else:
372
- create_threads(
373
- process=create_and_update_regscale_issues,
374
- args=(
375
- jira_issues,
376
- regscale_objects,
377
- sync_attachments,
378
- jira_client,
379
- app,
380
- parent_id,
381
- parent_module,
382
- creating_issues,
383
- job_progress,
384
- ),
385
- thread_count=len(jira_issues),
374
+ app.thread_manager.submit_tasks_from_list(
375
+ create_and_update_regscale_issues,
376
+ jira_issues,
377
+ regscale_objects,
378
+ use_poams,
379
+ sync_attachments,
380
+ jira_client,
381
+ app,
382
+ parent_id,
383
+ parent_module,
384
+ creating_issues,
385
+ job_progress,
386
386
  )
387
+ app.thread_manager.execute_and_verify(timeout=ScannerVariables.timeout)
387
388
  logger.info(
388
389
  "Analyzed %i Jira %s(s), created %i %s(s), updated %i %s(s), and closed %i %s(s) in RegScale.",
389
390
  len(jira_issues),
@@ -409,8 +410,6 @@ def create_jira_client(
409
410
  :return: JIRA Client
410
411
  :rtype: JIRA
411
412
  """
412
- from regscale.integrations.variables import ScannerVariables
413
-
414
413
  url = config["jiraUrl"]
415
414
  token = config["jiraApiToken"]
416
415
  jira_user = config["jiraUserName"]
@@ -421,36 +420,6 @@ def create_jira_client(
421
420
  return JIRA(basic_auth=(jira_user, token), options={"server": url})
422
421
 
423
422
 
424
- def update_regscale_issues(args: Tuple, thread: int) -> None:
425
- """
426
- Function to compare Jira issues and RegScale issues
427
-
428
- :param Tuple args: Tuple of args to use during the process
429
- :param int thread: Thread number of current thread
430
- :rtype: None
431
- """
432
- # set up local variables from the passed args
433
- (
434
- regscale_issues,
435
- task,
436
- ) = args
437
- # find which records should be executed by the current thread
438
- threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
439
- # iterate through the thread assignment items and process them
440
- for i in range(len(threads)):
441
- # set the issue for the thread for later use in the function
442
- issue = regscale_issues[threads[i]]
443
- # update the issue in RegScale
444
- issue.save()
445
- logger.debug(
446
- "RegScale Issue %i was updated with the Jira link.",
447
- issue.id,
448
- )
449
- update_counter.append(issue)
450
- # update progress bar
451
- job_progress.update(task, advance=1)
452
-
453
-
454
423
  def convert_task_status(name: str) -> str:
455
424
  """
456
425
  Convert the task status from Jira to RegScale
@@ -494,7 +463,7 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
494
463
  date_closed = status_change_date
495
464
  percent_complete = 100
496
465
 
497
- return Task(
466
+ task = Task(
498
467
  title=title,
499
468
  status=status,
500
469
  description=description,
@@ -507,6 +476,13 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
507
476
  extra_data={"jiraIssue": jira_issue}, # type: ignore
508
477
  )
509
478
 
479
+ # Apply custom field mappings from Jira to RegScale
480
+ custom_fields = config.get("jiraCustomFields", {})
481
+ if custom_fields:
482
+ apply_custom_fields_to_regscale_object(task, custom_fields, jira_issue)
483
+
484
+ return task
485
+
510
486
 
511
487
  def check_and_close_tasks(existing_tasks: list[Task], all_jira_titles: set[str]) -> list[Task]:
512
488
  """
@@ -567,9 +543,11 @@ def process_tasks_for_sync(
567
543
  jira_task = create_regscale_task_from_jira(config, jira_issue, parent_id, parent_module)
568
544
 
569
545
  # Check if we have a matching task in RegScale
570
- existing_task = existing_task_map.get(jira_issue.key)
546
+ if existing_task := existing_task_map.get(jira_issue.key):
547
+ # Apply custom field mappings from Jira to RegScale for existing tasks
548
+ if custom_fields := config.get("jiraCustomFields", {}):
549
+ apply_custom_fields_to_regscale_object(existing_task, custom_fields, jira_issue)
571
550
 
572
- if existing_task:
573
551
  # Check if task is closed in Jira and needs to be closed in regscale
574
552
  if jira_task.status in closed_statuses and existing_task.status not in closed_statuses:
575
553
  existing_task.status = "Closed"
@@ -681,64 +659,93 @@ def task_and_attachments_sync(
681
659
  )
682
660
 
683
661
 
684
- def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
662
+ def _create_new_regscale_issue(
663
+ jira_issue: jiraIssue, app: "Application", parent_id: int, parent_module: str, is_poam: Optional[bool] = False
664
+ ) -> Optional[Issue]:
665
+ """
666
+ Create a new RegScale issue from a Jira issue
667
+
668
+ :param jiraIssue jira_issue: The Jira issue to create from
669
+ :param Application app: RegScale application object
670
+ :param int parent_id: Parent record ID in RegScale
671
+ :param str parent_module: Parent record module in RegScale
672
+ :param bool is_poam: Whether to create the issue as a POAM in RegScale, defaults to False
673
+ :return: The created RegScale issue or None if creation failed
674
+ :rtype: Optional[Issue]
675
+ """
676
+ issue = map_jira_to_regscale_issue(
677
+ jira_issue=jira_issue,
678
+ config=app.config,
679
+ parent_id=parent_id,
680
+ parent_module=parent_module,
681
+ is_poam=is_poam,
682
+ )
683
+
684
+ if regscale_issue := issue.create():
685
+ logger.debug(
686
+ "Created issue #%i-%s in RegScale.",
687
+ regscale_issue.id,
688
+ regscale_issue.title,
689
+ )
690
+ return regscale_issue
691
+ else:
692
+ logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
693
+ return None
694
+
695
+
696
+ def _apply_custom_fields_and_update_issue(regscale_issue: Issue, app: "Application", jira_issue: jiraIssue) -> None:
697
+ """
698
+ Apply custom field mappings and update a RegScale issue
699
+
700
+ :param Issue regscale_issue: The RegScale issue to update
701
+ :param Application app: RegScale application object
702
+ :param jiraIssue jira_issue: The Jira issue to get data from
703
+ :rtype: None
704
+ """
705
+ if custom_fields := app.config.get("jiraCustomFields", {}):
706
+ apply_custom_fields_to_regscale_object(regscale_issue, custom_fields, jira_issue)
707
+ updated_regscale_issues.append(regscale_issue.save())
708
+
709
+
710
+ def create_and_update_regscale_issues(jira_issue: jiraIssue, *args, **_) -> None:
685
711
  """
686
712
  Function to create or update issues in RegScale from Jira
687
713
 
688
- :param Tuple args: Tuple of args to use during the process
689
- :param int thread: Thread number of current thread
714
+ :param jiraIssue jira_issue: Jira issue to create or update in RegScale
715
+ :param args: Additional arguments
690
716
  :rtype: None
691
717
  """
692
- # set up local variables from the passed args
693
- (jira_issues, regscale_issues, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
718
+ # set up local variables from the passed args Tuple
719
+ (regscale_issues, use_poams, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
694
720
  # find which records should be executed by the current thread
695
- threads = thread_assignment(thread=thread, total_items=len(jira_issues))
696
721
 
697
- # iterate through the thread assignment items and process them
698
- for i in range(len(threads)):
699
- jira_issue: jiraIssue = jira_issues[threads[i]]
700
- regscale_issue: Optional[Issue] = next(
701
- (issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None
722
+ regscale_issue: Optional[Issue] = next((issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None)
723
+ if regscale_issue:
724
+ regscale_issue.isPoam = use_poams
725
+
726
+ # Process the Jira issue based on its status and existing RegScale issue
727
+ if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
728
+ regscale_issue.status = "Closed"
729
+ regscale_issue.dateCompleted = get_current_datetime()
730
+ _apply_custom_fields_and_update_issue(regscale_issue, app, jira_issue)
731
+ elif regscale_issue:
732
+ _apply_custom_fields_and_update_issue(regscale_issue, app, jira_issue)
733
+ else:
734
+ regscale_issue = _create_new_regscale_issue(jira_issue, app, parent_id, parent_module, use_poams)
735
+ if regscale_issue:
736
+ new_regscale_issues.append(regscale_issue)
737
+
738
+ # Handle attachments if needed
739
+ if add_attachments and regscale_issue and jira_issue.fields.attachment:
740
+ compare_files_for_dupes_and_upload(
741
+ jira_issue=jira_issue,
742
+ regscale_object=regscale_issue,
743
+ jira_client=jira_client,
744
+ api=Api(),
702
745
  )
703
- # see if the Jira issue needs to be created in RegScale
704
- if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
705
- # update the status and date completed of the RegScale issue
706
- regscale_issue.status = "Closed"
707
- regscale_issue.dateCompleted = get_current_datetime()
708
- # update the issue in RegScale
709
- updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
710
- elif regscale_issue:
711
- # update the issue in RegScale
712
- updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
713
- else:
714
- # map the jira issue to a RegScale issue object
715
- issue = map_jira_to_regscale_issue(
716
- jira_issue=jira_issue,
717
- config=app.config,
718
- parent_id=parent_id,
719
- parent_module=parent_module,
720
- )
721
- # create the issue in RegScale
722
- if regscale_issue := issue.create():
723
- logger.debug(
724
- "Created issue #%i-%s in RegScale.",
725
- regscale_issue.id,
726
- regscale_issue.title,
727
- )
728
- new_regscale_issues.append(regscale_issue)
729
- else:
730
- logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
731
- if add_attachments and regscale_issue and jira_issue.fields.attachment:
732
- # determine which attachments need to be uploaded to prevent duplicates by
733
- # getting the hashes of all Jira & RegScale attachments
734
- compare_files_for_dupes_and_upload(
735
- jira_issue=jira_issue,
736
- regscale_object=regscale_issue,
737
- jira_client=jira_client,
738
- api=Api(),
739
- )
740
- # update progress bar
741
- progress.update(task, advance=1)
746
+
747
+ # update progress bar
748
+ progress.update(task, advance=1)
742
749
 
743
750
 
744
751
  def sync_regscale_to_jira(
@@ -749,6 +756,7 @@ def sync_regscale_to_jira(
749
756
  sync_attachments: bool = True,
750
757
  attachments: Optional[dict] = None,
751
758
  api: Optional[Api] = None,
759
+ custom_fields: Optional[dict] = None,
752
760
  ) -> list[Union[Issue, Task]]:
753
761
  """
754
762
  Sync issues or tasks from RegScale to Jira
@@ -760,6 +768,7 @@ def sync_regscale_to_jira(
760
768
  :param bool sync_attachments: Sync attachments from RegScale to Jira, defaults to True
761
769
  :param Optional[dict] attachments: Dict of attachments to sync from RegScale to Jira, defaults to None
762
770
  :param Optional[Api] api: API object to download attachments, defaults to None
771
+ :param Optional[dict] custom_fields: Custom field mappings from Jira custom fields to RegScale issue fields, defaults to None
763
772
  :return: list of RegScale issues or tasks that need to be updated
764
773
  :rtype: list[Union[Issue, Task]]
765
774
  """
@@ -787,6 +796,7 @@ def sync_regscale_to_jira(
787
796
  add_attachments=sync_attachments,
788
797
  attachments=attachments,
789
798
  api=api,
799
+ custom_fields=custom_fields,
790
800
  )
791
801
  # log progress
792
802
  new_issue_counter += 1
@@ -940,7 +950,9 @@ def save_jira_issues(jira_issues: list[jiraIssue], jira_project: str, jira_issue
940
950
  )
941
951
 
942
952
 
943
- def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str) -> Issue:
953
+ def map_jira_to_regscale_issue(
954
+ jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str, is_poam: Optional[bool] = False
955
+ ) -> Issue:
944
956
  """
945
957
  Map Jira issues to RegScale issues
946
958
 
@@ -948,6 +960,7 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
948
960
  :param dict config: Application config
949
961
  :param int parent_id: Parent record ID in RegScale
950
962
  :param str parent_module: Parent record module in RegScale
963
+ :param bool is_poam: Whether to create the issue as a POAM in RegScale
951
964
  :return: Issue object of the newly created issue in RegScale
952
965
  :rtype: Issue
953
966
  """
@@ -970,7 +983,14 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
970
983
  parentModule=parent_module,
971
984
  dateCreated=get_current_datetime(),
972
985
  dateCompleted=(get_current_datetime() if jira_issue.fields.status.name.lower() == "done" else None),
986
+ isPoam=is_poam,
973
987
  )
988
+
989
+ # Apply custom field mappings from Jira to RegScale
990
+ custom_fields = config.get("jiraCustomFields", {})
991
+ if custom_fields:
992
+ apply_custom_fields_to_regscale_object(issue, custom_fields, jira_issue)
993
+
974
994
  return issue
975
995
 
976
996
 
@@ -1024,6 +1044,7 @@ def create_issue_in_jira(
1024
1044
  add_attachments: Optional[bool] = True,
1025
1045
  attachments: Optional[dict] = None,
1026
1046
  api: Optional[Api] = None,
1047
+ custom_fields: Optional[dict] = None,
1027
1048
  ) -> jiraIssue:
1028
1049
  """
1029
1050
  Create a new issue in Jira
@@ -1035,6 +1056,7 @@ def create_issue_in_jira(
1035
1056
  :param Optional[bool] add_attachments: Whether to add attachments to new issue, defaults to true
1036
1057
  :param Optional[dict] attachments: Dictionary containing attachments, defaults to None
1037
1058
  :param Optional[Api] api: API object to download attachments, defaults to None
1059
+ :param Optional[dict] custom_fields: Custom field mappings from Jira custom fields to RegScale issue fields, defaults to None
1038
1060
  :return: Newly created issue in Jira
1039
1061
  :rtype: jiraIssue
1040
1062
  """
@@ -1050,6 +1072,11 @@ def create_issue_in_jira(
1050
1072
  issuetype=issue_type,
1051
1073
  )
1052
1074
  logger.debug("Jira issue created: %s", new_issue.key)
1075
+
1076
+ # Apply custom field mappings if provided
1077
+ if custom_fields:
1078
+ apply_custom_fields_to_jira_issue(new_issue, custom_fields, regscale_object)
1079
+
1053
1080
  # add a comment to the new Jira issue
1054
1081
  logger.debug("Adding comment to Jira issue: %s", new_issue.key)
1055
1082
  _ = jira_client.add_comment(
@@ -1309,3 +1336,217 @@ def download_regscale_attachments_to_directory(
1309
1336
  )
1310
1337
  )
1311
1338
  return jira_dir, regscale_dir
1339
+
1340
+
1341
+ def apply_custom_fields_to_jira_issue(
1342
+ jira_issue: jiraIssue, custom_fields: dict, regscale_object: Union[Issue, Task]
1343
+ ) -> None:
1344
+ """
1345
+ Apply custom field mappings to a Jira issue based on RegScale object attributes (RegScale -> Jira)
1346
+
1347
+ :param jiraIssue jira_issue: Jira issue to apply custom fields to
1348
+ :param dict custom_fields: Dictionary mapping Jira custom field names to RegScale attribute names
1349
+ :param Union[Issue, Task] regscale_object: RegScale object to get attribute values from
1350
+ :rtype: None
1351
+ """
1352
+ if not custom_fields:
1353
+ return
1354
+
1355
+ try:
1356
+ # Convert RegScale object to dictionary for easier attribute access
1357
+ if hasattr(regscale_object, "model_dump"):
1358
+ regscale_dict = regscale_object.model_dump()
1359
+ elif hasattr(regscale_object, "dict"):
1360
+ regscale_dict = regscale_object.dict()
1361
+ else:
1362
+ regscale_dict = regscale_object.__dict__
1363
+
1364
+ # Build custom fields dictionary for Jira update
1365
+ jira_custom_fields = {}
1366
+
1367
+ for jira_field_name, regscale_field_name in custom_fields.items():
1368
+ try:
1369
+ # Get the value from RegScale object
1370
+ field_value = regscale_dict.get(regscale_field_name)
1371
+
1372
+ if field_value is not None:
1373
+ jira_custom_fields[jira_field_name] = field_value
1374
+ logger.debug(
1375
+ "Mapped custom field %s (RegScale: %s) = %s for Jira issue %s",
1376
+ jira_field_name,
1377
+ regscale_field_name,
1378
+ field_value,
1379
+ jira_issue.key,
1380
+ )
1381
+ else:
1382
+ logger.debug(
1383
+ "Custom field %s (RegScale: %s) has no value, skipping for Jira issue %s",
1384
+ jira_field_name,
1385
+ regscale_field_name,
1386
+ jira_issue.key,
1387
+ )
1388
+ except Exception as e:
1389
+ logger.warning(
1390
+ "Unable to set custom field %s (RegScale: %s) for Jira issue %s: %s",
1391
+ jira_field_name,
1392
+ regscale_field_name,
1393
+ jira_issue.key,
1394
+ str(e),
1395
+ )
1396
+
1397
+ # Update the Jira issue with custom fields if any were found
1398
+ if jira_custom_fields:
1399
+ jira_issue.update(fields=jira_custom_fields)
1400
+ logger.info(
1401
+ "Applied %d custom fields to Jira issue %s: %s",
1402
+ len(jira_custom_fields),
1403
+ jira_issue.key,
1404
+ list(jira_custom_fields.keys()),
1405
+ )
1406
+ else:
1407
+ logger.debug("No custom field values found for Jira issue %s", jira_issue.key)
1408
+
1409
+ except Exception as e:
1410
+ logger.warning(
1411
+ "Error applying custom fields to Jira issue %s: %s",
1412
+ jira_issue.key,
1413
+ str(e),
1414
+ )
1415
+
1416
+
1417
+ def _get_jira_field_value(jira_issue: jiraIssue, jira_field_name: str) -> Optional[Any]:
1418
+ """
1419
+ Get a custom field value from a Jira issue
1420
+
1421
+ :param jiraIssue jira_issue: The Jira issue to get the field value from
1422
+ :param str jira_field_name: The name of the field to retrieve
1423
+ :return: The field value or None if not found
1424
+ :rtype: Optional[Any]
1425
+ """
1426
+ # Try to access the custom field from the Jira issue
1427
+ if hasattr(jira_issue.fields, jira_field_name):
1428
+ return getattr(jira_issue.fields, jira_field_name)
1429
+
1430
+ # Try accessing through raw fields (for custom fields)
1431
+ if hasattr(jira_issue.fields, "raw") and jira_field_name in jira_issue.fields.raw:
1432
+ return jira_issue.fields.raw[jira_field_name]
1433
+
1434
+ return None
1435
+
1436
+
1437
+ def _set_regscale_field_value(
1438
+ regscale_object: Union[Issue, Task],
1439
+ regscale_field_name: str,
1440
+ jira_field_value: Any,
1441
+ jira_field_name: str,
1442
+ jira_issue: jiraIssue,
1443
+ ) -> bool:
1444
+ """
1445
+ Set a field value on a RegScale object
1446
+
1447
+ :param Union[Issue, Task] regscale_object: The RegScale object to set the field on
1448
+ :param str regscale_field_name: The name of the field to set
1449
+ :param Any jira_field_value: The value to set
1450
+ :param str jira_field_name: The Jira field name for logging
1451
+ :param jiraIssue jira_issue: The Jira issue for logging
1452
+ :return: True if the field was set successfully, False otherwise
1453
+ :rtype: bool
1454
+ """
1455
+ if hasattr(regscale_object, regscale_field_name):
1456
+ setattr(regscale_object, regscale_field_name, jira_field_value)
1457
+ logger.debug(
1458
+ "Mapped custom field %s (Jira: %s) = %s for RegScale %s #%s from Jira issue %s",
1459
+ regscale_field_name,
1460
+ jira_field_name,
1461
+ jira_field_value,
1462
+ regscale_object.get_module_string().title(),
1463
+ regscale_object.id,
1464
+ jira_issue.key,
1465
+ )
1466
+ return True
1467
+ else:
1468
+ logger.debug(
1469
+ "RegScale object does not have field %s, skipping custom field %s from Jira issue %s",
1470
+ regscale_field_name,
1471
+ jira_field_name,
1472
+ jira_issue.key,
1473
+ )
1474
+ return False
1475
+
1476
+
1477
+ def _process_single_custom_field(
1478
+ jira_field_name: str, regscale_field_name: str, regscale_object: Union[Issue, Task], jira_issue: jiraIssue
1479
+ ) -> bool:
1480
+ """
1481
+ Process a single custom field mapping from Jira to RegScale
1482
+
1483
+ :param str jira_field_name: The Jira field name
1484
+ :param str regscale_field_name: The RegScale field name
1485
+ :param Union[Issue, Task] regscale_object: The RegScale object to update
1486
+ :param jiraIssue jira_issue: The Jira issue to get data from
1487
+ :return: True if the field was processed successfully, False otherwise
1488
+ :rtype: bool
1489
+ """
1490
+ try:
1491
+ jira_field_value = _get_jira_field_value(jira_issue, jira_field_name)
1492
+
1493
+ if jira_field_value is not None:
1494
+ return _set_regscale_field_value(
1495
+ regscale_object, regscale_field_name, jira_field_value, jira_field_name, jira_issue
1496
+ )
1497
+ else:
1498
+ logger.debug(
1499
+ "Custom field %s has no value in Jira issue %s, skipping",
1500
+ jira_field_name,
1501
+ jira_issue.key,
1502
+ )
1503
+ return False
1504
+ except Exception as e:
1505
+ logger.warning(
1506
+ "Unable to set custom field %s (Jira: %s) for RegScale %s #%s: %s",
1507
+ regscale_field_name,
1508
+ jira_field_name,
1509
+ regscale_object.get_module_string().title(),
1510
+ regscale_object.id,
1511
+ str(e),
1512
+ )
1513
+ return False
1514
+
1515
+
1516
+ def apply_custom_fields_to_regscale_object(
1517
+ regscale_object: Union[Issue, Task], custom_fields: dict, jira_issue: jiraIssue
1518
+ ) -> None:
1519
+ """
1520
+ Apply custom field mappings to a RegScale object based on Jira issue custom fields (Jira -> RegScale)
1521
+
1522
+ :param Union[Issue, Task] regscale_object: RegScale object to apply custom fields to
1523
+ :param dict custom_fields: Dictionary mapping Jira custom field names to RegScale attribute names
1524
+ :param jiraIssue jira_issue: Jira issue to get custom field values from
1525
+ :rtype: None
1526
+ """
1527
+ if not custom_fields:
1528
+ return
1529
+
1530
+ try:
1531
+ fields_updated = False
1532
+
1533
+ for jira_field_name, regscale_field_name in custom_fields.items():
1534
+ if _process_single_custom_field(jira_field_name, regscale_field_name, regscale_object, jira_issue):
1535
+ fields_updated = True
1536
+
1537
+ if fields_updated:
1538
+ logger.info(
1539
+ "Applied custom fields from Jira issue %s to RegScale %s #%s",
1540
+ jira_issue.key,
1541
+ regscale_object.get_module_string().title(),
1542
+ regscale_object.id,
1543
+ )
1544
+
1545
+ except Exception as e:
1546
+ logger.warning(
1547
+ "Error applying custom fields from Jira issue %s to RegScale %s #%s: %s",
1548
+ jira_issue.key,
1549
+ regscale_object.get_module_string().title(),
1550
+ regscale_object.id,
1551
+ str(e),
1552
+ )