regscale-cli 6.20.1.1__py3-none-any.whl → 6.20.2.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 (39) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/utils/variables.py +5 -3
  3. regscale/integrations/commercial/__init__.py +2 -0
  4. regscale/integrations/commercial/burp.py +14 -0
  5. regscale/integrations/commercial/grype/commands.py +8 -1
  6. regscale/integrations/commercial/grype/scanner.py +2 -1
  7. regscale/integrations/commercial/jira.py +290 -133
  8. regscale/integrations/commercial/opentext/commands.py +14 -5
  9. regscale/integrations/commercial/opentext/scanner.py +3 -2
  10. regscale/integrations/commercial/qualys/__init__.py +3 -3
  11. regscale/integrations/commercial/stigv2/click_commands.py +6 -37
  12. regscale/integrations/commercial/tenablev2/commands.py +12 -4
  13. regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
  14. regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
  15. regscale/integrations/commercial/trivy/commands.py +11 -4
  16. regscale/integrations/commercial/trivy/scanner.py +2 -1
  17. regscale/integrations/jsonl_scanner_integration.py +8 -1
  18. regscale/integrations/public/cisa.py +58 -63
  19. regscale/integrations/public/fedramp/fedramp_cis_crm.py +88 -93
  20. regscale/integrations/scanner_integration.py +22 -6
  21. regscale/models/app_models/click.py +49 -1
  22. regscale/models/integration_models/burp.py +11 -8
  23. regscale/models/integration_models/cisa_kev_data.json +142 -21
  24. regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
  25. regscale/models/integration_models/jira_task_sync.py +27 -0
  26. regscale/models/integration_models/qualys.py +6 -7
  27. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  28. regscale/models/regscale_models/control_implementation.py +39 -2
  29. regscale/models/regscale_models/regscale_model.py +49 -1
  30. regscale/models/regscale_models/task.py +1 -0
  31. regscale/regscale.py +1 -4
  32. regscale/utils/string.py +13 -0
  33. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.2.0.dist-info}/METADATA +1 -1
  34. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.2.0.dist-info}/RECORD +38 -38
  35. regscale/integrations/commercial/synqly_jira.py +0 -840
  36. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.2.0.dist-info}/LICENSE +0 -0
  37. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.2.0.dist-info}/WHEEL +0 -0
  38. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.2.0.dist-info}/entry_points.txt +0 -0
  39. {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.2.0.dist-info}/top_level.txt +0 -0
