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.
- regscale/__init__.py +1 -1
- regscale/core/app/utils/variables.py +5 -3
- regscale/integrations/commercial/__init__.py +15 -0
- regscale/integrations/commercial/axonius/__init__.py +0 -0
- regscale/integrations/commercial/axonius/axonius_integration.py +70 -0
- regscale/integrations/commercial/burp.py +14 -0
- regscale/integrations/commercial/grype/commands.py +8 -1
- regscale/integrations/commercial/grype/scanner.py +2 -1
- regscale/integrations/commercial/jira.py +288 -137
- regscale/integrations/commercial/opentext/commands.py +14 -5
- regscale/integrations/commercial/opentext/scanner.py +3 -2
- regscale/integrations/commercial/qualys/__init__.py +3 -3
- regscale/integrations/commercial/stigv2/click_commands.py +6 -37
- regscale/integrations/commercial/synqly/assets.py +10 -0
- regscale/integrations/commercial/tenablev2/commands.py +12 -4
- regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
- regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
- regscale/integrations/commercial/trivy/commands.py +11 -4
- regscale/integrations/commercial/trivy/scanner.py +2 -1
- regscale/integrations/commercial/wizv2/constants.py +4 -0
- regscale/integrations/commercial/wizv2/scanner.py +67 -14
- regscale/integrations/commercial/wizv2/utils.py +24 -10
- regscale/integrations/commercial/wizv2/variables.py +7 -0
- regscale/integrations/jsonl_scanner_integration.py +8 -1
- regscale/integrations/public/cisa.py +58 -63
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +153 -104
- regscale/integrations/scanner_integration.py +30 -8
- regscale/integrations/variables.py +1 -0
- regscale/models/app_models/click.py +49 -1
- regscale/models/app_models/import_validater.py +3 -1
- regscale/models/integration_models/axonius_models/__init__.py +0 -0
- regscale/models/integration_models/axonius_models/connectors/__init__.py +3 -0
- regscale/models/integration_models/axonius_models/connectors/assets.py +111 -0
- regscale/models/integration_models/burp.py +11 -8
- regscale/models/integration_models/cisa_kev_data.json +204 -23
- regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
- regscale/models/integration_models/jira_task_sync.py +27 -0
- regscale/models/integration_models/qualys.py +6 -7
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +2 -1
- regscale/models/regscale_models/control_implementation.py +39 -2
- regscale/models/regscale_models/issue.py +1 -0
- regscale/models/regscale_models/regscale_model.py +49 -1
- regscale/models/regscale_models/risk_issue_mapping.py +61 -0
- regscale/models/regscale_models/task.py +1 -0
- regscale/regscale.py +1 -4
- regscale/utils/graphql_client.py +4 -4
- regscale/utils/string.py +13 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/RECORD +54 -48
- regscale/integrations/commercial/synqly_jira.py +0 -840
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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=
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
267
|
+
if regscale_objects:
|
|
207
268
|
# sync RegScale issues to Jira
|
|
208
|
-
if
|
|
209
|
-
|
|
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(
|
|
221
|
-
total=len(
|
|
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
|
-
|
|
228
|
-
api,
|
|
288
|
+
regscale_objects_to_update,
|
|
229
289
|
updating_issues,
|
|
230
290
|
),
|
|
231
|
-
thread_count=len(
|
|
291
|
+
thread_count=len(regscale_objects_to_update),
|
|
232
292
|
)
|
|
233
293
|
# output the final result
|
|
234
294
|
logger.info(
|
|
235
|
-
"%i/%i
|
|
236
|
-
len(
|
|
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
|
-
|
|
240
|
-
logger.info("No
|
|
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,
|
|
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
|
-
|
|
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
|
|
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[
|
|
519
|
+
) -> tuple[list[Task], list[Task], list[Task]]:
|
|
458
520
|
"""
|
|
459
|
-
Function to create
|
|
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
|
|
468
|
-
:rtype: tuple[
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
app = Application()
|
|
473
|
-
config = app.config
|
|
533
|
+
closed_statuses = ["Closed", "Cancelled"]
|
|
474
534
|
insert_tasks = []
|
|
475
535
|
update_tasks = []
|
|
476
|
-
|
|
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
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
609
|
-
total=len(
|
|
734
|
+
f"[#f8b737]Verifying {len(regscale_objects)} RegScale {output_str}(s) exist in Jira...",
|
|
735
|
+
total=len(regscale_objects),
|
|
610
736
|
)
|
|
611
|
-
for
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
634
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
792
|
-
logger.debug("Creating Jira issue: %s",
|
|
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=
|
|
796
|
-
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=
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
975
|
+
jira_dir, regscale_dir = download_regscale_attachments_to_directory(
|
|
837
976
|
directory=temp_dir,
|
|
838
977
|
jira_issue=jira_issue,
|
|
839
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
|
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=
|
|
930
|
-
parent_module=
|
|
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
|
|
1078
|
+
"Uploaded %s to RegScale %s #%i.",
|
|
937
1079
|
Path(file).name,
|
|
938
|
-
|
|
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
|
|
1085
|
+
"Unable to upload %s to RegScale %s #%i.",
|
|
943
1086
|
Path(file).name,
|
|
944
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1127
|
+
"%i file(s) uploaded to RegScale %s #%i.",
|
|
978
1128
|
len(regscale_uploaded_attachments),
|
|
979
|
-
|
|
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
|
|
1153
|
+
def download_regscale_attachments_to_directory(
|
|
1003
1154
|
directory: str,
|
|
1004
1155
|
jira_issue: jiraIssue,
|
|
1005
|
-
|
|
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
|
|
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=
|
|
1029
|
-
parent_module=
|
|
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=
|
|
1041
|
-
module=
|
|
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
|
)
|