regscale-cli 6.16.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (481) hide show
  1. regscale/__init__.py +1 -0
  2. regscale/airflow/__init__.py +9 -0
  3. regscale/airflow/azure/__init__.py +9 -0
  4. regscale/airflow/azure/cli.py +89 -0
  5. regscale/airflow/azure/upload_dags.py +116 -0
  6. regscale/airflow/click_dags.py +127 -0
  7. regscale/airflow/click_mixins.py +82 -0
  8. regscale/airflow/config.py +25 -0
  9. regscale/airflow/factories/__init__.py +0 -0
  10. regscale/airflow/factories/connections.py +58 -0
  11. regscale/airflow/factories/workflows.py +78 -0
  12. regscale/airflow/hierarchy.py +88 -0
  13. regscale/airflow/operators/__init__.py +0 -0
  14. regscale/airflow/operators/click.py +36 -0
  15. regscale/airflow/sensors/__init__.py +0 -0
  16. regscale/airflow/sensors/sql.py +107 -0
  17. regscale/airflow/sessions/__init__.py +0 -0
  18. regscale/airflow/sessions/sql/__init__.py +3 -0
  19. regscale/airflow/sessions/sql/queries.py +64 -0
  20. regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
  21. regscale/airflow/tasks/__init__.py +0 -0
  22. regscale/airflow/tasks/branches.py +22 -0
  23. regscale/airflow/tasks/cli.py +116 -0
  24. regscale/airflow/tasks/click.py +73 -0
  25. regscale/airflow/tasks/debugging.py +9 -0
  26. regscale/airflow/tasks/groups.py +116 -0
  27. regscale/airflow/tasks/init.py +60 -0
  28. regscale/airflow/tasks/states.py +47 -0
  29. regscale/airflow/tasks/workflows.py +36 -0
  30. regscale/ansible/__init__.py +9 -0
  31. regscale/core/__init__.py +0 -0
  32. regscale/core/app/__init__.py +3 -0
  33. regscale/core/app/api.py +571 -0
  34. regscale/core/app/application.py +665 -0
  35. regscale/core/app/internal/__init__.py +136 -0
  36. regscale/core/app/internal/admin_actions.py +230 -0
  37. regscale/core/app/internal/assessments_editor.py +873 -0
  38. regscale/core/app/internal/catalog.py +316 -0
  39. regscale/core/app/internal/comparison.py +459 -0
  40. regscale/core/app/internal/control_editor.py +571 -0
  41. regscale/core/app/internal/encrypt.py +79 -0
  42. regscale/core/app/internal/evidence.py +1240 -0
  43. regscale/core/app/internal/file_uploads.py +151 -0
  44. regscale/core/app/internal/healthcheck.py +66 -0
  45. regscale/core/app/internal/login.py +305 -0
  46. regscale/core/app/internal/migrations.py +240 -0
  47. regscale/core/app/internal/model_editor.py +1701 -0
  48. regscale/core/app/internal/poam_editor.py +632 -0
  49. regscale/core/app/internal/workflow.py +105 -0
  50. regscale/core/app/logz.py +74 -0
  51. regscale/core/app/utils/XMLIR.py +258 -0
  52. regscale/core/app/utils/__init__.py +0 -0
  53. regscale/core/app/utils/api_handler.py +358 -0
  54. regscale/core/app/utils/app_utils.py +1110 -0
  55. regscale/core/app/utils/catalog_utils/__init__.py +0 -0
  56. regscale/core/app/utils/catalog_utils/common.py +91 -0
  57. regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
  58. regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
  59. regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
  60. regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
  61. regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
  62. regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
  63. regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
  64. regscale/core/app/utils/file_utils.py +238 -0
  65. regscale/core/app/utils/parser_utils.py +81 -0
  66. regscale/core/app/utils/pickle_file_handler.py +57 -0
  67. regscale/core/app/utils/regscale_utils.py +319 -0
  68. regscale/core/app/utils/report_utils.py +119 -0
  69. regscale/core/app/utils/variables.py +226 -0
  70. regscale/core/decorators.py +31 -0
  71. regscale/core/lazy_group.py +65 -0
  72. regscale/core/login.py +63 -0
  73. regscale/core/server/__init__.py +0 -0
  74. regscale/core/server/flask_api.py +473 -0
  75. regscale/core/server/helpers.py +373 -0
  76. regscale/core/server/rest.py +64 -0
  77. regscale/core/server/static/css/bootstrap.css +6030 -0
  78. regscale/core/server/static/css/bootstrap.min.css +6 -0
  79. regscale/core/server/static/css/main.css +176 -0
  80. regscale/core/server/static/images/regscale-cli.svg +49 -0
  81. regscale/core/server/static/images/regscale.svg +38 -0
  82. regscale/core/server/templates/base.html +74 -0
  83. regscale/core/server/templates/index.html +43 -0
  84. regscale/core/server/templates/login.html +28 -0
  85. regscale/core/server/templates/make_base64.html +22 -0
  86. regscale/core/server/templates/upload_STIG.html +109 -0
  87. regscale/core/server/templates/upload_STIG_result.html +26 -0
  88. regscale/core/server/templates/upload_ssp.html +144 -0
  89. regscale/core/server/templates/upload_ssp_result.html +128 -0
  90. regscale/core/static/__init__.py +0 -0
  91. regscale/core/static/regex.py +14 -0
  92. regscale/core/utils/__init__.py +117 -0
  93. regscale/core/utils/click_utils.py +13 -0
  94. regscale/core/utils/date.py +238 -0
  95. regscale/core/utils/graphql.py +254 -0
  96. regscale/core/utils/urls.py +23 -0
  97. regscale/dev/__init__.py +6 -0
  98. regscale/dev/analysis.py +454 -0
  99. regscale/dev/cli.py +235 -0
  100. regscale/dev/code_gen.py +492 -0
  101. regscale/dev/dirs.py +69 -0
  102. regscale/dev/docs.py +384 -0
  103. regscale/dev/monitoring.py +26 -0
  104. regscale/dev/profiling.py +216 -0
  105. regscale/exceptions/__init__.py +4 -0
  106. regscale/exceptions/license_exception.py +7 -0
  107. regscale/exceptions/validation_exception.py +9 -0
  108. regscale/integrations/__init__.py +1 -0
  109. regscale/integrations/commercial/__init__.py +486 -0
  110. regscale/integrations/commercial/ad.py +433 -0
  111. regscale/integrations/commercial/amazon/__init__.py +0 -0
  112. regscale/integrations/commercial/amazon/common.py +106 -0
  113. regscale/integrations/commercial/aqua/__init__.py +0 -0
  114. regscale/integrations/commercial/aqua/aqua.py +91 -0
  115. regscale/integrations/commercial/aws/__init__.py +6 -0
  116. regscale/integrations/commercial/aws/cli.py +322 -0
  117. regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
  118. regscale/integrations/commercial/aws/inventory/base.py +64 -0
  119. regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
  120. regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
  121. regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
  122. regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
  123. regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
  124. regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
  125. regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
  126. regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
  127. regscale/integrations/commercial/aws/scanner.py +823 -0
  128. regscale/integrations/commercial/azure/__init__.py +0 -0
  129. regscale/integrations/commercial/azure/common.py +32 -0
  130. regscale/integrations/commercial/azure/intune.py +488 -0
  131. regscale/integrations/commercial/azure/scanner.py +49 -0
  132. regscale/integrations/commercial/burp.py +78 -0
  133. regscale/integrations/commercial/cpe.py +144 -0
  134. regscale/integrations/commercial/crowdstrike.py +1117 -0
  135. regscale/integrations/commercial/defender.py +1511 -0
  136. regscale/integrations/commercial/dependabot.py +210 -0
  137. regscale/integrations/commercial/durosuite/__init__.py +0 -0
  138. regscale/integrations/commercial/durosuite/api.py +1546 -0
  139. regscale/integrations/commercial/durosuite/process_devices.py +101 -0
  140. regscale/integrations/commercial/durosuite/scanner.py +637 -0
  141. regscale/integrations/commercial/durosuite/variables.py +21 -0
  142. regscale/integrations/commercial/ecr.py +90 -0
  143. regscale/integrations/commercial/gcp/__init__.py +237 -0
  144. regscale/integrations/commercial/gcp/auth.py +96 -0
  145. regscale/integrations/commercial/gcp/control_tests.py +238 -0
  146. regscale/integrations/commercial/gcp/variables.py +18 -0
  147. regscale/integrations/commercial/gitlab.py +332 -0
  148. regscale/integrations/commercial/grype.py +165 -0
  149. regscale/integrations/commercial/ibm.py +90 -0
  150. regscale/integrations/commercial/import_all/__init__.py +0 -0
  151. regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
  152. regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
  153. regscale/integrations/commercial/jira.py +1046 -0
  154. regscale/integrations/commercial/mappings/__init__.py +0 -0
  155. regscale/integrations/commercial/mappings/csf_controls.json +713 -0
  156. regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
  157. regscale/integrations/commercial/nessus/__init__.py +0 -0
  158. regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
  159. regscale/integrations/commercial/nessus/scanner.py +416 -0
  160. regscale/integrations/commercial/nexpose.py +90 -0
  161. regscale/integrations/commercial/okta.py +798 -0
  162. regscale/integrations/commercial/opentext/__init__.py +0 -0
  163. regscale/integrations/commercial/opentext/click.py +99 -0
  164. regscale/integrations/commercial/opentext/scanner.py +143 -0
  165. regscale/integrations/commercial/prisma.py +91 -0
  166. regscale/integrations/commercial/qualys.py +1462 -0
  167. regscale/integrations/commercial/salesforce.py +980 -0
  168. regscale/integrations/commercial/sap/__init__.py +0 -0
  169. regscale/integrations/commercial/sap/click.py +31 -0
  170. regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
  171. regscale/integrations/commercial/sap/sysdig/click.py +57 -0
  172. regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
  173. regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
  174. regscale/integrations/commercial/sap/tenable/click.py +49 -0
  175. regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
  176. regscale/integrations/commercial/servicenow.py +1756 -0
  177. regscale/integrations/commercial/sicura/__init__.py +0 -0
  178. regscale/integrations/commercial/sicura/api.py +855 -0
  179. regscale/integrations/commercial/sicura/commands.py +73 -0
  180. regscale/integrations/commercial/sicura/scanner.py +481 -0
  181. regscale/integrations/commercial/sicura/variables.py +16 -0
  182. regscale/integrations/commercial/snyk.py +90 -0
  183. regscale/integrations/commercial/sonarcloud.py +260 -0
  184. regscale/integrations/commercial/sqlserver.py +369 -0
  185. regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
  186. regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
  187. regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
  188. regscale/integrations/commercial/stigv2/__init__.py +0 -0
  189. regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
  190. regscale/integrations/commercial/stigv2/click_commands.py +95 -0
  191. regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
  192. regscale/integrations/commercial/synqly/__init__.py +0 -0
  193. regscale/integrations/commercial/synqly/assets.py +46 -0
  194. regscale/integrations/commercial/synqly/ticketing.py +132 -0
  195. regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
  196. regscale/integrations/commercial/synqly_jira.py +840 -0
  197. regscale/integrations/commercial/tenablev2/__init__.py +0 -0
  198. regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
  199. regscale/integrations/commercial/tenablev2/click.py +1584 -0
  200. regscale/integrations/commercial/tenablev2/scanner.py +504 -0
  201. regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
  202. regscale/integrations/commercial/tenablev2/utils.py +78 -0
  203. regscale/integrations/commercial/tenablev2/variables.py +17 -0
  204. regscale/integrations/commercial/trivy.py +162 -0
  205. regscale/integrations/commercial/veracode.py +96 -0
  206. regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
  207. regscale/integrations/commercial/wizv2/__init__.py +0 -0
  208. regscale/integrations/commercial/wizv2/click.py +429 -0
  209. regscale/integrations/commercial/wizv2/constants.py +1001 -0
  210. regscale/integrations/commercial/wizv2/issue.py +361 -0
  211. regscale/integrations/commercial/wizv2/models.py +112 -0
  212. regscale/integrations/commercial/wizv2/parsers.py +339 -0
  213. regscale/integrations/commercial/wizv2/sbom.py +115 -0
  214. regscale/integrations/commercial/wizv2/scanner.py +416 -0
  215. regscale/integrations/commercial/wizv2/utils.py +796 -0
  216. regscale/integrations/commercial/wizv2/variables.py +39 -0
  217. regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
  218. regscale/integrations/commercial/xray.py +91 -0
  219. regscale/integrations/integration/__init__.py +2 -0
  220. regscale/integrations/integration/integration.py +26 -0
  221. regscale/integrations/integration/inventory.py +17 -0
  222. regscale/integrations/integration/issue.py +100 -0
  223. regscale/integrations/integration_override.py +149 -0
  224. regscale/integrations/public/__init__.py +103 -0
  225. regscale/integrations/public/cisa.py +641 -0
  226. regscale/integrations/public/criticality_updater.py +70 -0
  227. regscale/integrations/public/emass.py +411 -0
  228. regscale/integrations/public/emass_slcm_import.py +697 -0
  229. regscale/integrations/public/fedramp/__init__.py +0 -0
  230. regscale/integrations/public/fedramp/appendix_parser.py +548 -0
  231. regscale/integrations/public/fedramp/click.py +479 -0
  232. regscale/integrations/public/fedramp/components.py +714 -0
  233. regscale/integrations/public/fedramp/docx_parser.py +259 -0
  234. regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
  235. regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
  236. regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
  237. regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
  238. regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
  239. regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
  240. regscale/integrations/public/fedramp/import_workbook.py +495 -0
  241. regscale/integrations/public/fedramp/inventory_items.py +244 -0
  242. regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
  243. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
  244. regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
  245. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
  246. regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
  247. regscale/integrations/public/fedramp/mappings/user.py +175 -0
  248. regscale/integrations/public/fedramp/mappings/values.py +141 -0
  249. regscale/integrations/public/fedramp/markdown_parser.py +150 -0
  250. regscale/integrations/public/fedramp/metadata.py +689 -0
  251. regscale/integrations/public/fedramp/models/__init__.py +59 -0
  252. regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
  253. regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
  254. regscale/integrations/public/fedramp/parts_mapper.py +107 -0
  255. regscale/integrations/public/fedramp/poam/__init__.py +0 -0
  256. regscale/integrations/public/fedramp/poam/scanner.py +851 -0
  257. regscale/integrations/public/fedramp/properties.py +201 -0
  258. regscale/integrations/public/fedramp/reporting.py +84 -0
  259. regscale/integrations/public/fedramp/resources.py +496 -0
  260. regscale/integrations/public/fedramp/rosetta.py +110 -0
  261. regscale/integrations/public/fedramp/ssp_logger.py +87 -0
  262. regscale/integrations/public/fedramp/system_characteristics.py +922 -0
  263. regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
  264. regscale/integrations/public/fedramp/system_implementation.py +190 -0
  265. regscale/integrations/public/fedramp/xml_utils.py +87 -0
  266. regscale/integrations/public/nist_catalog.py +275 -0
  267. regscale/integrations/public/oscal.py +1946 -0
  268. regscale/integrations/public/otx.py +169 -0
  269. regscale/integrations/scanner_integration.py +2692 -0
  270. regscale/integrations/variables.py +25 -0
  271. regscale/models/__init__.py +7 -0
  272. regscale/models/app_models/__init__.py +5 -0
  273. regscale/models/app_models/catalog_compare.py +213 -0
  274. regscale/models/app_models/click.py +252 -0
  275. regscale/models/app_models/datetime_encoder.py +21 -0
  276. regscale/models/app_models/import_validater.py +321 -0
  277. regscale/models/app_models/mapping.py +260 -0
  278. regscale/models/app_models/pipeline.py +37 -0
  279. regscale/models/click_models.py +413 -0
  280. regscale/models/config.py +154 -0
  281. regscale/models/email_style.css +67 -0
  282. regscale/models/hierarchy.py +8 -0
  283. regscale/models/inspect_models.py +79 -0
  284. regscale/models/integration_models/__init__.py +0 -0
  285. regscale/models/integration_models/amazon_models/__init__.py +0 -0
  286. regscale/models/integration_models/amazon_models/inspector.py +262 -0
  287. regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
  288. regscale/models/integration_models/aqua.py +247 -0
  289. regscale/models/integration_models/azure_alerts.py +255 -0
  290. regscale/models/integration_models/base64.py +23 -0
  291. regscale/models/integration_models/burp.py +433 -0
  292. regscale/models/integration_models/burp_models.py +128 -0
  293. regscale/models/integration_models/cisa_kev_data.json +19333 -0
  294. regscale/models/integration_models/defender_data.py +93 -0
  295. regscale/models/integration_models/defenderimport.py +143 -0
  296. regscale/models/integration_models/drf.py +443 -0
  297. regscale/models/integration_models/ecr_models/__init__.py +0 -0
  298. regscale/models/integration_models/ecr_models/data.py +69 -0
  299. regscale/models/integration_models/ecr_models/ecr.py +239 -0
  300. regscale/models/integration_models/flat_file_importer.py +1079 -0
  301. regscale/models/integration_models/grype_import.py +247 -0
  302. regscale/models/integration_models/ibm.py +126 -0
  303. regscale/models/integration_models/implementation_results.py +85 -0
  304. regscale/models/integration_models/nexpose.py +140 -0
  305. regscale/models/integration_models/prisma.py +202 -0
  306. regscale/models/integration_models/qualys.py +720 -0
  307. regscale/models/integration_models/qualys_scanner.py +160 -0
  308. regscale/models/integration_models/sbom/__init__.py +0 -0
  309. regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
  310. regscale/models/integration_models/send_reminders.py +620 -0
  311. regscale/models/integration_models/snyk.py +155 -0
  312. regscale/models/integration_models/synqly_models/__init__.py +0 -0
  313. regscale/models/integration_models/synqly_models/capabilities.json +1 -0
  314. regscale/models/integration_models/synqly_models/connector_types.py +22 -0
  315. regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
  316. regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
  317. regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
  318. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
  319. regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
  320. regscale/models/integration_models/synqly_models/param.py +72 -0
  321. regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
  322. regscale/models/integration_models/synqly_models/tenants.py +39 -0
  323. regscale/models/integration_models/tenable_models/__init__.py +0 -0
  324. regscale/models/integration_models/tenable_models/integration.py +187 -0
  325. regscale/models/integration_models/tenable_models/models.py +513 -0
  326. regscale/models/integration_models/trivy_import.py +231 -0
  327. regscale/models/integration_models/veracode.py +217 -0
  328. regscale/models/integration_models/xray.py +135 -0
  329. regscale/models/locking.py +100 -0
  330. regscale/models/platform.py +110 -0
  331. regscale/models/regscale_models/__init__.py +67 -0
  332. regscale/models/regscale_models/assessment.py +570 -0
  333. regscale/models/regscale_models/assessment_plan.py +52 -0
  334. regscale/models/regscale_models/asset.py +567 -0
  335. regscale/models/regscale_models/asset_mapping.py +190 -0
  336. regscale/models/regscale_models/case.py +42 -0
  337. regscale/models/regscale_models/catalog.py +261 -0
  338. regscale/models/regscale_models/cci.py +46 -0
  339. regscale/models/regscale_models/change.py +167 -0
  340. regscale/models/regscale_models/checklist.py +372 -0
  341. regscale/models/regscale_models/comment.py +49 -0
  342. regscale/models/regscale_models/compliance_settings.py +112 -0
  343. regscale/models/regscale_models/component.py +412 -0
  344. regscale/models/regscale_models/component_mapping.py +65 -0
  345. regscale/models/regscale_models/control.py +38 -0
  346. regscale/models/regscale_models/control_implementation.py +1128 -0
  347. regscale/models/regscale_models/control_objective.py +261 -0
  348. regscale/models/regscale_models/control_parameter.py +100 -0
  349. regscale/models/regscale_models/control_test.py +34 -0
  350. regscale/models/regscale_models/control_test_plan.py +75 -0
  351. regscale/models/regscale_models/control_test_result.py +52 -0
  352. regscale/models/regscale_models/custom_field.py +245 -0
  353. regscale/models/regscale_models/data.py +109 -0
  354. regscale/models/regscale_models/data_center.py +40 -0
  355. regscale/models/regscale_models/deviation.py +203 -0
  356. regscale/models/regscale_models/email.py +97 -0
  357. regscale/models/regscale_models/evidence.py +47 -0
  358. regscale/models/regscale_models/evidence_mapping.py +40 -0
  359. regscale/models/regscale_models/facility.py +59 -0
  360. regscale/models/regscale_models/file.py +382 -0
  361. regscale/models/regscale_models/filetag.py +37 -0
  362. regscale/models/regscale_models/form_field_value.py +94 -0
  363. regscale/models/regscale_models/group.py +169 -0
  364. regscale/models/regscale_models/implementation_objective.py +335 -0
  365. regscale/models/regscale_models/implementation_option.py +275 -0
  366. regscale/models/regscale_models/implementation_role.py +33 -0
  367. regscale/models/regscale_models/incident.py +177 -0
  368. regscale/models/regscale_models/interconnection.py +43 -0
  369. regscale/models/regscale_models/issue.py +1176 -0
  370. regscale/models/regscale_models/leveraged_authorization.py +125 -0
  371. regscale/models/regscale_models/line_of_inquiry.py +52 -0
  372. regscale/models/regscale_models/link.py +205 -0
  373. regscale/models/regscale_models/meta_data.py +64 -0
  374. regscale/models/regscale_models/mixins/__init__.py +0 -0
  375. regscale/models/regscale_models/mixins/parent_cache.py +124 -0
  376. regscale/models/regscale_models/module.py +224 -0
  377. regscale/models/regscale_models/modules.py +191 -0
  378. regscale/models/regscale_models/objective.py +14 -0
  379. regscale/models/regscale_models/parameter.py +87 -0
  380. regscale/models/regscale_models/ports_protocol.py +81 -0
  381. regscale/models/regscale_models/privacy.py +89 -0
  382. regscale/models/regscale_models/profile.py +50 -0
  383. regscale/models/regscale_models/profile_link.py +68 -0
  384. regscale/models/regscale_models/profile_mapping.py +124 -0
  385. regscale/models/regscale_models/project.py +63 -0
  386. regscale/models/regscale_models/property.py +278 -0
  387. regscale/models/regscale_models/question.py +85 -0
  388. regscale/models/regscale_models/questionnaire.py +87 -0
  389. regscale/models/regscale_models/questionnaire_instance.py +177 -0
  390. regscale/models/regscale_models/rbac.py +132 -0
  391. regscale/models/regscale_models/reference.py +86 -0
  392. regscale/models/regscale_models/regscale_model.py +1643 -0
  393. regscale/models/regscale_models/requirement.py +29 -0
  394. regscale/models/regscale_models/risk.py +274 -0
  395. regscale/models/regscale_models/sbom.py +54 -0
  396. regscale/models/regscale_models/scan_history.py +436 -0
  397. regscale/models/regscale_models/search.py +53 -0
  398. regscale/models/regscale_models/security_control.py +132 -0
  399. regscale/models/regscale_models/security_plan.py +204 -0
  400. regscale/models/regscale_models/software_inventory.py +159 -0
  401. regscale/models/regscale_models/stake_holder.py +64 -0
  402. regscale/models/regscale_models/stig.py +647 -0
  403. regscale/models/regscale_models/supply_chain.py +152 -0
  404. regscale/models/regscale_models/system_role.py +188 -0
  405. regscale/models/regscale_models/system_role_external_assignment.py +40 -0
  406. regscale/models/regscale_models/tag.py +37 -0
  407. regscale/models/regscale_models/tag_mapping.py +19 -0
  408. regscale/models/regscale_models/task.py +133 -0
  409. regscale/models/regscale_models/threat.py +196 -0
  410. regscale/models/regscale_models/user.py +175 -0
  411. regscale/models/regscale_models/user_group.py +55 -0
  412. regscale/models/regscale_models/vulnerability.py +242 -0
  413. regscale/models/regscale_models/vulnerability_mapping.py +162 -0
  414. regscale/models/regscale_models/workflow.py +55 -0
  415. regscale/models/regscale_models/workflow_action.py +34 -0
  416. regscale/models/regscale_models/workflow_instance.py +269 -0
  417. regscale/models/regscale_models/workflow_instance_step.py +114 -0
  418. regscale/models/regscale_models/workflow_template.py +58 -0
  419. regscale/models/regscale_models/workflow_template_step.py +45 -0
  420. regscale/regscale.py +815 -0
  421. regscale/utils/__init__.py +7 -0
  422. regscale/utils/b64conversion.py +14 -0
  423. regscale/utils/click_utils.py +118 -0
  424. regscale/utils/decorators.py +48 -0
  425. regscale/utils/dict_utils.py +59 -0
  426. regscale/utils/files.py +79 -0
  427. regscale/utils/fxns.py +30 -0
  428. regscale/utils/graphql_client.py +113 -0
  429. regscale/utils/lists.py +16 -0
  430. regscale/utils/numbers.py +12 -0
  431. regscale/utils/shell.py +148 -0
  432. regscale/utils/string.py +121 -0
  433. regscale/utils/synqly_utils.py +165 -0
  434. regscale/utils/threading/__init__.py +8 -0
  435. regscale/utils/threading/threadhandler.py +131 -0
  436. regscale/utils/threading/threadsafe_counter.py +47 -0
  437. regscale/utils/threading/threadsafe_dict.py +242 -0
  438. regscale/utils/threading/threadsafe_list.py +83 -0
  439. regscale/utils/version.py +104 -0
  440. regscale/validation/__init__.py +0 -0
  441. regscale/validation/address.py +37 -0
  442. regscale/validation/record.py +48 -0
  443. regscale/visualization/__init__.py +5 -0
  444. regscale/visualization/click.py +34 -0
  445. regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
  446. regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
  447. regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
  448. regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
  449. regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
  450. regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
  451. tests/fixtures/__init__.py +2 -0
  452. tests/fixtures/api.py +87 -0
  453. tests/fixtures/models.py +91 -0
  454. tests/fixtures/test_fixture.py +144 -0
  455. tests/mocks/__init__.py +0 -0
  456. tests/mocks/objects.py +3 -0
  457. tests/mocks/response.py +32 -0
  458. tests/mocks/xml.py +13 -0
  459. tests/regscale/__init__.py +0 -0
  460. tests/regscale/core/__init__.py +0 -0
  461. tests/regscale/core/test_api.py +232 -0
  462. tests/regscale/core/test_app.py +406 -0
  463. tests/regscale/core/test_login.py +37 -0
  464. tests/regscale/core/test_logz.py +66 -0
  465. tests/regscale/core/test_sbom_generator.py +87 -0
  466. tests/regscale/core/test_validation_utils.py +163 -0
  467. tests/regscale/core/test_version.py +78 -0
  468. tests/regscale/models/__init__.py +0 -0
  469. tests/regscale/models/test_asset.py +71 -0
  470. tests/regscale/models/test_config.py +26 -0
  471. tests/regscale/models/test_control_implementation.py +27 -0
  472. tests/regscale/models/test_import.py +97 -0
  473. tests/regscale/models/test_issue.py +36 -0
  474. tests/regscale/models/test_mapping.py +52 -0
  475. tests/regscale/models/test_platform.py +31 -0
  476. tests/regscale/models/test_regscale_model.py +346 -0
  477. tests/regscale/models/test_report.py +32 -0
  478. tests/regscale/models/test_tenable_integrations.py +118 -0
  479. tests/regscale/models/test_user_model.py +121 -0
  480. tests/regscale/test_about.py +19 -0
  481. tests/regscale/test_authorization.py +65 -0