@@ -1,840 +0,0 @@
1
- """
2
- Synqly Python SDK Ticketing Example
3
-
4
- This example demonstrates how to use the Synqly Python SDK to create a
5
- Ticketing Integration for a tenant.
6
- """
7
-
8
- # Standard imports
9
- import base64
10
- import os
11
- import tempfile
12
- from datetime import datetime
13
- from typing import Tuple, Optional
14
-
15
- import click
16
- from pathlib import Path
17
- from synqly import engine, management as mgmt
18
- from synqly.engine import CreateTicketRequest
19
- from synqly.engine.resources.ticketing.types.priority import Priority
20
- from synqly.engine.resources.ticketing.types.ticket import Ticket
21
-
22
- import regscale.utils.synqly_utils as utils
23
- from regscale.core.app.api import Api
24
- from regscale.core.app.application import Application
25
- from regscale.core.app.utils.app_utils import (
26
- create_progress_object,
27
- create_logger,
28
- get_current_datetime,
29
- convert_datetime_to_regscale_string,
30
- error_and_exit,
31
- check_file_path,
32
- compute_hashes_in_directory,
33
- )
34
- from regscale.utils.threading.threadhandler import create_threads, thread_assignment
35
- from regscale.models import regscale_id, regscale_module, File
36
- from regscale.models.regscale_models.issue import Issue
37
-
38
- job_progress = create_progress_object()
39
- logger = create_logger()
40
- update_issues = []
41
- new_regscale_issues = []
42
- updated_regscale_issues = []
43
- update_counter = []
44
-
45
-
46
- @click.group()
47
- def synqly():
48
- """
49
- Sync RegScale issues with Jira issues using Synqly
50
- """
51
-
52
-
53
- def jira_provider_config(jira_url: str, jira_username: str, jira_token: str) -> mgmt.ProviderConfig_TicketingJira:
54
- """
55
- Helper method to construct a JIRA ProviderConfig object.
56
-
57
- :param str jira_url: JIRA URL
58
- :param str jira_username: JIRA username
59
- :param str jira_token: JIRA token
60
- :return: JIRA ProviderConfig object
61
- :rtype: mgmt.ProviderConfig_TicketingJira
62
- """
63
- return mgmt.ProviderConfig_TicketingJira(
64
- type="ticketing_jira",
65
- url=jira_url,
66
- credential=mgmt.JiraCredential_Basic(
67
- type="basic",
68
- username=jira_username,
69
- secret=jira_token,
70
- ),
71
- )
72
-
73
-
74
- def download_issue_attachments_to_directory(
75
- directory: str,
76
- synqly_issue: Ticket,
77
- regscale_issue: Issue,
78
- api: Api,
79
- synqly_client: utils.Tenant,
80
- ) -> tuple[str, str]:
81
- """
82
- Function to download attachments from Jira and RegScale issues to a directory
83
-
84
- :param str directory: Directory to store the files in
85
- :param Ticket synqly_issue: Synqly issue to download the attachments for
86
- :param Issue regscale_issue: RegScale issue to download the attachments for
87
- :param Api api: Api object to use for interacting with RegScale
88
- :param utils.Tenant synqly_client: Synqly client to use for uploading attachments
89
- :return: Tuple of strings containing the Jira and RegScale directories
90
- :rtype: tuple[str, str]
91
- """
92
- # determine which attachments need to be uploaded to prevent duplicates by checking hashes
93
- jira_dir = os.path.join(directory, "jira")
94
- check_file_path(jira_dir, False)
95
- # download all attachments from Jira to the jira directory in temp_dir
96
- download_synqly_attachments(tenant=synqly_client, ticket_id=synqly_issue.id, download_dir=jira_dir)
97
- # get the regscale issue attachments
98
- regscale_issue_attachments = File.get_files_for_parent_from_regscale(
99
- api=api,
100
- parent_id=regscale_issue.id,
101
- parent_module="issues",
102
- )
103
- # create a directory for the regscale attachments
104
- regscale_dir = os.path.join(directory, "regscale")
105
- check_file_path(regscale_dir, False)
106
- # download regscale attachments to the directory
107
- for attachment in regscale_issue_attachments:
108
- with open(os.path.join(regscale_dir, attachment.trustedDisplayName), "wb") as file:
109
- file.write(
110
- File.download_file_from_regscale_to_memory(
111
- api=api,
112
- record_id=regscale_issue.id,
113
- module="issues",
114
- stored_name=attachment.trustedStorageName,
115
- file_hash=(attachment.fileHash if attachment.fileHash else attachment.shaHash),
116
- )
117
- )
118
- return jira_dir, regscale_dir
119
-
120
-
121
- def compare_files_for_dupes_and_upload(
122
- synqly_issue: Ticket, regscale_issue: Issue, synqly_client: utils.Tenant, api: Api
123
- ) -> None:
124
- """
125
- Compare attachments for provided Jira and RegScale issues via hash to prevent duplicates
126
-
127
- :param Ticket synqly_issue: Synqly issue object to compare attachments from
128
- :param Issue regscale_issue: RegScale issue object to compare attachments from
129
- :param utils.Tenant synqly_client: Jira client to use for uploading attachments
130
- :param Api api: RegScale API object to use for interacting with RegScale
131
- :rtype: None
132
- """
133
- jira_uploaded_attachments = []
134
- regscale_uploaded_attachments = []
135
- # create a temporary directory to store the downloaded attachments from Jira and RegScale
136
- with tempfile.TemporaryDirectory() as temp_dir:
137
- # write attachments to the temporary directory
138
- jira_dir, regscale_dir = download_issue_attachments_to_directory(
139
- directory=temp_dir,
140
- synqly_issue=synqly_issue,
141
- regscale_issue=regscale_issue,
142
- api=api,
143
- synqly_client=synqly_client,
144
- )
145
- # get the hashes for the attachments in the regscale and jira directories
146
- # iterate all files in the jira directory and compute their hashes
147
- jira_attachment_hashes = compute_hashes_in_directory(jira_dir)
148
- regscale_attachment_hashes = compute_hashes_in_directory(regscale_dir)
149
-
150
- # check where the files need to be uploaded to before uploading
151
- for file_hash, file in regscale_attachment_hashes.items():
152
- if file_hash not in jira_attachment_hashes:
153
- try:
154
- upload_synqly_attachments(
155
- tenant=synqly_client,
156
- ticket_id=synqly_issue.id,
157
- file_path=Path(file),
158
- )
159
- jira_uploaded_attachments.append(file)
160
- except TypeError as ex:
161
- logger.error(
162
- "Unable to upload %s to Jira issue %s.\nError: %s",
163
- Path(file).name,
164
- synqly_issue.id,
165
- ex,
166
- )
167
- for file_hash, file in jira_attachment_hashes.items():
168
- if file_hash not in regscale_attachment_hashes:
169
- with open(file, "rb") as in_file:
170
- if File.upload_file_to_regscale(
171
- file_name=f"Jira_attachment_{Path(file).name}",
172
- parent_id=regscale_issue.id,
173
- parent_module="issues",
174
- api=api,
175
- file_data=in_file.read(),
176
- ):
177
- regscale_uploaded_attachments.append(file)
178
- logger.debug(
179
- "Uploaded %s to RegScale issue #%i.",
180
- Path(file).name,
181
- regscale_issue.id,
182
- )
183
- else:
184
- logger.warning(
185
- "Unable to upload %s to RegScale issue #%i.",
186
- Path(file).name,
187
- regscale_issue.id,
188
- )
189
- log_upload_results(regscale_uploaded_attachments, jira_uploaded_attachments, regscale_issue, synqly_issue)
190
-
191
-
192
- def log_upload_results(
193
- regscale_uploaded_attachments: list, jira_uploaded_attachments: list, regscale_issue: Issue, synqly_issue: Ticket
194
- ) -> None:
195
- """
196
- Log the results of the upload process
197
-
198
- :param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
199
- :param list jira_uploaded_attachments: List of Jira attachments that were uploaded
200
- :param Issue regscale_issue: RegScale issue that the attachments were uploaded to
201
- :param Ticket synqly_issue: Jira issue that the attachments were uploaded to
202
- :rtype: None
203
- :return: None
204
- """
205
- if regscale_uploaded_attachments and jira_uploaded_attachments:
206
- logger.info(
207
- "%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to Jira issue %s.",
208
- len(regscale_uploaded_attachments),
209
- regscale_issue.id,
210
- len(jira_uploaded_attachments),
211
- synqly_issue.id,
212
- )
213
- elif jira_uploaded_attachments:
214
- logger.info(
215
- "%i file(s) uploaded to Jira issue %s.",
216
- len(jira_uploaded_attachments),
217
- synqly_issue.id,
218
- )
219
- elif regscale_uploaded_attachments:
220
- logger.info(
221
- "%i file(s) uploaded to RegScale issue #%i.",
222
- len(regscale_uploaded_attachments),
223
- regscale_issue.id,
224
- )
225
-
226
-
227
- def download_synqly_attachments(tenant: utils.Tenant, ticket_id: str, download_dir: str) -> int:
228
- """
229
- Downloads attachments from a ticket via Synqly
230
-
231
- :param utils.Tenant tenant: Synqly Tenant object
232
- :param str ticket_id: Ticket ID to download attachments from
233
- :param str download_dir: Directory to download attachments to
234
- :return: # of Synqly attachments downloaded
235
- :rtype: int
236
- """
237
- attachments = tenant.synqly_engine_client.ticketing.list_attachments_metadata(ticket_id)
238
- logger.debug("Found %i attachments for ticket %s", len(attachments.result), ticket_id)
239
- for attachment in attachments.result:
240
- download_response = tenant.synqly_engine_client.ticketing.download_attachment(
241
- ticket_id=ticket_id, attachment_id=attachment.id
242
- )
243
- output_path = os.path.join(download_dir, attachment.file_name)
244
- with open(output_path, "wb") as f:
245
- f.write(base64.b64decode(download_response.result.content))
246
- logger.debug(
247
- "Downloaded attachment: %s and wrote its contents to %s",
248
- download_response.result.file_name,
249
- attachment.file_name,
250
- )
251
- return len(attachments.result)
252
-
253
-
254
- def upload_synqly_attachments(tenant: utils.Tenant, ticket_id: str, file_path: Path) -> None:
255
- """
256
- Uploads an attachment to a ticket via Synqly
257
-
258
- :param utils.Tenant tenant: Synqly Tenant object
259
- :param str ticket_id: Ticket ID to attach the file to
260
- :param Path file_path: Path to the file to attach
261
- :rtype: None
262
- """
263
- with open(file_path, "rb") as file:
264
- content = base64.b64encode(file.read())
265
- logger.debug("Creating attachment for ticket %s", ticket_id)
266
- tenant.synqly_engine_client.ticketing.create_attachment(
267
- ticket_id=ticket_id,
268
- request=engine.CreateAttachmentRequest(
269
- file_name=file_path.name,
270
- content=content,
271
- ),
272
- )
273
- logger.info("Added an attachment to ticket %s", ticket_id)
274
-
275
-
276
- def map_jira_issue_to_regscale_issue(synqly_issue: Ticket, parent_id: int, parent_module: str, config: dict) -> Issue:
277
- """
278
- Maps a JIRA issue to a RegScale issue
279
-
280
- :param Ticket synqly_issue: Synqly Ticket object
281
- :param int parent_id: Parent ID of the issue
282
- :param str parent_module: Parent module of the issue
283
- :param dict config: Configuration object
284
- :return: RegScale issue object
285
- :rtype: Issue
286
- """
287
- due_date = convert_datetime_to_regscale_string(synqly_issue.due_date)
288
- return Issue(
289
- title=synqly_issue.summary,
290
- severityLevel=Issue.assign_severity(synqly_issue.priority),
291
- issueOwnerId=config["userId"],
292
- dueDate=due_date,
293
- description=(f"Description {synqly_issue.description}\nStatus: {synqly_issue.status}\nDue Date: {due_date}"),
294
- status=("Closed" if synqly_issue.status.lower() == "done" else config["issues"]["jira"]["status"]),
295
- jiraId=synqly_issue.id,
296
- parentId=parent_id,
297
- parentModule=parent_module,
298
- dateCreated=get_current_datetime(),
299
- dateCompleted=(
300
- convert_datetime_to_regscale_string(synqly_issue.completion_date)
301
- if synqly_issue.status.lower() == "done"
302
- else None
303
- ),
304
- )
305
-
306
-
307
- def map_regscale_issue_to_jira_issue(
308
- regscale_issue: Issue, jira_project_key: str, jira_username: str, issue_type: str
309
- ) -> CreateTicketRequest:
310
- """
311
- Maps a RegScale issue to a JIRA issue
312
-
313
- :param Issue regscale_issue: RegScale issue object
314
- :param str jira_project_key: Jira project key
315
- :param str jira_username: Username to use for creating issues in Jira
316
- :param str issue_type: Type of issue to create in Jira
317
- :return: Synqly CreateTicketRequest object
318
- :rtype: CreateTicketRequest
319
- """
320
- return engine.CreateTicketRequest(
321
- id=regscale_issue.title,
322
- name=regscale_issue.title,
323
- summary=f"{regscale_issue.description}\n\n{regscale_issue_fields_to_markdown(regscale_issue)}",
324
- project=jira_project_key,
325
- creator=jira_username,
326
- issue_type=issue_type,
327
- priority=map_regscale_severity_to_jira_priority(regscale_issue.severityLevel.lower()),
328
- status="To Do",
329
- due_date=datetime.strptime(regscale_issue.dueDate, "%Y-%m-%dT%H:%M:%S"), # convert string to datetime
330
- )
331
-
332
-
333
- def regscale_issue_fields_to_markdown(regscale_issue: Issue) -> str:
334
- """
335
- Converts a RegScale issue's fields to a Markdown string
336
-
337
- :param Issue regscale_issue: RegScale issue object
338
- :return: Markdown string of RegScale issue fields
339
- :rtype: str
340
- """
341
- regscale_issue_dict = regscale_issue.dict()
342
- markdown_table = "| RegScale Field | Value |\n| --- | --- |\n"
343
- for key in regscale_issue_dict:
344
- markdown_table += f"| {key} | {regscale_issue_dict[key]} |\n"
345
- return markdown_table
346
-
347
-
348
- def map_regscale_severity_to_jira_priority(regscale_severity: str) -> Priority:
349
- """
350
- Map RegScale severity to OCSF priority
351
-
352
- :param str regscale_severity: RegScale severity
353
- :return: Jira priority
354
- :rtype: Priority
355
- """
356
- if "high" in regscale_severity.lower():
357
- return Priority.HIGH
358
- elif "moderate" in regscale_severity.lower():
359
- return Priority.MEDIUM
360
- else:
361
- return Priority.LOW
362
-
363
-
364
- def background_job(
365
- synqly_app: utils.App,
366
- jira_project_key: str,
367
- jira_username: str,
368
- jira_issue_type: str,
369
- regscale_id: int,
370
- regscale_module: str,
371
- sync_attachments: bool = True,
372
- ) -> None:
373
- """
374
- Simulates a background process performing work on behalf of tenants.
375
- Iterates through all tenants and, for any tenant with a Synqly Engine
376
- Client defined, creates a new ticket. After a short delay,
377
- background_job will update the ticket's status to "Done".
378
-
379
- :param utils.App synqly_app: Synqly Application object
380
- :param str jira_project_key: Jira project key
381
- :param str jira_username: Username to use for creating issues in Jira
382
- :param str jira_issue_type: Type of issue to create in Jira
383
- :param int regscale_id: RegScale record ID Number to sync issues to
384
- :param str regscale_module: RegScale module to sync issues to
385
- :param bool sync_attachments: Flag to determine if attachments should be synced, defaults to True
386
- :rtype: None
387
- """
388
- # Iterate through each tenant and send an event to their Event Logger
389
- for tenant in synqly_app.tenants.values():
390
- # Skip tenants that don't have a Synqly Engine Client initialized
391
- if tenant.synqly_engine_client is None:
392
- continue
393
-
394
- regscale_issues = Issue.get_all_by_parent(regscale_id, regscale_module)
395
- logger.info("Found {} issues in RegScale".format(len(regscale_issues)))
396
- jira_issues: list[Issue] = []
397
- fetch_res = tenant.synqly_engine_client.ticketing.query_tickets(
398
- filter=f"project[eq]{jira_project_key}",
399
- limit=100,
400
- )
401
- jira_issues.extend(fetch_res.result)
402
- # check and handle pagination
403
- while int(fetch_res.cursor) == len(jira_issues):
404
- fetch_res = tenant.synqly_engine_client.ticketing.query_tickets(
405
- filter=f"project[eq]{jira_project_key}",
406
- limit=100,
407
- cursor=fetch_res.cursor,
408
- )
409
- jira_issues.extend(fetch_res.result)
410
- logger.info("Found {} issues in JIRA".format(len(jira_issues)))
411
- regscale_cli = Application()
412
- api = Api()
413
- (
414
- regscale_issues,
415
- regscale_attachments,
416
- ) = Issue.fetch_issues_and_attachments_by_parent(
417
- app=regscale_cli,
418
- parent_id=regscale_id,
419
- parent_module=regscale_module,
420
- fetch_attachments=sync_attachments,
421
- )
422
-
423
- if regscale_issues:
424
- # sync RegScale issues to Jira
425
- if issues_to_update := sync_regscale_to_jira(
426
- regscale_issues=regscale_issues,
427
- jira_client=tenant,
428
- jira_project=jira_project_key,
429
- jira_issue_type=jira_issue_type,
430
- jira_username=jira_username,
431
- sync_attachments=sync_attachments,
432
- attachments=regscale_attachments,
433
- ):
434
- with job_progress:
435
- # create task to update RegScale issues
436
- updating_issues = job_progress.add_task(
437
- f"[#f8b737]Updating {len(issues_to_update)} RegScale issue(s) from Jira...",
438
- total=len(issues_to_update),
439
- )
440
- # create threads to analyze Jira issues and RegScale issues
441
- create_threads(
442
- process=update_regscale_issues,
443
- args=(
444
- issues_to_update,
445
- api,
446
- updating_issues,
447
- ),
448
- thread_count=len(issues_to_update),
449
- )
450
- # output the final result
451
- logger.info(
452
- "%i/%i issue(s) updated in RegScale.",
453
- len(issues_to_update),
454
- len(update_counter),
455
- )
456
- else:
457
- logger.info("No issues need to be updated in RegScale.")
458
-
459
- if jira_issues:
460
- # sync Jira issues to RegScale
461
- with job_progress:
462
- # create task to create RegScale issues
463
- creating_issues = job_progress.add_task(
464
- f"[#f8b737]Analyzing {len(jira_issues)} Jira issue(s)"
465
- f" and {len(regscale_issues)} RegScale issue(s)...",
466
- total=len(jira_issues),
467
- )
468
- # create threads to analyze Jira issues and RegScale issues
469
- create_threads(
470
- process=create_and_update_regscale_issues,
471
- args=(
472
- jira_issues,
473
- regscale_issues,
474
- sync_attachments,
475
- regscale_cli,
476
- regscale_id,
477
- regscale_module,
478
- tenant,
479
- creating_issues,
480
- ),
481
- thread_count=len(jira_issues),
482
- )
483
- # output the final result
484
- logger.info(
485
- "Analyzed %i Jira issue(s), created %i issue(s) and updated %i issue(s) in RegScale.",
486
- len(jira_issues),
487
- len(new_regscale_issues),
488
- len(updated_regscale_issues),
489
- )
490
- else:
491
- logger.info("No issues need to be analyzed from Jira.")
492
-
493
-
494
- def update_regscale_issues(args: Tuple, thread: int) -> None:
495
- """
496
- Function to compare Jira issues and RegScale issues
497
-
498
- :param Tuple args: Tuple of args to use during the process
499
- :param int thread: Thread number of current thread
500
- :rtype: None
501
- """
502
- # set up local variables from the passed args
503
- (
504
- regscale_issues,
505
- regscale_cli,
506
- task,
507
- ) = args
508
- # find which records should be executed by the current thread
509
- threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
510
- # iterate through the thread assignment items and process them
511
- for i in range(len(threads)):
512
- # set the issue for the thread for later use in the function
513
- issue = regscale_issues[threads[i]]
514
- # update the issue in RegScale
515
- Issue.update_issue(app=regscale_cli, issue=issue)
516
- logger.info(
517
- "RegScale Issue %i was updated with the Jira link.",
518
- issue.id,
519
- )
520
- update_counter.append(issue)
521
- # update progress bar
522
- job_progress.update(task, advance=1)
523
-
524
-
525
- def get_synqly_attachment_count(tenant: utils.Tenant, ticket_id: str) -> int:
526
- """
527
- Get the number of attachments for a ticket in Synqly
528
-
529
- :param utils.Tenant tenant: Synqly Tenant object
530
- :param str ticket_id: Ticket ID to get the attachments for
531
- :return: Number of attachments for the ticket
532
- :rtype: int
533
- """
534
- try:
535
- attachments = tenant.synqly_engine_client.ticketing.list_attachments_metadata(ticket_id)
536
- return len(attachments.result)
537
- except Exception as ex:
538
- logger.error(f"Unable to get attachments for ticket {ticket_id}.\nError: {ex}")
539
- return 0
540
-
541
-
542
- def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
543
- """
544
- Function to create or update issues in RegScale from Jira
545
-
546
- :param Tuple args: Tuple of args to use during the process
547
- :param int thread: Thread number of current thread
548
- :rtype: None
549
- """
550
- # set up local variables from the passed args
551
- (
552
- jira_issues,
553
- regscale_issues,
554
- add_attachments,
555
- regscale_cli,
556
- parent_id,
557
- parent_module,
558
- jira_client,
559
- task,
560
- ) = args
561
- # find which records should be executed by the current thread
562
- threads = thread_assignment(thread=thread, total_items=len(jira_issues))
563
-
564
- # iterate through the thread assignment items and process them
565
- for i in range(len(threads)):
566
- jira_issue: Ticket = jira_issues[threads[i]]
567
- regscale_issue: Optional[Issue] = next(
568
- (issue for issue in regscale_issues if issue.jiraId == jira_issue.id), None
569
- )
570
- # see if the Jira issue needs to be created in RegScale
571
- if jira_issue.status.lower() == "done" and regscale_issue:
572
- # update the status and date completed of the RegScale issue
573
- regscale_issue.status = "Closed"
574
- regscale_issue.dateCompleted = get_current_datetime()
575
- # update the issue in RegScale
576
- updated_regscale_issues.append(Issue.update_issue(app=regscale_cli, issue=regscale_issue))
577
- elif regscale_issue:
578
- # update the issue in RegScale
579
- updated_regscale_issues.append(Issue.update_issue(app=regscale_cli, issue=regscale_issue))
580
- else:
581
- # map the jira issue to a RegScale issue object
582
- issue = map_jira_issue_to_regscale_issue(
583
- synqly_issue=jira_issue,
584
- config=regscale_cli.config,
585
- parent_id=parent_id,
586
- parent_module=parent_module,
587
- )
588
- # create the issue in RegScale
589
- if regscale_issue := Issue.insert_issue(
590
- app=regscale_cli,
591
- issue=issue,
592
- ):
593
- if regscale_issue.id:
594
- logger.debug(
595
- "Created issue #%i-%s in RegScale.",
596
- regscale_issue.id,
597
- regscale_issue.title,
598
- )
599
- else:
600
- logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
601
- new_regscale_issues.append(regscale_issue)
602
- handle_attachments(
603
- add_attachments=add_attachments,
604
- regscale_issue=regscale_issue,
605
- jira_client=jira_client,
606
- jira_issue=jira_issue,
607
- )
608
- # update progress bar
609
- job_progress.update(task, advance=1)
610
-
611
-
612
- def handle_attachments(
613
- add_attachments: bool, regscale_issue: Issue, jira_client: utils.Tenant, jira_issue: Ticket
614
- ) -> None:
615
- """
616
- Handle attachments for Jira and RegScale issues
617
-
618
- :param bool add_attachments: Flag to determine if attachments should be added to the issue
619
- :param Issue regscale_issue: RegScale issue object
620
- :param utils.Tenant jira_client: Jira client to use for issue creation in Jira
621
- :param Ticket jira_issue: Jira issue object
622
- :rtype: None
623
- """
624
- if add_attachments and regscale_issue:
625
- # check if the jira issue has attachments
626
- attachment_count = get_synqly_attachment_count(jira_client, jira_issue.id)
627
- if attachment_count > 0:
628
- # determine which attachments need to be uploaded to prevent duplicates by
629
- # getting the hashes of all Jira & RegScale attachments
630
- compare_files_for_dupes_and_upload(
631
- synqly_issue=jira_issue,
632
- regscale_issue=regscale_issue,
633
- synqly_client=jira_client,
634
- api=Api(),
635
- )
636
-
637
-
638
- def sync_regscale_to_jira(
639
- regscale_issues: list[Issue],
640
- jira_client: utils.Tenant,
641
- jira_project: str,
642
- jira_issue_type: str,
643
- jira_username: str,
644
- sync_attachments: bool = True,
645
- attachments: Optional[dict] = None,
646
- ) -> list[Issue]:
647
- """
648
- Sync issues from RegScale to Jira
649
-
650
- :param list[Issue] regscale_issues: list of RegScale issues to sync to Jira
651
- :param utils.Tenant jira_client: Jira client to use for issue creation in Jira
652
- :param str jira_project: Jira Project to create the issues in
653
- :param str jira_issue_type: Type of issue to create in Jira
654
- :param str jira_username: Username to use for creating issues in Jira
655
- :param bool sync_attachments: Flag to determine if attachments should be synced, defaults to True
656
- :param Optional[dict] attachments: Dictionary of attachments to sync, defaults to None
657
- :return: list of RegScale issues that need to be updated
658
- :rtype: list[Issue]
659
- """
660
- new_issue_counter = 0
661
- issuess_to_update = []
662
- for issue in regscale_issues:
663
- # see if Jira issue already exists
664
- if not issue.jiraId or issue.jiraId == "":
665
- new_issue = create_issue_in_jira(
666
- issue=issue,
667
- jira_client=jira_client,
668
- jira_project=jira_project,
669
- issue_type=jira_issue_type,
670
- jira_username=jira_username,
671
- add_attachments=sync_attachments,
672
- attachments=attachments,
673
- )
674
- if not new_issue:
675
- continue
676
- # log progress
677
- new_issue_counter += 1
678
- # get the Jira ID
679
- jira_id = new_issue.id
680
- # update the RegScale issue for the Jira link
681
- issue.jiraId = jira_id
682
- # add the issue to the update_issues global list
683
- issuess_to_update.append(issue)
684
- # output the final result
685
- logger.info("%i new issue(s) opened in Jira.", new_issue_counter)
686
- return issuess_to_update
687
-
688
-
689
- def create_issue_in_jira(
690
- issue: Issue,
691
- jira_client: utils.Tenant,
692
- jira_project: str,
693
- issue_type: str,
694
- jira_username: str,
695
- add_attachments: Optional[bool] = False,
696
- attachments: list[File] = None,
697
- api: Optional[Api] = None,
698
- ) -> Optional[Ticket]:
699
- """
700
- Create a new issue in Jira
701
-
702
- :param Issue issue: RegScale issue object
703
- :param utils.Tenant jira_client: Jira client to use for issue creation in Jira
704
- :param str jira_project: Project name in Jira to create the issue in
705
- :param str issue_type: The type of issue to create in Jira
706
- :param str jira_username: Username to use for creating issues in Jira
707
- :param Optional[bool] add_attachments: Flag to determine if attachments should be added to the issue
708
- :param list[File] attachments: List of attachments to add to the issue
709
- :param Optional[Api] api: RegScale API object to use for interacting with RegScale
710
- :return: Newly created issue in Jira
711
- :rtype: Optional[Ticket]
712
- """
713
- try:
714
- new_issue = map_regscale_issue_to_jira_issue(issue, jira_project, jira_username, issue_type)
715
- create_response = jira_client.synqly_engine_client.ticketing.create_ticket(request=new_issue)
716
- except Exception as ex:
717
- logger.error(f"Unable to create Jira issue.\nError: {ex}")
718
- return None
719
- # error_and_exit(f"Unable to create Jira issue.\nError: {ex}")
720
- if add_attachments and attachments:
721
- if not api:
722
- api = Api()
723
- compare_files_for_dupes_and_upload(
724
- synqly_issue=create_response.result.Ticket,
725
- regscale_issue=issue,
726
- synqly_client=jira_client,
727
- api=Api(),
728
- )
729
- logger.info("Created ticket: {}".format(create_response.result.name))
730
- return create_response.result
731
-
732
-
733
- @synqly.command(name="sync_jira")
734
- @regscale_id()
735
- @regscale_module()
736
- @click.option(
737
- "--jira_project",
738
- type=click.STRING,
739
- help="RegScale will sync the issues for the record to the Jira project.",
740
- prompt="Enter the name of the project in Jira",
741
- required=True,
742
- )
743
- @click.option(
744
- "--jira_issue_type",
745
- type=click.STRING,
746
- help="Enter the Jira issue type to use when creating new issues from RegScale. (CASE SENSITIVE)",
747
- prompt="Enter the Jira issue type",
748
- required=True,
749
- )
750
- @click.option(
751
- "--sync_attachments",
752
- type=click.BOOL,
753
- help=(
754
- "Whether RegScale will sync the attachments for the issue "
755
- "in the provided Jira project and vice versa. Defaults to True."
756
- ),
757
- required=False,
758
- default=True,
759
- )
760
- def sync_jira(
761
- regscale_id: int, regscale_module: str, jira_project: str, jira_issue_type: str, sync_attachments: bool = True
762
- ):
763
- """
764
- Parses command line arguments for this example.
765
- """
766
- sync_with_jira(
767
- regscale_id,
768
- regscale_module,
769
- jira_project,
770
- jira_issue_type,
771
- sync_attachments,
772
- )
773
-
774
-
775
- def sync_with_jira(
776
- regscale_id: int, regscale_module: str, jira_project_key: str, jira_issue_type: str, sync_attachments: bool = True
777
- ) -> None:
778
- """
779
- Syncs RegScale issues with Jira issues using Synqly
780
-
781
- :param int regscale_id: RegScale record ID Number to sync issues to
782
- :param str regscale_module: RegScale module to sync issues to
783
- :param str jira_project_key: Jira project key
784
- :param str jira_issue_type: Type of issue to create in Jira
785
- :param bool sync_attachments: Flag to determine if attachments should be synced, defaults to True
786
- :raises Exception: If an error occurs during the sync process
787
- :rtype: None
788
- """
789
- # Initialize an empty application to store our simulated tenants
790
- synqly = utils.App("ticketing")
791
- app = Application()
792
- synqly_access_token = os.getenv("SYNQLY_ACCESS_TOKEN") or app.config["synqlyAccessToken"]
793
- jira_url = os.getenv("JIRA_URL") or app.config["jiraUrl"]
794
- jira_username = os.getenv("JIRA_USERNAME") or app.config["jiraUserName"]
795
- jira_token = os.getenv("JIRA_API_TOKEN") or app.config["jiraApiToken"]
796
- if (
797
- not jira_url
798
- or not jira_username
799
- or not jira_token
800
- or not synqly_access_token
801
- or synqly_access_token == app.template["synqlyAccessToken"]
802
- or jira_url == app.template["jiraUrl"]
803
- or jira_username == app.template["jiraUserName"]
804
- or jira_token == app.template["jiraApiToken"]
805
- ):
806
- error_and_exit(
807
- "jiraUrl, jiraUserName, and jiraApiToken are required. Please provide them in the environment or init.yaml."
808
- )
809
- # Initialize tenants within our Application
810
- try:
811
- synqly.new_tenant(synqly_access_token, "RegScale-CLI")
812
- app.logger.debug("RegScale-CLI created")
813
- except Exception as e:
814
- app.logger.error("Error creating Tenant RegScale-CLI:" + str(e))
815
- synqly._cleanup_handler()
816
- raise e
817
-
818
- # Configure a ticketing integration based on the configuration. If no jira credentials
819
- # are provided, then mock ticket provider is used
820
- provider_config: mgmt.ProviderConfig = jira_provider_config(jira_url, jira_username, jira_token)
821
-
822
- # Configure a mock integration for tenant ABC and a JIRA Integration for Tenant XYZ
823
- try:
824
- synqly.configure_integration("RegScale-CLI", provider_config)
825
- except Exception as e:
826
- print("Error configuring provider integration for Tenant RegScale-CLI: " + str(e))
827
- synqly._cleanup_handler()
828
- raise e
829
-
830
- # Start a background job to generate data
831
- try:
832
- background_job(
833
- synqly, jira_project_key, jira_username, jira_issue_type, regscale_id, regscale_module, sync_attachments
834
- )
835
- except Exception as e:
836
- print("Error running background job: " + str(e))
837
- synqly._cleanup_handler()
838
- raise e
839
-
840
- synqly._cleanup_handler()