regscale-cli 6.20.1.1__py3-none-any.whl → 6.20.3.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 (55) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/utils/variables.py +5 -3
  3. regscale/integrations/commercial/__init__.py +15 -0
  4. regscale/integrations/commercial/axonius/__init__.py +0 -0
  5. regscale/integrations/commercial/axonius/axonius_integration.py +70 -0
  6. regscale/integrations/commercial/burp.py +14 -0
  7. regscale/integrations/commercial/grype/commands.py +8 -1
  8. regscale/integrations/commercial/grype/scanner.py +2 -1
  9. regscale/integrations/commercial/jira.py +288 -137
  10. regscale/integrations/commercial/opentext/commands.py +14 -5
  11. regscale/integrations/commercial/opentext/scanner.py +3 -2
  12. regscale/integrations/commercial/qualys/__init__.py +3 -3
  13. regscale/integrations/commercial/stigv2/click_commands.py +6 -37
  14. regscale/integrations/commercial/synqly/assets.py +10 -0
  15. regscale/integrations/commercial/tenablev2/commands.py +12 -4
  16. regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
  17. regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
  18. regscale/integrations/commercial/trivy/commands.py +11 -4
  19. regscale/integrations/commercial/trivy/scanner.py +2 -1
  20. regscale/integrations/commercial/wizv2/constants.py +4 -0
  21. regscale/integrations/commercial/wizv2/scanner.py +67 -14
  22. regscale/integrations/commercial/wizv2/utils.py +24 -10
  23. regscale/integrations/commercial/wizv2/variables.py +7 -0
  24. regscale/integrations/jsonl_scanner_integration.py +8 -1
  25. regscale/integrations/public/cisa.py +58 -63
  26. regscale/integrations/public/fedramp/fedramp_cis_crm.py +153 -104
  27. regscale/integrations/scanner_integration.py +30 -8
  28. regscale/integrations/variables.py +1 -0
  29. regscale/models/app_models/click.py +49 -1
  30. regscale/models/app_models/import_validater.py +3 -1
  31. regscale/models/integration_models/axonius_models/__init__.py +0 -0
  32. regscale/models/integration_models/axonius_models/connectors/__init__.py +3 -0
  33. regscale/models/integration_models/axonius_models/connectors/assets.py +111 -0
  34. regscale/models/integration_models/burp.py +11 -8
  35. regscale/models/integration_models/cisa_kev_data.json +204 -23
  36. regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
  37. regscale/models/integration_models/jira_task_sync.py +27 -0
  38. regscale/models/integration_models/qualys.py +6 -7
  39. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  40. regscale/models/regscale_models/__init__.py +2 -1
  41. regscale/models/regscale_models/control_implementation.py +39 -2
  42. regscale/models/regscale_models/issue.py +1 -0
  43. regscale/models/regscale_models/regscale_model.py +49 -1
  44. regscale/models/regscale_models/risk_issue_mapping.py +61 -0
  45. regscale/models/regscale_models/task.py +1 -0
  46. regscale/regscale.py +1 -4
  47. regscale/utils/graphql_client.py +4 -4
  48. regscale/utils/string.py +13 -0
  49. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/METADATA +1 -1
  50. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/RECORD +54 -48
  51. regscale/integrations/commercial/synqly_jira.py +0 -840
  52. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/LICENSE +0 -0
  53. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/WHEEL +0 -0
  54. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/entry_points.txt +0 -0
  55. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/top_level.txt +0 -0
@@ -8,10 +8,11 @@ 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
12
- from typing import TYPE_CHECKING, Any, Optional, Tuple, Union
11
+ from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, Literal
13
12
  from urllib.parse import urljoin
14
13
 
14
+ from pathlib import Path
15
+
15
16
  if TYPE_CHECKING:
16
17
  from regscale.core.app.application import Application
17
18
 
@@ -21,6 +22,7 @@ from jira import Issue as jiraIssue
21
22
  from jira import JIRAError
22
23
  from rich.progress import Progress
23
24
 
25
+ from regscale.core.app.utils.parser_utils import safe_datetime_str
24
26
  from regscale.core.app.api import Api
25
27
  from regscale.core.app.logz import create_logger