@@ -0,0 +1,1046 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Jira integration for RegScale CLI"""
4
+
5
+ # Standard python imports
6
+ import os
7
+ import tempfile
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from datetime import datetime, timedelta
10
+ from io import BytesIO
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any, Optional, Tuple, Union
13
+ from urllib.parse import urljoin
14
+
15
+ if TYPE_CHECKING:
16
+ from regscale.core.app.application import Application
17
+
18
+ import click
19
+ from jira import JIRA
20
+ from jira import Issue as jiraIssue
21
+ from jira import JIRAError
22
+ from rich.progress import Progress
23
+
24
+ from regscale.core.app.api import Api
25
+ from regscale.core.app.logz import create_logger
26
+ from regscale.core.app.utils.app_utils import (
27
+ check_file_path,
28
+ check_license,
29
+ compute_hashes_in_directory,
30
+ convert_datetime_to_regscale_string,
31
+ create_progress_object,
32
+ error_and_exit,
33
+ get_current_datetime,
34
+ save_data_to,
35
+ )
36
+ from regscale.core.app.utils.regscale_utils import verify_provided_module
37
+ from regscale.models import regscale_id, regscale_module
38
+ from regscale.models.regscale_models.file import File
39
+ from regscale.models.regscale_models.issue import Issue
40
+ from regscale.models.regscale_models.task import Task
41
+ from regscale.utils.threading.threadhandler import create_threads, thread_assignment
42
+
43
+ job_progress = create_progress_object()
44
+ logger = create_logger()
45
+ update_issues = []
46
+ new_regscale_issues = []
47
+ updated_regscale_issues = []
48
+ update_counter = []
49
+
50
+
51
+ ####################################################################################################
52
+ #
53
+ # PROCESS ISSUES TO JIRA
54
+ # JIRA CLI Python Docs: https://jira.readthedocs.io/examples.html#issues
55
+ # JIRA API Docs: https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/
56
+ #
57
+ ####################################################################################################
58
+
59
+
60
+ # Create group to handle Jira integration
61
+ @click.group()
62
+ def jira():
63
+ """Sync issues between Jira and RegScale."""
64
+
65
+
66
+ @jira.command()
67
+ @regscale_id()
68
+ @regscale_module()
69
+ @click.option(
70
+ "--jira_project",
71
+ type=click.STRING,
72
+ help="RegScale will sync the issues for the record to the Jira project.",
73
+ prompt="Enter the name of the project in Jira",
74
+ required=True,
75
+ )
76
+ @click.option(
77
+ "--jira_issue_type",
78
+ type=click.STRING,
79
+ help="Enter the Jira issue type to use when creating new issues from RegScale. (CASE SENSITIVE)",
80
+ prompt="Enter the Jira issue type",
81
+ required=True,
82
+ )
83
+ @click.option(
84
+ "--sync_attachments",
85
+ type=click.BOOL,
86
+ help=(
87
+ "Whether RegScale will sync the attachments for the issue "
88
+ "in the provided Jira project and vice versa. Defaults to True."
89
+ ),
90
+ required=False,
91
+ default=True,
92
+ )
93
+ @click.option(
94
+ "--token_auth",
95
+ "-t",
96
+ is_flag=True,
97
+ help="Use token authentication for Jira API instead of basic auth, defaults to False.",
98
+ )
99
+ def issues(
100
+ regscale_id: int,
101
+ regscale_module: str,
102
+ jira_project: str,
103
+ jira_issue_type: str,
104
+ sync_attachments: bool = True,
105
+ token_auth: bool = False,
106
+ ):
107
+ """Sync issues from Jira into RegScale."""
108
+ sync_regscale_and_jira(
109
+ parent_id=regscale_id,
110
+ parent_module=regscale_module,
111
+ jira_project=jira_project,
112
+ jira_issue_type=jira_issue_type,
113
+ sync_attachments=sync_attachments,
114
+ token_auth=token_auth,
115
+ )
116
+
117
+
118
+ @jira.command()
119
+ @regscale_id()
120
+ @regscale_module()
121
+ @click.option(
122
+ "--jira_project",
123
+ type=click.STRING,
124
+ help="RegScale will sync the issues for the record to the Jira project.",
125
+ prompt="Enter the name of the project in Jira",
126
+ required=True,
127
+ )
128
+ @click.option(
129
+ "--token_auth",
130
+ "-t",
131
+ is_flag=True,
132
+ help="Use token authentication for Jira API instead of basic auth, defaults to False.",
133
+ )
134
+ def tasks(
135
+ regscale_id: int,
136
+ regscale_module: str,
137
+ jira_project: str,
138
+ token_auth: bool = False,
139
+ ):
140
+ """Sync tasks from Jira into RegScale."""
141
+ sync_regscale_and_jira(
142
+ parent_id=regscale_id,
143
+ parent_module=regscale_module,
144
+ jira_project=jira_project,
145
+ jira_issue_type="Task",
146
+ sync_attachments=False,
147
+ sync_tasks_only=True,
148
+ token_auth=token_auth,
149
+ )
150
+
151
+
152
+ def sync_regscale_and_jira(
153
+ parent_id: int,
154
+ parent_module: str,
155
+ jira_project: str,
156
+ jira_issue_type: str,
157
+ sync_attachments: bool = True,
158
+ sync_tasks_only: bool = False,
159
+ token_auth: bool = False,
160
+ ) -> None:
161
+ """
162
+ Sync issues, bidirectionally, from Jira into RegScale as issues
163
+
164
+ :param int parent_id: ID # from RegScale to associate issues with
165
+ :param str parent_module: RegScale module to associate issues with
166
+ :param str jira_project: Name of the project in Jira
167
+ :param str jira_issue_type: Type of issues to sync from Jira
168
+ :param bool sync_attachments: Whether to sync attachments in RegScale & Jira, defaults to True
169
+ :param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
170
+ :param bool token_auth: Use token authentication for Jira API, defaults to False
171
+ :rtype: None
172
+ """
173
+ app = check_license()
174
+ api = Api()
175
+ config = app.config
176
+
177
+ # see if provided RegScale Module is an accepted option
178
+ verify_provided_module(parent_module)
179
+
180
+ # create Jira client
181
+ jira_client = create_jira_client(config, token_auth)
182
+
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
+ )
197
+
198
+ jira_objects = fetch_jira_objects(
199
+ jira_client=jira_client,
200
+ jira_project=jira_project,
201
+ jql_str=jql_str,
202
+ jira_issue_type=jira_issue_type,
203
+ sync_tasks_only=sync_tasks_only,
204
+ )
205
+
206
+ if regscale_issues and not sync_tasks_only:
207
+ # sync RegScale issues to Jira
208
+ if issues_to_update := sync_regscale_to_jira(
209
+ regscale_issues=regscale_issues,
210
+ jira_client=jira_client,
211
+ jira_project=jira_project,
212
+ jira_issue_type=jira_issue_type,
213
+ api=api,
214
+ sync_attachments=sync_attachments,
215
+ attachments=regscale_attachments,
216
+ ):
217
+ with job_progress:
218
+ # create task to update RegScale issues
219
+ 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),
222
+ )
223
+ # create threads to analyze Jira issues and RegScale issues
224
+ create_threads(
225
+ process=update_regscale_issues,
226
+ args=(
227
+ issues_to_update,
228
+ api,
229
+ updating_issues,
230
+ ),
231
+ thread_count=len(issues_to_update),
232
+ )
233
+ # output the final result
234
+ logger.info(
235
+ "%i/%i issue(s) updated in RegScale.",
236
+ len(issues_to_update),
237
+ len(update_counter),
238
+ )
239
+ elif not sync_tasks_only:
240
+ logger.info("No issues need to be updated in RegScale.")
241
+
242
+ if jira_objects:
243
+ sync_regscale_objects_to_jira(
244
+ jira_objects, regscale_issues, sync_attachments, app, parent_id, parent_module, sync_tasks_only
245
+ )
246
+ else:
247
+ logger.info(f"No {'tasks' if sync_tasks_only else 'issues'} need to be analyzed from Jira.")
248
+
249
+
250
+ def sync_regscale_objects_to_jira(
251
+ jira_issues: list[jiraIssue],
252
+ regscale_objects: list[Union[Issue, Task]],
253
+ sync_attachments: bool,
254
+ app: "Application",
255
+ parent_id: int,
256
+ parent_module: str,
257
+ sync_tasks_only: bool,
258
+ ):
259
+ """
260
+ Sync issues from Jira to RegScale
261
+
262
+ :param list[jiraIssue] jira_issues: List of Jira issues to sync to RegScale
263
+ :param list[Union[Issue, Task]] regscale_objects: List of RegScale issues or tasks to compare to Jira issues
264
+ :param bool sync_attachments: Sync attachments from Jira to RegScale, defaults to True
265
+ :param Application app: RegScale CLI application object
266
+ :param int parent_id: Parent record ID in RegScale
267
+ :param str parent_module: Parent record module in RegScale
268
+ :param bool sync_tasks_only: Whether to sync only tasks from Jira
269
+ """
270
+ issues_closed = []
271
+ with job_progress:
272
+ type_str = "task" if sync_tasks_only else "issue"
273
+ creating_issues = job_progress.add_task(
274
+ f"[#f8b737]Comparing {len(jira_issues)} Jira {type_str}(s)"
275
+ f" and {len(regscale_objects)} RegScale {type_str}(s)...",
276
+ total=len(jira_issues),
277
+ )
278
+ jira_client = create_jira_client(app.config)
279
+ if sync_tasks_only:
280
+ tasks_inserted, tasks_updated, tasks_closed = create_and_update_regscale_tasks(
281
+ jira_issues=jira_issues,
282
+ existing_tasks=regscale_objects,
283
+ parent_id=parent_id,
284
+ parent_module=parent_module,
285
+ progress=job_progress,
286
+ progress_task=creating_issues,
287
+ )
288
+ else:
289
+ create_threads(
290
+ process=create_and_update_regscale_issues,
291
+ args=(
292
+ jira_issues,
293
+ regscale_objects,
294
+ sync_attachments,
295
+ jira_client,
296
+ app,
297
+ parent_id,
298
+ parent_module,
299
+ creating_issues,
300
+ job_progress,
301
+ ),
302
+ thread_count=len(jira_issues),
303
+ )
304
+ logger.info(
305
+ "Analyzed %i Jira %s(s), created %i %s(s), updated %i %s(s), and closed %i %s(s) in RegScale.",
306
+ len(jira_issues),
307
+ type_str,
308
+ len(new_regscale_issues) if not sync_tasks_only else tasks_inserted,
309
+ type_str,
310
+ len(updated_regscale_issues) if not sync_tasks_only else tasks_updated,
311
+ type_str,
312
+ len(issues_closed) if not sync_tasks_only else tasks_closed,
313
+ type_str,
314
+ )
315
+
316
+
317
+ def create_jira_client(
318
+ config: dict,
319
+ token_auth: bool = False,
320
+ ) -> JIRA:
321
+ """
322
+ Create a Jira client to use for interacting with Jira
323
+
324
+ :param dict config: RegScale CLI application config
325
+ :param bool token_auth: Use token authentication for Jira API, defaults to False
326
+ :return: JIRA Client
327
+ :rtype: JIRA
328
+ """
329
+ from regscale.integrations.variables import ScannerVariables
330
+
331
+ url = config["jiraUrl"]
332
+ token = config["jiraApiToken"]
333
+ jira_user = config["jiraUserName"]
334
+ if token_auth:
335
+ return JIRA(token_auth=token, options={"server": url, "verify": ScannerVariables.sslVerify})
336
+
337
+ # set the JIRA Url
338
+ return JIRA(basic_auth=(jira_user, token), options={"server": url})
339
+
340
+
341
+ def update_regscale_issues(args: Tuple, thread: int) -> None:
342
+ """
343
+ Function to compare Jira issues and RegScale issues
344
+
345
+ :param Tuple args: Tuple of args to use during the process
346
+ :param int thread: Thread number of current thread
347
+ :rtype: None
348
+ """
349
+ # set up local variables from the passed args
350
+ (
351
+ regscale_issues,
352
+ app,
353
+ task,
354
+ ) = args
355
+ # find which records should be executed by the current thread
356
+ threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
357
+ # iterate through the thread assignment items and process them
358
+ for i in range(len(threads)):
359
+ # set the issue for the thread for later use in the function
360
+ issue = regscale_issues[threads[i]]
361
+ # update the issue in RegScale
362
+ issue.save()
363
+ logger.debug(
364
+ "RegScale Issue %i was updated with the Jira link.",
365
+ issue.id,
366
+ )
367
+ update_counter.append(issue)
368
+ # update progress bar
369
+ job_progress.update(task, advance=1)
370
+
371
+
372
+ def convert_task_status(name: str) -> str:
373
+ """
374
+ Convert the task status from Jira to RegScale
375
+
376
+ :param str name: Name of the task status in Jira
377
+ :return: Name of the task status in RegScale
378
+ :rtype: str
379
+ """
380
+ jira_regscale_map = {
381
+ "to do": "Backlog",
382
+ "in progress": "Open",
383
+ "done": "Closed",
384
+ "closed": "Closed",
385
+ "cancelled": "Cancelled",
386
+ "canceled": "Cancelled",
387
+ }
388
+ return jira_regscale_map.get(name.lower(), "Open")
389
+
390
+
391
+ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_id: int, parent_module: str) -> Task:
392
+ """
393
+ Function to create a Task object from a Jira issue
394
+
395
+ :param dict config: Application config
396
+ :param jiraIssue jira_issue: Jira issue to create a Task object from
397
+ :param int parent_id: Parent record ID in RegScale
398
+ :param str parent_module: Parent record module in RegScale
399
+ :return: RegScale Task object
400
+ :rtype: Task
401
+ """
402
+ description = jira_issue.fields.description
403
+ due_date = jira_issue.fields.duedate
404
+ status = convert_task_status(jira_issue.fields.status.name)
405
+ status_change_date = convert_datetime_to_regscale_string(
406
+ datetime.strptime(jira_issue.fields.statuscategorychangedate, "%Y-%m-%dT%H:%M:%S.%f%z")
407
+ )
408
+ title = jira_issue.fields.summary
409
+ date_closed = None
410
+ percent_complete = None
411
+ if not due_date:
412
+ delta = config["issues"]["jira"]["medium"]
413
+ due_date = convert_datetime_to_regscale_string(datetime.now() + timedelta(days=delta))
414
+ if status == "Closed":
415
+ date_closed = status_change_date
416
+ percent_complete = 100
417
+
418
+ return Task(
419
+ title=title,
420
+ status=status,
421
+ description=description,
422
+ dueDate=due_date,
423
+ parentId=parent_id,
424
+ parentModule=parent_module,
425
+ dateClosed=date_closed,
426
+ percentComplete=percent_complete,
427
+ otherIdentifier=jira_issue.key,
428
+ )
429
+
430
+
431
+ def check_and_close_tasks(existing_tasks: list[Task], all_jira_titles: set[str]) -> list[Task]:
432
+ """
433
+ Function to check and close tasks that are not in Jira
434
+
435
+ :param list[Task] existing_tasks: List of existing tasks in RegScale
436
+ :param set[str] all_jira_titles: Set of all Jira task titles
437
+ :return: List of tasks to close
438
+ :rtype: list[Task]
439
+ """
440
+ close_tasks = []
441
+ for task in existing_tasks:
442
+ if task.title not in all_jira_titles and task.status != "Closed":
443
+ task.status = "Closed"
444
+ task.percentComplete = 100
445
+ task.dateClosed = get_current_datetime()
446
+ close_tasks.append(task)
447
+ return close_tasks
448
+
449
+
450
+ def create_and_update_regscale_tasks(
451
+ jira_issues: list[jiraIssue],
452
+ existing_tasks: list[Task],
453
+ parent_id: int,
454
+ parent_module: str,
455
+ progress: Progress,
456
+ progress_task: Any,
457
+ ) -> tuple[int, int, int]:
458
+ """
459
+ Function to create or update Tasks in RegScale from Jira
460
+
461
+ :param list[jiraIssue] jira_issues: List of Jira issues to create or update in RegScale
462
+ :param list[Task] existing_tasks: List of existing tasks in RegScale
463
+ :param int parent_id: Parent record ID in RegScale
464
+ :param str parent_module: Parent record module in RegScale
465
+ :param Progress progress: Job progress object to use for updating the progress bar
466
+ :param Any progress_task: Task object to update the progress bar
467
+ :return: A tuple of counts
468
+ :rtype: tuple[int, int, int]
469
+ """
470
+ from regscale.core.app.application import Application
471
+
472
+ app = Application()
473
+ config = app.config
474
+ insert_tasks = []
475
+ update_tasks = []
476
+ all_jira_titles = {jira_issue.fields.summary for jira_issue in jira_issues}
477
+ for jira_issue in jira_issues:
478
+ 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)
484
+ task.id = existing_task.id
485
+ update_tasks.append(task)
486
+ progress.update(progress_task, advance=1)
487
+ close_tasks = check_and_close_tasks(existing_tasks, all_jira_titles)
488
+
489
+ with progress:
490
+ 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),
495
+ )
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)
516
+
517
+
518
+ def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
519
+ """
520
+ Function to create or update issues in RegScale from Jira
521
+
522
+ :param Tuple args: Tuple of args to use during the process
523
+ :param int thread: Thread number of current thread
524
+ :rtype: None
525
+ """
526
+ # set up local variables from the passed args
527
+ (jira_issues, regscale_issues, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
528
+ # find which records should be executed by the current thread
529
+ threads = thread_assignment(thread=thread, total_items=len(jira_issues))
530
+
531
+ # iterate through the thread assignment items and process them
532
+ for i in range(len(threads)):
533
+ jira_issue: jiraIssue = jira_issues[threads[i]]
534
+ regscale_issue: Optional[Issue] = next(
535
+ (issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None
536
+ )
537
+ # see if the Jira issue needs to be created in RegScale
538
+ if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
539
+ # update the status and date completed of the RegScale issue
540
+ regscale_issue.status = "Closed"
541
+ regscale_issue.dateCompleted = get_current_datetime()
542
+ # update the issue in RegScale
543
+ updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
544
+ elif regscale_issue:
545
+ # update the issue in RegScale
546
+ updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
547
+ else:
548
+ # map the jira issue to a RegScale issue object
549
+ issue = map_jira_to_regscale_issue(
550
+ jira_issue=jira_issue,
551
+ config=app.config,
552
+ parent_id=parent_id,
553
+ parent_module=parent_module,
554
+ )
555
+ # create the issue in RegScale
556
+ if regscale_issue := Issue.insert_issue(
557
+ app=app,
558
+ issue=issue,
559
+ ):
560
+ logger.debug(
561
+ "Created issue #%i-%s in RegScale.",
562
+ regscale_issue.id,
563
+ regscale_issue.title,
564
+ )
565
+ new_regscale_issues.append(regscale_issue)
566
+ else:
567
+ logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
568
+ if add_attachments and regscale_issue and jira_issue.fields.attachment:
569
+ # determine which attachments need to be uploaded to prevent duplicates by
570
+ # getting the hashes of all Jira & RegScale attachments
571
+ compare_files_for_dupes_and_upload(
572
+ jira_issue=jira_issue,
573
+ regscale_issue=regscale_issue,
574
+ jira_client=jira_client,
575
+ api=Api(),
576
+ )
577
+ # update progress bar
578
+ progress.update(task, advance=1)
579
+
580
+
581
+ def sync_regscale_to_jira(
582
+ regscale_issues: list[Issue],
583
+ jira_client: JIRA,
584
+ jira_project: str,
585
+ jira_issue_type: str,
586
+ sync_attachments: bool = True,
587
+ attachments: Optional[dict] = None,
588
+ api: Optional[Api] = None,
589
+ ) -> list[Issue]:
590
+ """
591
+ Sync issues from RegScale to Jira
592
+
593
+ :param list[Issue] regscale_issues: list of RegScale issues to sync to Jira
594
+ :param JIRA jira_client: Jira client to use for issue creation in Jira
595
+ :param str jira_project: Jira Project to create the issues in
596
+ :param str jira_issue_type: Type of issue to create in Jira
597
+ :param bool sync_attachments: Sync attachments from RegScale to Jira, defaults to True
598
+ :param Optional[dict] attachments: Dict of attachments to sync from RegScale to Jira, defaults to None
599
+ :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]
602
+ """
603
+ new_issue_counter = 0
604
+ issuess_to_update = []
605
+ with job_progress:
606
+ # create task to create Jira issues
607
+ creating_issues = job_progress.add_task(
608
+ f"[#f8b737]Verifying {len(regscale_issues)} RegScale issue(s) exist in Jira...",
609
+ total=len(regscale_issues),
610
+ )
611
+ for issue in regscale_issues:
612
+ # see if Jira issue already exists
613
+ if not issue.jiraId or issue.jiraId == "":
614
+ new_issue = create_issue_in_jira(
615
+ issue=issue,
616
+ jira_client=jira_client,
617
+ jira_project=jira_project,
618
+ issue_type=jira_issue_type,
619
+ add_attachments=sync_attachments,
620
+ attachments=attachments,
621
+ api=api,
622
+ )
623
+ # log progress
624
+ new_issue_counter += 1
625
+ # get the Jira ID
626
+ jira_id = new_issue.key
627
+ # update the RegScale issue for the Jira link
628
+ issue.jiraId = jira_id
629
+ # add the issue to the update_issues global list
630
+ issuess_to_update.append(issue)
631
+ job_progress.update(creating_issues, advance=1)
632
+ # output the final result
633
+ logger.info("%i new issue(s) opened in Jira.", new_issue_counter)
634
+ return issuess_to_update
635
+
636
+
637
+ def fetch_jira_objects(
638
+ jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, sync_tasks_only: bool = False
639
+ ) -> list[jiraIssue]:
640
+ """
641
+ Fetch all issues from Jira for the provided project
642
+
643
+ :param JIRA jira_client: Jira client to use for the request
644
+ :param str jira_project: Name of the project in Jira
645
+ :param str jira_issue_type: Type of issue to fetch from Jira
646
+ :param str jql_str: JQL string to use for the request, default None
647
+ :param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
648
+ :return: List of Jira issues
649
+ :rtype: list[jiraIssue]
650
+ """
651
+ start_pointer = 0
652
+ page_size = 100
653
+ jira_objects = []
654
+ if sync_tasks_only:
655
+ validate_issue_type(jira_client, jira_issue_type)
656
+ output_str = "task"
657
+ else:
658
+ output_str = "issue"
659
+ logger.info("Fetching %s(s) from Jira...", output_str.lower())
660
+ # get all issues for the Jira project
661
+ while True:
662
+ start = start_pointer * page_size
663
+ jira_issues_response = jira_client.search_issues(
664
+ jql_str=jql_str,
665
+ startAt=start,
666
+ maxResults=page_size,
667
+ )
668
+ if len(jira_objects) == jira_issues_response.total:
669
+ break
670
+ start_pointer += 1
671
+ # append new records to jira_issues
672
+ jira_objects.extend(jira_issues_response)
673
+ logger.info(
674
+ "%i/%i Jira %s(s) retrieved.",
675
+ len(jira_objects),
676
+ jira_issues_response.total,
677
+ output_str.lower(),
678
+ )
679
+ if jira_objects:
680
+ check_file_path("artifacts")
681
+ file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
682
+ file_path = Path(f"./artifacts/{file_name}")
683
+ save_data_to(
684
+ file=file_path,
685
+ data=[issue.raw for issue in jira_objects],
686
+ output_log=False,
687
+ )
688
+ logger.info(
689
+ "Saved %i Jira %s(s), see %s",
690
+ len(jira_objects),
691
+ jira_issue_type.lower(),
692
+ str(file_path.absolute()),
693
+ )
694
+ logger.info("%i %s(s) retrieved from Jira.", len(jira_objects), output_str.lower())
695
+ return jira_objects
696
+
697
+
698
+ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str) -> Issue:
699
+ """
700
+ Map Jira issues to RegScale issues
701
+
702
+ :param jiraIssue jira_issue: Jira issue to map to issue in RegScale
703
+ :param dict config: Application config
704
+ :param int parent_id: Parent record ID in RegScale
705
+ :param str parent_module: Parent record module in RegScale
706
+ :return: Issue object of the newly created issue in RegScale
707
+ :rtype: Issue
708
+ """
709
+ due_date = map_jira_due_date(jira_issue, config)
710
+ issue = Issue(
711
+ title=jira_issue.fields.summary,
712
+ severityLevel=Issue.assign_severity(jira_issue.fields.priority.name),
713
+ issueOwnerId=config["userId"],
714
+ dueDate=due_date,
715
+ description=(
716
+ f"Description {jira_issue.fields.description}"
717
+ f"\nStatus: {jira_issue.fields.status.name}"
718
+ f"\nDue Date: {due_date}"
719
+ ),
720
+ status=("Closed" if jira_issue.fields.status.name.lower() == "done" else config["issues"]["jira"]["status"]),
721
+ jiraId=jira_issue.key,
722
+ parentId=parent_id,
723
+ parentModule=parent_module,
724
+ dateCreated=get_current_datetime(),
725
+ dateCompleted=(get_current_datetime() if jira_issue.fields.status.name.lower() == "done" else None),
726
+ )
727
+ return issue
728
+
729
+
730
+ def map_jira_due_date(jira_issue: Optional[jiraIssue], config: dict) -> str:
731
+ """
732
+ Parses the provided jira_issue for a due date and returns it as a string
733
+
734
+ :param Optional[jiraIssue] jira_issue: Jira issue to parse for a due date
735
+ :param dict config: Application config
736
+ :return: Due date as a string
737
+ :rtype: str
738
+ """
739
+ if jira_issue.fields.duedate:
740
+ due_date = jira_issue.fields.duedate
741
+ elif jira_issue.fields.priority:
742
+ due_date = datetime.now() + timedelta(days=config["issues"]["jira"][jira_issue.fields.priority.name.lower()])
743
+ due_date = convert_datetime_to_regscale_string(due_date)
744
+ else:
745
+ due_date = datetime.now() + timedelta(days=config["issues"]["jira"]["medium"])
746
+ due_date = convert_datetime_to_regscale_string(due_date)
747
+ return due_date
748
+
749
+
750
+ def _generate_jira_comment(issue: Issue) -> str:
751
+ """
752
+ Generate a Jira comment from a RegScale issue and it's populated fields
753
+
754
+ :param Issue issue: RegScale issue to generate a Jira comment from
755
+ :return: Jira comment
756
+ :rtype: str
757
+ """
758
+ comment = ""
759
+ exclude_fields = ["createdById", "lastUpdatedById", "issueOwnerId", "uuid"] + issue._exclude_graphql_fields
760
+ for field_name, field_value in issue.__dict__.items():
761
+ if field_value and field_name not in exclude_fields:
762
+ comment += f"**{field_name}:** {field_value}\n"
763
+ return comment
764
+
765
+
766
+ def create_issue_in_jira(
767
+ issue: Issue,
768
+ jira_client: JIRA,
769
+ jira_project: str,
770
+ issue_type: str,
771
+ add_attachments: Optional[bool] = True,
772
+ attachments: Optional[dict] = None,
773
+ api: Optional[Api] = None,
774
+ ) -> jiraIssue:
775
+ """
776
+ Create a new issue in Jira
777
+
778
+ :param Issue issue: RegScale issue object
779
+ :param JIRA jira_client: Jira client to use for issue creation in Jira
780
+ :param str jira_project: Project name in Jira to create the issue in
781
+ :param str issue_type: The type of issue to create in Jira
782
+ :param Optional[bool] add_attachments: Whether to add attachments to new issue, defaults to true
783
+ :param Optional[dict] attachments: Dictionary containing attachments, defaults to None
784
+ :param Optional[Api] api: API object to download attachments, defaults to None
785
+ :return: Newly created issue in Jira
786
+ :rtype: jiraIssue
787
+ """
788
+ if not api:
789
+ api = Api()
790
+ 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)
793
+ new_issue = jira_client.create_issue(
794
+ project=jira_project,
795
+ summary=issue.title,
796
+ description=reg_issue_url + issue.description,
797
+ issuetype=issue_type,
798
+ )
799
+ logger.debug("Jira issue created: %s", new_issue.key)
800
+ # add a comment to the new Jira issue
801
+ logger.debug("Adding comment to Jira issue: %s", new_issue.key)
802
+ _ = jira_client.add_comment(
803
+ issue=new_issue,
804
+ body=reg_issue_url + _generate_jira_comment(issue),
805
+ )
806
+ logger.debug("Comment added to Jira issue: %s", new_issue.key)
807
+ except JIRAError as ex:
808
+ error_and_exit(f"Unable to create Jira issue.\nError: {ex}")
809
+ # add the attachments to the new Jira issue
810
+ if add_attachments and attachments:
811
+ compare_files_for_dupes_and_upload(
812
+ jira_issue=new_issue,
813
+ regscale_issue=issue,
814
+ jira_client=jira_client,
815
+ api=api,
816
+ )
817
+ return new_issue
818
+
819
+
820
+ def compare_files_for_dupes_and_upload(
821
+ jira_issue: jiraIssue, regscale_issue: Issue, jira_client: JIRA, api: Api
822
+ ) -> None:
823
+ """
824
+ Compare files for duplicates and upload them to Jira and RegScale
825
+
826
+ :param jiraIssue jira_issue: Jira issue to upload the attachments to
827
+ :param Issue regscale_issue: RegScale issue to upload the attachments from
828
+ :param JIRA jira_client: Jira client to use for uploading the attachments
829
+ :param Api api: Api object to use for interacting with RegScale
830
+ :rtype: None
831
+ :return: None
832
+ """
833
+ jira_uploaded_attachments = []
834
+ regscale_uploaded_attachments = []
835
+ with tempfile.TemporaryDirectory() as temp_dir:
836
+ jira_dir, regscale_dir = download_issue_attachments_to_directory(
837
+ directory=temp_dir,
838
+ jira_issue=jira_issue,
839
+ regscale_issue=regscale_issue,
840
+ api=api,
841
+ )
842
+ jira_attachment_hashes = compute_hashes_in_directory(jira_dir)
843
+ regscale_attachment_hashes = compute_hashes_in_directory(regscale_dir)
844
+
845
+ upload_files_to_jira(
846
+ jira_attachment_hashes,
847
+ regscale_attachment_hashes,
848
+ jira_issue,
849
+ regscale_issue,
850
+ jira_client,
851
+ jira_uploaded_attachments,
852
+ )
853
+ upload_files_to_regscale(
854
+ jira_attachment_hashes, regscale_attachment_hashes, regscale_issue, api, regscale_uploaded_attachments
855
+ )
856
+
857
+ log_upload_results(regscale_uploaded_attachments, jira_uploaded_attachments, regscale_issue, jira_issue)
858
+
859
+
860
+ def upload_files_to_jira(
861
+ jira_attachment_hashes: dict,
862
+ regscale_attachment_hashes: dict,
863
+ jira_issue: jiraIssue,
864
+ regscale_issue: Issue,
865
+ jira_client: JIRA,
866
+ jira_uploaded_attachments: list,
867
+ ) -> None:
868
+ """
869
+ Upload files to Jira
870
+
871
+ :param dict jira_attachment_hashes: Dictionary of Jira attachment hashes
872
+ :param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
873
+ :param jiraIssue jira_issue: Jira issue to upload the attachments to
874
+ :param Issue regscale_issue: RegScale issue to upload the attachments from
875
+ :param JIRA jira_client: Jira client to use for uploading the attachments
876
+ :param list jira_uploaded_attachments: List of Jira attachments that were uploaded
877
+ :rtype: None
878
+ :return: None
879
+ """
880
+ for file_hash, file in regscale_attachment_hashes.items():
881
+ if file_hash not in jira_attachment_hashes:
882
+ try:
883
+ with open(file, "rb") as in_file:
884
+ jira_client.add_attachment(
885
+ issue=jira_issue.id,
886
+ attachment=BytesIO(in_file.read()), # type: ignore
887
+ filename=f"RegScale_Issue_{regscale_issue.id}_{Path(file).name}",
888
+ )
889
+ jira_uploaded_attachments.append(file)
890
+ except JIRAError as ex:
891
+ logger.error(
892
+ "Unable to upload %s to Jira issue %s.\nError: %s",
893
+ Path(file).name,
894
+ jira_issue.key,
895
+ ex,
896
+ )
897
+ except TypeError as ex:
898
+ logger.error(
899
+ "Unable to upload %s to Jira issue %s.\nError: %s",
900
+ Path(file).name,
901
+ jira_issue.key,
902
+ ex,
903
+ )
904
+
905
+
906
+ def upload_files_to_regscale(
907
+ jira_attachment_hashes: dict,
908
+ regscale_attachment_hashes: dict,
909
+ regscale_issue: Issue,
910
+ api: Api,
911
+ regscale_uploaded_attachments: list,
912
+ ) -> None:
913
+ """
914
+ Upload files to RegScale
915
+
916
+ :param dict jira_attachment_hashes: Dictionary of Jira attachment hashes
917
+ :param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
918
+ :param Issue regscale_issue: RegScale issue to upload the attachments to
919
+ :param Api api: Api object to use for interacting with RegScale
920
+ :param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
921
+ :rtype: None
922
+ :return: None
923
+ """
924
+ for file_hash, file in jira_attachment_hashes.items():
925
+ if file_hash not in regscale_attachment_hashes:
926
+ with open(file, "rb") as in_file:
927
+ if File.upload_file_to_regscale(
928
+ file_name=f"Jira_attachment_{Path(file).name}",
929
+ parent_id=regscale_issue.id,
930
+ parent_module="issues",
931
+ api=api,
932
+ file_data=in_file.read(),
933
+ ):
934
+ regscale_uploaded_attachments.append(file)
935
+ logger.debug(
936
+ "Uploaded %s to RegScale issue #%i.",
937
+ Path(file).name,
938
+ regscale_issue.id,
939
+ )
940
+ else:
941
+ logger.warning(
942
+ "Unable to upload %s to RegScale issue #%i.",
943
+ Path(file).name,
944
+ regscale_issue.id,
945
+ )
946
+
947
+
948
+ def log_upload_results(
949
+ regscale_uploaded_attachments: list, jira_uploaded_attachments: list, regscale_issue: Issue, jira_issue: jiraIssue
950
+ ) -> None:
951
+ """
952
+ Log the results of the upload process
953
+
954
+ :param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
955
+ :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
957
+ :param jiraIssue jira_issue: Jira issue that the attachments were uploaded to
958
+ :rtype: None
959
+ :return: None
960
+ """
961
+ if regscale_uploaded_attachments and jira_uploaded_attachments:
962
+ logger.info(
963
+ "%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to Jira issue %s.",
964
+ len(regscale_uploaded_attachments),
965
+ regscale_issue.id,
966
+ len(jira_uploaded_attachments),
967
+ jira_issue.key,
968
+ )
969
+ elif jira_uploaded_attachments:
970
+ logger.info(
971
+ "%i file(s) uploaded to Jira issue %s.",
972
+ len(jira_uploaded_attachments),
973
+ jira_issue.key,
974
+ )
975
+ elif regscale_uploaded_attachments:
976
+ logger.info(
977
+ "%i file(s) uploaded to RegScale issue #%i.",
978
+ len(regscale_uploaded_attachments),
979
+ regscale_issue.id,
980
+ )
981
+
982
+
983
+ def validate_issue_type(jira_client: JIRA, issue_type: str) -> Any:
984
+ """
985
+ Validate the provided issue type in Jira
986
+
987
+ :param JIRA jira_client: Jira client to use for the request
988
+ :param str issue_type: Issue type to validate
989
+ :rtype: Any
990
+ :return: True if the issue type is valid, otherwise exit with an error
991
+ """
992
+ issue_types = jira_client.issue_types()
993
+ for issue in issue_types:
994
+ if issue.name == issue_type:
995
+ return True
996
+ message = f"Invalid Jira issue type provided: {issue_type}, the available types are: " + ", ".join(
997
+ {iss.name for iss in issue_types}
998
+ )
999
+ error_and_exit(error_desc=message)
1000
+
1001
+
1002
+ def download_issue_attachments_to_directory(
1003
+ directory: str,
1004
+ jira_issue: jiraIssue,
1005
+ regscale_issue: Issue,
1006
+ api: Api,
1007
+ ) -> tuple[str, str]:
1008
+ """
1009
+ Function to download attachments from Jira and RegScale issues to a directory
1010
+
1011
+ :param str directory: Directory to store the files in
1012
+ :param jiraIssue jira_issue: Jira issue to download the attachments for
1013
+ :param Issue regscale_issue: RegScale issue to download the attachments for
1014
+ :param Api api: Api object to use for interacting with RegScale
1015
+ :return: Tuple of strings containing the Jira and RegScale directories
1016
+ :rtype: tuple[str, str]
1017
+ """
1018
+ # determine which attachments need to be uploaded to prevent duplicates by checking hashes
1019
+ jira_dir = os.path.join(directory, "jira")
1020
+ check_file_path(jira_dir, False)
1021
+ # download all attachments from Jira to the jira directory in temp_dir
1022
+ for attachment in jira_issue.fields.attachment:
1023
+ with open(os.path.join(jira_dir, attachment.filename), "wb") as file:
1024
+ file.write(attachment.get())
1025
+ # get the regscale issue attachments
1026
+ regscale_issue_attachments = File.get_files_for_parent_from_regscale(
1027
+ api=api,
1028
+ parent_id=regscale_issue.id,
1029
+ parent_module="issues",
1030
+ )
1031
+ # create a directory for the regscale attachments
1032
+ regscale_dir = os.path.join(directory, "regscale")
1033
+ check_file_path(regscale_dir, False)
1034
+ # download regscale attachments to the directory
1035
+ for attachment in regscale_issue_attachments:
1036
+ with open(os.path.join(regscale_dir, attachment.trustedDisplayName), "wb") as file:
1037
+ file.write(
1038
+ File.download_file_from_regscale_to_memory(
1039
+ api=api,
1040
+ record_id=regscale_issue.id,
1041
+ module="issues",
1042
+ stored_name=attachment.trustedStorageName,
1043
+ file_hash=(attachment.fileHash if attachment.fileHash else attachment.shaHash),
1044
+ )
1045
+ )
1046
+ return jira_dir, regscale_dir