26
28
  from regscale.core.app.utils.app_utils import (
@@ -125,6 +127,16 @@ def issues(
125
127
  prompt="Enter the name of the project in Jira",
126
128
  required=True,
127
129
  )
130
+ @click.option(
131
+ "--sync_attachments",
132
+ type=click.BOOL,
133
+ help=(
134
+ "Whether RegScale will sync the attachments for the issue "
135
+ "in the provided Jira project and vice versa. Defaults to True."
136
+ ),
137
+ required=False,
138
+ default=True,
139
+ )
128
140
  @click.option(
129
141
  "--token_auth",
130
142
  "-t",
@@ -135,6 +147,7 @@ def tasks(
135
147
  regscale_id: int,
136
148
  regscale_module: str,
137
149
  jira_project: str,
150
+ sync_attachments: bool = True,
138
151
  token_auth: bool = False,
139
152
  ):
140
153
  """Sync tasks from Jira into RegScale."""
@@ -143,12 +156,44 @@ def tasks(
143
156
  parent_module=regscale_module,
144
157
  jira_project=jira_project,
145
158
  jira_issue_type="Task",
146
- sync_attachments=False,
159
+ sync_attachments=sync_attachments,
147
160
  sync_tasks_only=True,
148
161
  token_auth=token_auth,
149
162
  )
150
163
 
151
164
 
165
+ def get_regscale_data_and_attachments(
166
+ parent_id: int, parent_module: str, sync_attachments: bool = True, sync_tasks_only: bool = False
167
+ ) -> Tuple[list[Union[Issue, Task]], dict[int, list[File]]]:
168
+ """
169
+ Get the RegScale data and attachments for the given parent ID and module
170
+
171
+ :param int parent_id: The ID of the parent
172
+ :param str parent_module: The module of the parent
173
+ :param bool sync_attachments: Whether to sync attachments
174
+ :param bool sync_tasks_only: Whether to sync tasks only
175
+ :return: Tuple of RegScale issues, RegScale attachments
176
+ :rtype: Tuple[list[Union[Issue, Task]], dict[int, list[File]]]
177
+ """
178
+ if sync_tasks_only and sync_attachments:
179
+ regscale_issues, regscale_attachments = Task.get_objects_and_attachments_by_parent(
180
+ parent_id=parent_id,
181
+ parent_module=parent_module,
182
+ )
183
+ elif sync_tasks_only and not sync_attachments:
184
+ regscale_issues = Task.get_all_by_parent(parent_id, parent_module)
185
+ regscale_attachments = []
186
+ elif sync_attachments:
187
+ regscale_issues, regscale_attachments = Issue.get_objects_and_attachments_by_parent(
188
+ parent_id=parent_id,
189
+ parent_module=parent_module,
190
+ )
191
+ else:
192
+ regscale_issues = Issue.get_all_by_parent(parent_id, parent_module)
193
+ regscale_attachments = []
194
+ return regscale_issues, regscale_attachments
195
+
196
+
152
197
  def sync_regscale_and_jira(
153
198
  parent_id: int,
154
199
  parent_module: str,
@@ -180,20 +225,36 @@ def sync_regscale_and_jira(
180
225
  # create Jira client
181
226
  jira_client = create_jira_client(config, token_auth)
182
227
 
183
- if sync_tasks_only:
184
- jql_str = f"project = {jira_project} AND issueType = {jira_issue_type}"
185
- regscale_issues = Task.get_all_by_parent(parent_id, parent_module)
186
- regscale_attachments = []
187
- else:
188
- jql_str = f"project = {jira_project}"
189
- (
190
- regscale_issues,
191
- regscale_attachments,
192
- ) = Issue.fetch_issues_and_attachments_by_parent(
193
- parent_id=parent_id,
194
- parent_module=parent_module,
195
- fetch_attachments=sync_attachments,
196
- )
228
+ jql_str = (
229
+ f"project = {jira_project} AND issueType = {jira_issue_type}"
230
+ if sync_tasks_only
231
+ else f"project = {jira_project}"
232
+ )
233
+ regscale_objects, regscale_attachments = get_regscale_data_and_attachments(
234
+ parent_id=parent_id,
235
+ parent_module=parent_module,
236
+ sync_attachments=sync_attachments,
237
+ sync_tasks_only=sync_tasks_only,
238
+ )
239
+
240
+ output_str = "task" if sync_tasks_only else "issue"
241
+
242
+ # write regscale data to a json file
243
+ check_file_path("artifacts")
244
+ file_name = f"existingRegScale{output_str}s.json"
245
+ file_path = Path("./artifacts") / file_name
246
+ save_data_to(
247
+ file=file_path,
248
+ data=[issue.dict() for issue in regscale_objects],
249
+ output_log=False,
250
+ )
251
+ logger.info(
252
+ "Saved RegScale %s(s) for %s #%i, see %s",
253
+ output_str,
254
+ parent_module,
255
+ parent_id,
256
+ str(file_path.absolute()),
257
+ )
197
258
 
198
259
  jira_objects = fetch_jira_objects(
199
260
  jira_client=jira_client,
@@ -203,10 +264,10 @@ def sync_regscale_and_jira(
203
264
  sync_tasks_only=sync_tasks_only,
204
265
  )
205
266
 
206
- if regscale_issues and not sync_tasks_only:
267
+ if regscale_objects:
207
268
  # sync RegScale issues to Jira
208
- if issues_to_update := sync_regscale_to_jira(
209
- regscale_issues=regscale_issues,
269
+ if regscale_objects_to_update := sync_regscale_to_jira(
270
+ regscale_objects=regscale_objects,
210
271
  jira_client=jira_client,
211
272
  jira_project=jira_project,
212
273
  jira_issue_type=jira_issue_type,
@@ -217,34 +278,33 @@ def sync_regscale_and_jira(
217
278
  with job_progress:
218
279
  # create task to update RegScale issues
219
280
  updating_issues = job_progress.add_task(
220
- f"[#f8b737]Updating {len(issues_to_update)} RegScale {jira_issue_type.lower()}(s) from Jira...",
221
- total=len(issues_to_update),
281
+ f"[#f8b737]Updating {len(regscale_objects_to_update)} RegScale {output_str}(s) from Jira...",
282
+ total=len(regscale_objects_to_update),
222
283
  )
223
284
  # create threads to analyze Jira issues and RegScale issues
224
285
  create_threads(
225
286
  process=update_regscale_issues,
226
287
  args=(
227
- issues_to_update,
228
- api,
288
+ regscale_objects_to_update,
229
289
  updating_issues,
230
290
  ),
231
- thread_count=len(issues_to_update),
291
+ thread_count=len(regscale_objects_to_update),
232
292
  )
233
293
  # output the final result
234
294
  logger.info(
235
- "%i/%i issue(s) updated in RegScale.",
236
- len(issues_to_update),
295
+ "%i/%i %s(s) updated in RegScale.",
296
+ len(regscale_objects_to_update),
237
297
  len(update_counter),
298
+ output_str,
238
299
  )
239
- elif not sync_tasks_only:
240
- logger.info("No issues need to be updated in RegScale.")
300
+ else:
301
+ logger.info("No %s(s) need to be updated in RegScale.", output_str)
241
302
 
242
303
  if jira_objects:
243
- sync_regscale_objects_to_jira(
244
- jira_objects, regscale_issues, sync_attachments, app, parent_id, parent_module, sync_tasks_only
304
+ return sync_regscale_objects_to_jira(
305
+ jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only
245
306
  )
246
- else:
247
- logger.info(f"No {'tasks' if sync_tasks_only else 'issues'} need to be analyzed from Jira.")
307
+ logger.info("No %s need to be analyzed from Jira.", output_str)
248
308
 
249
309
 
250
310
  def sync_regscale_objects_to_jira(
@@ -280,6 +340,7 @@ def sync_regscale_objects_to_jira(
280
340
  tasks_inserted, tasks_updated, tasks_closed = create_and_update_regscale_tasks(
281
341
  jira_issues=jira_issues,
282
342
  existing_tasks=regscale_objects,
343
+ jira_client=jira_client,
283
344
  parent_id=parent_id,
284
345
  parent_module=parent_module,
285
346
  progress=job_progress,
@@ -349,7 +410,6 @@ def update_regscale_issues(args: Tuple, thread: int) -> None:
349
410
  # set up local variables from the passed args
350
411
  (
351
412
  regscale_issues,
352
- app,
353
413
  task,
354
414
  ) = args
355
415
  # find which records should be executed by the current thread
@@ -425,6 +485,7 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
425
485
  dateClosed=date_closed,
426
486
  percentComplete=percent_complete,
427
487
  otherIdentifier=jira_issue.key,
488
+ extra_data={"jiraIssue": jira_issue}, # type: ignore
428
489
  )
429
490
 
430
491
 
@@ -447,72 +508,136 @@ def check_and_close_tasks(existing_tasks: list[Task], all_jira_titles: set[str])
447
508
  return close_tasks
448
509
 
449
510
 
450
- def create_and_update_regscale_tasks(
511
+ def process_tasks_for_sync(
512
+ config: dict,
451
513
  jira_issues: list[jiraIssue],
452
514
  existing_tasks: list[Task],
453
515
  parent_id: int,
454
516
  parent_module: str,
455
517
  progress: Progress,
456
518
  progress_task: Any,
457
- ) -> tuple[int, int, int]:
519
+ ) -> tuple[list[Task], list[Task], list[Task]]:
458
520
  """
459
- Function to create or update Tasks in RegScale from Jira
521
+ Function to create lists of Tasks that need to be created, updated, and closed in RegScale from Jira
460
522
 
523
+ :param dict config: Application config
461
524
  :param list[jiraIssue] jira_issues: List of Jira issues to create or update in RegScale
462
525
  :param list[Task] existing_tasks: List of existing tasks in RegScale
463
526
  :param int parent_id: Parent record ID in RegScale
464
527
  :param str parent_module: Parent record module in RegScale
465
528
  :param Progress progress: Job progress object to use for updating the progress bar
466
529
  :param Any progress_task: Task object to update the progress bar
467
- :return: A tuple of counts
468
- :rtype: tuple[int, int, int]
530
+ :return: A tuple of lists of Tasks to create, update, and close
531
+ :rtype: tuple[list[Task], list[Task], list[Task]]
469
532
  """
470
- from regscale.core.app.application import Application
471
-
472
- app = Application()
473
- config = app.config
533
+ closed_statuses = ["Closed", "Cancelled"]
474
534
  insert_tasks = []
475
535
  update_tasks = []
476
- all_jira_titles = {jira_issue.fields.summary for jira_issue in jira_issues}
536
+ close_tasks = []
477
537
  for jira_issue in jira_issues:
478
538
  task = create_regscale_task_from_jira(config, jira_issue, parent_id, parent_module)
479
- if task not in existing_tasks:
480
- # set due date to today if not provided
481
- insert_tasks.append(task)
482
- else:
483
- existing_task = next((t for t in existing_tasks if t == task), None)
539
+ existing_task = next((t for t in existing_tasks if t == task and not t.extra_data.get("updated", False)), None)
540
+ if existing_task:
484
541
  task.id = existing_task.id
485
- update_tasks.append(task)
542
+ if (jira_issue.fields.status.name.lower() == "done" and existing_task.status not in closed_statuses) or (
543
+ task.status in closed_statuses and task != existing_task
544
+ ):
545
+ task.status = "Closed"
546
+ task.percentComplete = 100
547
+ task.dateClosed = safe_datetime_str(jira_issue.fields.statuscategorychangedate)
548
+ close_tasks.append(task)
549
+ elif task != existing_task:
550
+ update_tasks.append(task)
551
+ else:
552
+ insert_tasks.append(task)
486
553
  progress.update(progress_task, advance=1)
487
- close_tasks = check_and_close_tasks(existing_tasks, all_jira_titles)
554
+ return insert_tasks, update_tasks, close_tasks
555
+
556
+
557
+ def create_and_update_regscale_tasks(
558
+ jira_issues: list[jiraIssue],
559
+ existing_tasks: list[Task],
560
+ jira_client: JIRA,
561
+ parent_id: int,
562
+ parent_module: str,
563
+ progress: Progress,
564
+ progress_task: Any,
565
+ ) -> tuple[int, int, int]:
566
+ """
567
+ Function to create or update Tasks in RegScale from Jira
488
568
 
569
+ :param list[jiraIssue] jira_issues: List of Jira issues to create or update in RegScale
570
+ :param list[Task] existing_tasks: List of existing tasks in RegScale
571
+ :param JIRA jira_client: Jira client to use for the request
572
+ :param int parent_id: Parent record ID in RegScale
573
+ :param str parent_module: Parent record module in RegScale
574
+ :param Progress progress: Job progress object to use for updating the progress bar
575
+ :param Any progress_task: Task object to update the progress bar
576
+ :return: A tuple of counts
577
+ :rtype: tuple[int, int, int]
578
+ """
579
+ from regscale.core.app.api import Api
580
+ from regscale.models.integration_models.jira_task_sync import TaskSync
581
+
582
+ api = Api()
583
+ insert_tasks, update_tasks, close_tasks = process_tasks_for_sync(
584
+ config=api.app.config,
585
+ jira_issues=jira_issues,
586
+ existing_tasks=existing_tasks,
587
+ parent_id=parent_id,
588
+ parent_module=parent_module,
589
+ progress=progress,
590
+ progress_task=progress_task,
591
+ )
592
+
593
+ task_sync_operations: list[TaskSync] = []
594
+ if insert_tasks:
595
+ task_sync_operations.append(TaskSync(insert_tasks, "create"))
596
+ if update_tasks:
597
+ task_sync_operations.append(TaskSync(update_tasks, "update"))
598
+ if close_tasks:
599
+ task_sync_operations.append(TaskSync(close_tasks, "close"))
489
600
  with progress:
490
601
  with ThreadPoolExecutor(max_workers=10) as executor:
491
- if insert_tasks:
492
- creating_tasks = progress.add_task(
493
- f"[#f8b737]Creating {len(insert_tasks)} task(s) in RegScale...",
494
- total=len(insert_tasks),
602
+ for task_sync_operation in task_sync_operations:
603
+ progress_task = progress.add_task(
604
+ task_sync_operation.progress_message, total=len(task_sync_operation.tasks)
495
605
  )
496
- create_futures = {executor.submit(task.create) for task in insert_tasks}
497
- for _ in as_completed(create_futures):
498
- progress.update(creating_tasks, advance=1)
499
- if update_tasks:
500
- update_task = progress.add_task(
501
- f"[#f8b737]Updating {len(update_tasks)} task(s) in RegScale...",
502
- total=len(update_tasks),
503
- )
504
- update_futures = {executor.submit(task.save) for task in update_tasks}
505
- for _ in as_completed(update_futures):
506
- progress.update(update_task, advance=1)
507
- if close_tasks:
508
- closing_tasks = progress.add_task(
509
- f"[#f8b737]Closing {len(close_tasks)} task(s) in RegScale...",
510
- total=len(close_tasks),
511
- )
512
- close_futures = {executor.submit(task.save) for task in close_tasks}
513
- for _ in as_completed(close_futures):
514
- progress.update(closing_tasks, advance=1)
515
- return len(insert_tasks), len(update_tasks), len(close_tasks)
606
+ task_futures = {
607
+ executor.submit(
608
+ task_and_attachments_sync, operation=task_sync_operation.operation, task=task, jira_client=jira_client, api=api # type: ignore
609
+ )
610
+ for task in task_sync_operation.tasks
611
+ }
612
+ for _ in as_completed(task_futures):
613
+ progress.update(progress_task, advance=1)
614
+ return len(insert_tasks), len(update_tasks) + len(update_counter), len(close_tasks)
615
+
616
+
617
+ def task_and_attachments_sync(
618
+ operation: Literal["create", "update", "close"], task: Task, jira_client: JIRA, api: Api
619
+ ) -> None:
620
+ """
621
+ Function to create, update and close tasks as well as attachments between RegScale and Jira
622
+
623
+ :param Literal["create", "update", "close"] operation: Operation to perform on the tasks
624
+ :param Task task: Task to perform the operation on
625
+ :param JIRA jira_client: Jira client to use for the request
626
+ :param Api api: API object to use for the request
627
+ :rtype: None
628
+ """
629
+ task_to_sync = None
630
+ if operation == "create":
631
+ task_to_sync = task.create()
632
+ elif operation in ["update", "close"]:
633
+ task_to_sync = task.save()
634
+ if task_to_sync:
635
+ compare_files_for_dupes_and_upload(
636
+ jira_issue=task.extra_data["jiraIssue"],
637
+ regscale_object=task_to_sync,
638
+ jira_client=jira_client,
639
+ api=api,
640
+ )
516
641
 
517
642
 
518
643
  def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
@@ -570,7 +695,7 @@ def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
570
695
  # getting the hashes of all Jira & RegScale attachments
571
696
  compare_files_for_dupes_and_upload(
572
697
  jira_issue=jira_issue,
573
- regscale_issue=regscale_issue,
698
+ regscale_object=regscale_issue,
574
699
  jira_client=jira_client,
575
700
  api=Api(),
576
701
  )
@@ -579,40 +704,45 @@ def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
579
704
 
580
705
 
581
706
  def sync_regscale_to_jira(
582
- regscale_issues: list[Issue],
707
+ regscale_objects: list[Union[Issue, Task]],
583
708
  jira_client: JIRA,
584
709
  jira_project: str,
585
710
  jira_issue_type: str,
586
711
  sync_attachments: bool = True,
587
712
  attachments: Optional[dict] = None,
588
713
  api: Optional[Api] = None,
589
- ) -> list[Issue]:
714
+ ) -> list[Union[Issue, Task]]:
590
715
  """
591
- Sync issues from RegScale to Jira
716
+ Sync issues or tasks from RegScale to Jira
592
717
 
593
- :param list[Issue] regscale_issues: list of RegScale issues to sync to Jira
718
+ :param list[Union[Issue, Task]] regscale_issues: list of RegScale issues or tasks to sync to Jira
594
719
  :param JIRA jira_client: Jira client to use for issue creation in Jira
595
720
  :param str jira_project: Jira Project to create the issues in
596
721
  :param str jira_issue_type: Type of issue to create in Jira
597
722
  :param bool sync_attachments: Sync attachments from RegScale to Jira, defaults to True
598
723
  :param Optional[dict] attachments: Dict of attachments to sync from RegScale to Jira, defaults to None
599
724
  :param Optional[Api] api: API object to download attachments, defaults to None
600
- :return: list of RegScale issues that need to be updated
601
- :rtype: list[Issue]
725
+ :return: list of RegScale issues or tasks that need to be updated
726
+ :rtype: list[Union[Issue, Task]]
602
727
  """
603
728
  new_issue_counter = 0
604
- issuess_to_update = []
729
+ regscale_objects_to_update = []
605
730
  with job_progress:
731
+ output_str = "issue" if jira_issue_type.lower() != "task" else "task"
606
732
  # create task to create Jira issues
607
733
  creating_issues = job_progress.add_task(
608
- f"[#f8b737]Verifying {len(regscale_issues)} RegScale issue(s) exist in Jira...",
609
- total=len(regscale_issues),
734
+ f"[#f8b737]Verifying {len(regscale_objects)} RegScale {output_str}(s) exist in Jira...",
735
+ total=len(regscale_objects),
610
736
  )
611
- for issue in regscale_issues:
612
- # see if Jira issue already exists
613
- if not issue.jiraId or issue.jiraId == "":
737
+ for regscale_object in regscale_objects:
738
+ if (
739
+ isinstance(regscale_object, Issue) and (not regscale_object.jiraId or regscale_object.jiraId == "")
740
+ ) or (
741
+ isinstance(regscale_object, Task)
742
+ and (not regscale_object.otherIdentifier or regscale_object.otherIdentifier == "")
743
+ ):
614
744
  new_issue = create_issue_in_jira(
615
- issue=issue,
745
+ regscale_object=regscale_object,
616
746
  jira_client=jira_client,
617
747
  jira_project=jira_project,
618
748
  issue_type=jira_issue_type,
@@ -625,13 +755,16 @@ def sync_regscale_to_jira(
625
755
  # get the Jira ID
626
756
  jira_id = new_issue.key
627
757
  # update the RegScale issue for the Jira link
628
- issue.jiraId = jira_id
758
+ if isinstance(regscale_object, Issue):
759
+ regscale_object.jiraId = jira_id
760
+ elif isinstance(regscale_object, Task):
761
+ regscale_object.otherIdentifier = jira_id
629
762
  # add the issue to the update_issues global list
630
- issuess_to_update.append(issue)
763
+ regscale_objects_to_update.append(regscale_object)
631
764
  job_progress.update(creating_issues, advance=1)
632
765
  # output the final result
633
- logger.info("%i new issue(s) opened in Jira.", new_issue_counter)
634
- return issuess_to_update
766
+ logger.info("%i new %s(s) opened in Jira.", new_issue_counter, output_str)
767
+ return regscale_objects_to_update
635
768
 
636
769
 
637
770
  def fetch_jira_objects(
@@ -747,24 +880,30 @@ def map_jira_due_date(jira_issue: Optional[jiraIssue], config: dict) -> str:
747
880
  return due_date
748
881
 
749
882
 
750
- def _generate_jira_comment(issue: Issue) -> str:
883
+ def _generate_jira_comment(regscale_object: Union[Issue, Task]) -> str:
751
884
  """
752
885
  Generate a Jira comment from a RegScale issue and it's populated fields
753
886
 
754
- :param Issue issue: RegScale issue to generate a Jira comment from
887
+ :param Union[Issue, Task] regscale_object: RegScale issue or task to generate a Jira comment from
755
888
  :return: Jira comment
756
889
  :rtype: str
757
890
  """
891
+ exclude_fields = [
892
+ "createdById",
893
+ "lastUpdatedById",
894
+ "issueOwnerId",
895
+ "assignedToId",
896
+ "uuid",
897
+ ] + regscale_object._exclude_graphql_fields
758
898
  comment = ""
759
- exclude_fields = ["createdById", "lastUpdatedById", "issueOwnerId", "uuid"] + issue._exclude_graphql_fields
760
- for field_name, field_value in issue.__dict__.items():
899
+ for field_name, field_value in regscale_object.__dict__.items():
761
900
  if field_value and field_name not in exclude_fields:
762
901
  comment += f"**{field_name}:** {field_value}\n"
763
902
  return comment
764
903
 
765
904
 
766
905
  def create_issue_in_jira(
767
- issue: Issue,
906
+ regscale_object: Union[Issue, Task],
768
907
  jira_client: JIRA,
769
908
  jira_project: str,
770
909
  issue_type: str,
@@ -775,7 +914,7 @@ def create_issue_in_jira(
775
914
  """
776
915
  Create a new issue in Jira
777
916
 
778
- :param Issue issue: RegScale issue object
917
+ :param Union[Issue, Task] regscale_object: RegScale issue or task object
779
918
  :param JIRA jira_client: Jira client to use for issue creation in Jira
780
919
  :param str jira_project: Project name in Jira to create the issue in
781
920
  :param str issue_type: The type of issue to create in Jira
@@ -788,12 +927,12 @@ def create_issue_in_jira(
788
927
  if not api:
789
928
  api = Api()
790
929
  try:
791
- reg_issue_url = f"RegScale Issue #{issue.id}: {urljoin(api.config['domain'], f'/form/issues/{issue.id}')}\n\n"
792
- logger.debug("Creating Jira issue: %s", issue.title)
930
+ regscale_object_url = f"RegScale {regscale_object.get_module_string().title()} #{regscale_object.id}: {urljoin(api.config['domain'], f'/form/{regscale_object.get_module_string()}/{regscale_object.id}')}\n\n"
931
+ logger.debug("Creating Jira issue: %s", regscale_object.title)
793
932
  new_issue = jira_client.create_issue(
794
933
  project=jira_project,
795
- summary=issue.title,
796
- description=reg_issue_url + issue.description,
934
+ summary=regscale_object.title,
935
+ description=regscale_object_url + regscale_object.description,
797
936
  issuetype=issue_type,
798
937
  )
799
938
  logger.debug("Jira issue created: %s", new_issue.key)
@@ -801,7 +940,7 @@ def create_issue_in_jira(
801
940
  logger.debug("Adding comment to Jira issue: %s", new_issue.key)
802
941
  _ = jira_client.add_comment(
803
942
  issue=new_issue,
804
- body=reg_issue_url + _generate_jira_comment(issue),
943
+ body=regscale_object_url + _generate_jira_comment(regscale_object),
805
944
  )
806
945
  logger.debug("Comment added to Jira issue: %s", new_issue.key)
807
946
  except JIRAError as ex:
@@ -810,7 +949,7 @@ def create_issue_in_jira(
810
949
  if add_attachments and attachments:
811
950
  compare_files_for_dupes_and_upload(
812
951
  jira_issue=new_issue,
813
- regscale_issue=issue,
952
+ regscale_object=regscale_object,
814
953
  jira_client=jira_client,
815
954
  api=api,
816
955
  )
@@ -818,13 +957,13 @@ def create_issue_in_jira(
818
957
 
819
958
 
820
959
  def compare_files_for_dupes_and_upload(
821
- jira_issue: jiraIssue, regscale_issue: Issue, jira_client: JIRA, api: Api
960
+ jira_issue: jiraIssue, regscale_object: Union[Issue, Task], jira_client: JIRA, api: Api
822
961
  ) -> None:
823
962
  """
824
963
  Compare files for duplicates and upload them to Jira and RegScale
825
964
 
826
965
  :param jiraIssue jira_issue: Jira issue to upload the attachments to
827
- :param Issue regscale_issue: RegScale issue to upload the attachments from
966
+ :param Union[Issue, Task] regscale_object: RegScale issue or task to upload the attachments from
828
967
  :param JIRA jira_client: Jira client to use for uploading the attachments
829
968
  :param Api api: Api object to use for interacting with RegScale
830
969
  :rtype: None
@@ -833,10 +972,10 @@ def compare_files_for_dupes_and_upload(
833
972
  jira_uploaded_attachments = []
834
973
  regscale_uploaded_attachments = []
835
974
  with tempfile.TemporaryDirectory() as temp_dir:
836
- jira_dir, regscale_dir = download_issue_attachments_to_directory(
975
+ jira_dir, regscale_dir = download_regscale_attachments_to_directory(
837
976
  directory=temp_dir,
838
977
  jira_issue=jira_issue,
839
- regscale_issue=regscale_issue,
978
+ regscale_object=regscale_object,
840
979
  api=api,
841
980
  )
842
981
  jira_attachment_hashes = compute_hashes_in_directory(jira_dir)
@@ -846,22 +985,22 @@ def compare_files_for_dupes_and_upload(
846
985
  jira_attachment_hashes,
847
986
  regscale_attachment_hashes,
848
987
  jira_issue,
849
- regscale_issue,
988
+ regscale_object,
850
989
  jira_client,
851
990
  jira_uploaded_attachments,
852
991
  )
853
992
  upload_files_to_regscale(
854
- jira_attachment_hashes, regscale_attachment_hashes, regscale_issue, api, regscale_uploaded_attachments
993
+ jira_attachment_hashes, regscale_attachment_hashes, regscale_object, api, regscale_uploaded_attachments
855
994
  )
856
995
 
857
- log_upload_results(regscale_uploaded_attachments, jira_uploaded_attachments, regscale_issue, jira_issue)
996
+ log_upload_results(regscale_uploaded_attachments, jira_uploaded_attachments, regscale_object, jira_issue)
858
997
 
859
998
 
860
999
  def upload_files_to_jira(
861
1000
  jira_attachment_hashes: dict,
862
1001
  regscale_attachment_hashes: dict,
863
1002
  jira_issue: jiraIssue,
864
- regscale_issue: Issue,
1003
+ regscale_object: Union[Issue, Task],
865
1004
  jira_client: JIRA,
866
1005
  jira_uploaded_attachments: list,
867
1006
  ) -> None:
@@ -871,7 +1010,7 @@ def upload_files_to_jira(
871
1010
  :param dict jira_attachment_hashes: Dictionary of Jira attachment hashes
872
1011
  :param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
873
1012
  :param jiraIssue jira_issue: Jira issue to upload the attachments to
874
- :param Issue regscale_issue: RegScale issue to upload the attachments from
1013
+ :param Union[Issue, Task] regscale_object: RegScale issue or task to upload the attachments from
875
1014
  :param JIRA jira_client: Jira client to use for uploading the attachments
876
1015
  :param list jira_uploaded_attachments: List of Jira attachments that were uploaded
877
1016
  :rtype: None
@@ -884,7 +1023,10 @@ def upload_files_to_jira(
884
1023
  jira_client.add_attachment(
885
1024
  issue=jira_issue.id,
886
1025
  attachment=BytesIO(in_file.read()), # type: ignore
887
- filename=f"RegScale_Issue_{regscale_issue.id}_{Path(file).name}",
1026
+ filename=(
1027
+ f"RegScale_{regscale_object.get_module_string().title()}_{regscale_object.id}_"
1028
+ f"{Path(file).name}"
1029
+ ),
888
1030
  )
889
1031
  jira_uploaded_attachments.append(file)
890
1032
  except JIRAError as ex:
@@ -906,7 +1048,7 @@ def upload_files_to_jira(
906
1048
  def upload_files_to_regscale(
907
1049
  jira_attachment_hashes: dict,
908
1050
  regscale_attachment_hashes: dict,
909
- regscale_issue: Issue,
1051
+ regscale_object: Union[Issue, Task],
910
1052
  api: Api,
911
1053
  regscale_uploaded_attachments: list,
912
1054
  ) -> None:
@@ -915,7 +1057,7 @@ def upload_files_to_regscale(
915
1057
 
916
1058
  :param dict jira_attachment_hashes: Dictionary of Jira attachment hashes
917
1059
  :param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
918
- :param Issue regscale_issue: RegScale issue to upload the attachments to
1060
+ :param Union[Issue, Task] regscale_object: RegScale issue or task to upload the attachments to
919
1061
  :param Api api: Api object to use for interacting with RegScale
920
1062
  :param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
921
1063
  :rtype: None
@@ -926,57 +1068,66 @@ def upload_files_to_regscale(
926
1068
  with open(file, "rb") as in_file:
927
1069
  if File.upload_file_to_regscale(
928
1070
  file_name=f"Jira_attachment_{Path(file).name}",
929
- parent_id=regscale_issue.id,
930
- parent_module="issues",
1071
+ parent_id=regscale_object.id,
1072
+ parent_module=regscale_object.get_module_string(),
931
1073
  api=api,
932
1074
  file_data=in_file.read(),
933
1075
  ):
934
1076
  regscale_uploaded_attachments.append(file)
935
1077
  logger.debug(
936
- "Uploaded %s to RegScale issue #%i.",
1078
+ "Uploaded %s to RegScale %s #%i.",
937
1079
  Path(file).name,
938
- regscale_issue.id,
1080
+ regscale_object.get_module_string().title(),
1081
+ regscale_object.id,
939
1082
  )
940
1083
  else:
941
1084
  logger.warning(
942
- "Unable to upload %s to RegScale issue #%i.",
1085
+ "Unable to upload %s to RegScale %s #%i.",
943
1086
  Path(file).name,
944
- regscale_issue.id,
1087
+ regscale_object.get_module_string().title(),
1088
+ regscale_object.id,
945
1089
  )
946
1090
 
947
1091
 
948
1092
  def log_upload_results(
949
- regscale_uploaded_attachments: list, jira_uploaded_attachments: list, regscale_issue: Issue, jira_issue: jiraIssue
1093
+ regscale_uploaded_attachments: list,
1094
+ jira_uploaded_attachments: list,
1095
+ regscale_object: Union[Issue, Task],
1096
+ jira_issue: jiraIssue,
950
1097
  ) -> None:
951
1098
  """
952
1099
  Log the results of the upload process
953
1100
 
954
1101
  :param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
955
1102
  :param list jira_uploaded_attachments: List of Jira attachments that were uploaded
956
- :param Issue regscale_issue: RegScale issue that the attachments were uploaded to
1103
+ :param Union[Issue, Task] regscale_object: RegScale issue or task that the attachments were uploaded to
957
1104
  :param jiraIssue jira_issue: Jira issue that the attachments were uploaded to
958
1105
  :rtype: None
959
1106
  :return: None
960
1107
  """
961
1108
  if regscale_uploaded_attachments and jira_uploaded_attachments:
962
1109
  logger.info(
963
- "%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to Jira issue %s.",
1110
+ "%i file(s) uploaded to RegScale %s #%i and %i file(s) uploaded to Jira %s %s.",
964
1111
  len(regscale_uploaded_attachments),
965
- regscale_issue.id,
1112
+ regscale_object.get_module_string().title(),
1113
+ regscale_object.id,
966
1114
  len(jira_uploaded_attachments),
1115
+ jira_issue.fields.issuetype.name,
967
1116
  jira_issue.key,
968
1117
  )
969
1118
  elif jira_uploaded_attachments:
970
1119
  logger.info(
971
- "%i file(s) uploaded to Jira issue %s.",
1120
+ "%i file(s) uploaded to Jira %s %s.",
972
1121
  len(jira_uploaded_attachments),
1122
+ jira_issue.fields.issuetype.name,
973
1123
  jira_issue.key,
974
1124
  )
975
1125
  elif regscale_uploaded_attachments:
976
1126
  logger.info(
977
- "%i file(s) uploaded to RegScale issue #%i.",
1127
+ "%i file(s) uploaded to RegScale %s #%i.",
978
1128
  len(regscale_uploaded_attachments),
979
- regscale_issue.id,
1129
+ regscale_object.get_module_string().title(),
1130
+ regscale_object.id,
980
1131
  )
981
1132
 
982
1133
 
@@ -999,10 +1150,10 @@ def validate_issue_type(jira_client: JIRA, issue_type: str) -> Any:
999
1150
  error_and_exit(error_desc=message)
1000
1151
 
1001
1152
 
1002
- def download_issue_attachments_to_directory(
1153
+ def download_regscale_attachments_to_directory(
1003
1154
  directory: str,
1004
1155
  jira_issue: jiraIssue,
1005
- regscale_issue: Issue,
1156
+ regscale_object: Union[Issue, Task],
1006
1157
  api: Api,
1007
1158
  ) -> tuple[str, str]:
1008
1159
  """
@@ -1010,7 +1161,7 @@ def download_issue_attachments_to_directory(
1010
1161
 
1011
1162
  :param str directory: Directory to store the files in
1012
1163
  :param jiraIssue jira_issue: Jira issue to download the attachments for
1013
- :param Issue regscale_issue: RegScale issue to download the attachments for
1164
+ :param Union[Issue, Task] regscale_object: RegScale issue or task to download the attachments for
1014
1165
  :param Api api: Api object to use for interacting with RegScale
1015
1166
  :return: Tuple of strings containing the Jira and RegScale directories
1016
1167
  :rtype: tuple[str, str]
@@ -1025,8 +1176,8 @@ def download_issue_attachments_to_directory(
1025
1176
  # get the regscale issue attachments
1026
1177
  regscale_issue_attachments = File.get_files_for_parent_from_regscale(
1027
1178
  api=api,
1028
- parent_id=regscale_issue.id,
1029
- parent_module="issues",
1179
+ parent_id=regscale_object.id,
1180
+ parent_module=regscale_object.get_module_string(),
1030
1181
  )
1031
1182
  # create a directory for the regscale attachments
1032
1183
  regscale_dir = os.path.join(directory, "regscale")
@@ -1037,8 +1188,8 @@ def download_issue_attachments_to_directory(
1037
1188
  file.write(
1038
1189
  File.download_file_from_regscale_to_memory(
1039
1190
  api=api,
1040
- record_id=regscale_issue.id,
1041
- module="issues",
1191
+ record_id=regscale_object.id,
1192
+ module=regscale_object.get_module_string(),
1042
1193
  stored_name=attachment.trustedStorageName,
1043
1194
  file_hash=(attachment.fileHash if attachment.fileHash else attachment.shaHash),
1044
1195
  )