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,980 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Salesforce integration for RegScale CLI to sync Salesforce Cases with RegScale Issues"""
4
+
5
+ import base64
6
+ import mimetypes
7
+ import os
8
+ import tempfile
9
+ from collections import OrderedDict
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+ from typing import Optional, Tuple
13
+
14
+ import click
15
+ import simple_salesforce.exceptions
16
+ from requests.exceptions import HTTPError
17
+ from simple_salesforce import Salesforce
18
+
19
+ from regscale.core.app.api import Api
20
+ from regscale.core.app.application import Application
21
+ from regscale.core.app.utils.app_utils import (
22
+ check_file_path,
23
+ check_license,
24
+ compute_hashes_in_directory,
25
+ convert_datetime_to_regscale_string,
26
+ create_logger,
27
+ create_progress_object,
28
+ error_and_exit,
29
+ get_current_datetime,
30
+ get_file_name,
31
+ get_file_type,
32
+ save_data_to,
33
+ )
34
+ from regscale.utils.threading.threadhandler import create_threads, thread_assignment
35
+ from regscale.models import File, Issue, regscale_id, regscale_module
36
+
37
+ ####################################################################################################
38
+ #
39
+ # simple-salesforce Documentation:
40
+ # https://github.com/simple-salesforce/simple-salesforce
41
+ # Salesforce API Docs:
42
+ # https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm
43
+ # How to get Security Token:
44
+ # https://help.salesforce.com/s/articleView?id=sf.user_security_token.htm&type=5
45
+ #
46
+ ####################################################################################################
47
+
48
+
49
+ job_progress = create_progress_object()
50
+ logger = create_logger()
51
+ updated_regscale_issues = []
52
+ new_regscale_issues = []
53
+ update_counter = []
54
+ new_sf_cases = []
55
+
56
+
57
+ @click.group()
58
+ def salesforce():
59
+ """Sync data between Salesforce Cases & RegScale Issues"""
60
+
61
+
62
+ @salesforce.command()
63
+ @regscale_id()
64
+ @regscale_module()
65
+ @click.option(
66
+ "--sf_status",
67
+ type=click.STRING,
68
+ help="Salesforce status to filter Cases. (CASE SENSITIVE)",
69
+ required=False,
70
+ default=None,
71
+ )
72
+ @click.option(
73
+ "--not_equal",
74
+ is_flag=True,
75
+ help="Exclude the provided status from the filter.",
76
+ )
77
+ def sync(
78
+ regscale_id: int,
79
+ regscale_module: str,
80
+ sync_attachments: bool = True,
81
+ sf_status: Optional[str] = None,
82
+ not_equal: Optional[bool] = False,
83
+ ):
84
+ """Sync Salesforce cases and RegScale issues."""
85
+ sync_sf_and_regscale(
86
+ regscale_id=regscale_id,
87
+ regscale_module=regscale_module,
88
+ sync_attachments=sync_attachments,
89
+ sales_force_status=sf_status,
90
+ not_equal=not_equal,
91
+ )
92
+
93
+
94
+ def create_salesforce_client(config: dict) -> Salesforce:
95
+ """
96
+ Create a Salesforce client with the provided config, if values aren't
97
+ in the provided config, it will use ENV vars
98
+
99
+ :param dict config: config to use to create the Salesforce client
100
+ :return: Salesforce client
101
+ :rtype: Salesforce
102
+ """
103
+ # try to get the needed credentials before creating the Salesforce client
104
+ username = os.getenv("SF_USERNAME") or config.get("salesforceUserName")
105
+ password = os.getenv("SF_PASSWORD") or config.get("salesforcePassword")
106
+ security_token = os.getenv("SF_TOKEN") or config.get("salesforceToken")
107
+ if not username or not password or not security_token:
108
+ error_and_exit(
109
+ "Unable to retrieve Salesforce credentials, please provide them in"
110
+ " the init.yaml or as environment variables."
111
+ )
112
+ # make sure the credentials aren't the ones from Application.template
113
+ if any(credential in Application().template.values() for credential in [username, password, security_token]):
114
+ error_and_exit("Please update the Salesforce credentials in init.yaml or set up environment variables.")
115
+ return Salesforce(
116
+ username=username,
117
+ password=password,
118
+ security_token=security_token,
119
+ )
120
+
121
+
122
+ def sync_sf_and_regscale(
123
+ regscale_id: int,
124
+ regscale_module: str,
125
+ sync_attachments: Optional[bool] = True,
126
+ sales_force_status: Optional[str] = None,
127
+ not_equal: Optional[bool] = False,
128
+ ) -> None:
129
+ """
130
+ Sync Salesforce cases and RegScale issues
131
+
132
+ :param int regscale_id: ID # from RegScale to associate cases with
133
+ :param str regscale_module: RegScale module to associate cases with
134
+ :param Optional[bool] sync_attachments: Sync attachments between Salesforce & RegScale, defaults to True
135
+ :param Optional[str] sales_force_status: Status to filter Salesforce cases by, defaults to None
136
+ :param Optional[bool] not_equal: Exclude the provided status, defaults to False
137
+ :rtype: None
138
+ """
139
+ if not_equal and not sales_force_status:
140
+ error_and_exit("Cannot use '--not-equal' without providing '--sf-status'.")
141
+
142
+ app = check_license()
143
+ api = Api()
144
+ api.timeout = 60
145
+
146
+ sf_client = create_salesforce_client(app.config)
147
+
148
+ (
149
+ regscale_issues,
150
+ regscale_attachments,
151
+ ) = Issue.fetch_issues_and_attachments_by_parent(
152
+ parent_id=regscale_id,
153
+ parent_module=regscale_module,
154
+ fetch_attachments=sync_attachments,
155
+ )
156
+
157
+ sf_cases = fetch_sf_cases(
158
+ sf_client=sf_client,
159
+ sf_status=sales_force_status,
160
+ not_equal=not_equal,
161
+ )
162
+
163
+ if regscale_issues:
164
+ # sync RegScale issues to Salesforce
165
+ if issues_to_update := sync_regscale_to_sf(
166
+ regscale_issues=regscale_issues,
167
+ sf_client=sf_client,
168
+ sf_status=sales_force_status,
169
+ sync_attachments=sync_attachments,
170
+ attachments=regscale_attachments,
171
+ api=api,
172
+ ):
173
+ with job_progress:
174
+ # create task to update RegScale issues
175
+ updating_issues = job_progress.add_task(
176
+ f"[#f8b737]Updating {len(issues_to_update)} RegScale issue(s)" "from Salesforce...",
177
+ total=len(issues_to_update),
178
+ )
179
+ # create threads to analyze Salesforce cases and RegScale issues
180
+ create_threads(
181
+ process=update_regscale_issues,
182
+ args=(
183
+ issues_to_update,
184
+ api,
185
+ updating_issues,
186
+ ),
187
+ thread_count=len(issues_to_update),
188
+ )
189
+ # output the final result
190
+ logger.info(
191
+ "%i/%i issue(s) updated in RegScale.",
192
+ len(issues_to_update),
193
+ len(update_counter),
194
+ )
195
+ else:
196
+ logger.info("No issues need to be updated in RegScale.")
197
+
198
+ if sf_cases:
199
+ # sync Salesforce cases to RegScale
200
+ with job_progress:
201
+ # create task to create RegScale issues
202
+ creating_issues = job_progress.add_task(
203
+ (
204
+ f"[#f8b737]Analyzing {len(sf_cases)} Salesforce case(s)"
205
+ f" and {len(regscale_issues)} RegScale issue(s)..."
206
+ ),
207
+ total=len(sf_cases),
208
+ )
209
+ # create threads to analyze Salesforce cases and RegScale issues
210
+ create_threads(
211
+ process=create_and_update_regscale_issues,
212
+ args=(
213
+ sf_cases,
214
+ sf_client,
215
+ regscale_issues,
216
+ sync_attachments,
217
+ app,
218
+ regscale_id,
219
+ regscale_module,
220
+ creating_issues,
221
+ ),
222
+ thread_count=len(sf_cases),
223
+ )
224
+ # output the final result
225
+ logger.info(
226
+ "Created %i Salesforce case(s), created %i issue(s) and updated %i issue(s) in RegScale.",
227
+ len(new_sf_cases),
228
+ len(new_regscale_issues),
229
+ len(updated_regscale_issues),
230
+ )
231
+ else:
232
+ logger.info("No cases need to be analyzed from Salesforce.")
233
+
234
+
235
+ def fetch_sf_cases(
236
+ sf_client: Salesforce,
237
+ sf_status: Optional[str] = None,
238
+ not_equal: Optional[bool] = False,
239
+ ) -> list:
240
+ """
241
+ Fetch all cases from Salesforce by generating a SOQL with the optional params
242
+
243
+ :param Salesforce sf_client: Salesforce client to use for the request
244
+ :param Optional[str] sf_status: Status to filter Salesforce cases by, defaults to None
245
+ :param Optional[bool] not_equal: Exclude the provided status, defaults to False
246
+ :return: List of Salesforce cases
247
+ :rtype: list
248
+ """
249
+ sf_cases = []
250
+ case_fields = sf_client.Case.describe()["fields"]
251
+ operator = "!=" if not_equal else "="
252
+ status = f"WHERE Status {operator} '{sf_status}'" if sf_status else ""
253
+ cases_query = f"""
254
+ SELECT {', '.join([field['name'] for field in case_fields])}
255
+ FROM Case
256
+ {status}
257
+ """
258
+ cases = sf_client.query_all(cases_query)
259
+ sf_cases.extend(cases["records"])
260
+ while len(sf_cases) < cases["totalSize"]:
261
+ # extend the list of cases with the next page of results
262
+ sf_cases.extend(sf_client.query_more(cases["nextRecordsUrl"], identifier_is_url=True)["records"])
263
+ if sf_cases:
264
+ # write issue data to a json file
265
+ check_file_path("artifacts")
266
+ save_data_to(
267
+ file=Path("./artifacts/existingSalesforceCases.json"),
268
+ data=sf_cases,
269
+ output_log=False,
270
+ )
271
+ logger.info(
272
+ "Saved %i Salesforce case(s) with the provided criteria, see /artifacts/existingSalesforceCases.json",
273
+ len(sf_cases),
274
+ )
275
+ return sf_cases
276
+
277
+
278
+ def fetch_sf_attachments(
279
+ sf_client: Salesforce,
280
+ case: OrderedDict,
281
+ ) -> None:
282
+ """
283
+ Fetch all attachments from Salesforce for the provided case
284
+
285
+ :param Salesforce sf_client: Salesforce client to use for the request
286
+ :param OrderedDict case: Case to fetch attachments for
287
+ :rtype: None
288
+ """
289
+ attachments = None
290
+ attempt_one = sf_client.query_all(
291
+ f"""
292
+ SELECT
293
+ ContentDocumentId,
294
+ ContentDocument.Title,
295
+ ContentDocument.FileType,
296
+ ContentDocument.FileExtension
297
+ FROM ContentDocumentLink
298
+ WHERE LinkedEntityId = '{case.get("Id") or case.get("id")}'
299
+ """
300
+ )
301
+ attempt_two = sf_client.query_all(
302
+ f"""
303
+ SELECT Id, Name, ContentType
304
+ FROM Attachment
305
+ WHERE ParentId = '{case.get("Id") or case.get("id")}'
306
+ """
307
+ )
308
+ if attempt_one["totalSize"] > 0 and attempt_two["totalSize"] > 0:
309
+ attachments = {
310
+ key: attempt_one.get(key, []) + attempt_two.get(key, []) for key in set(attempt_one).union(set(attempt_two))
311
+ }
312
+ elif attempt_one["totalSize"] > 0:
313
+ attachments = attempt_one
314
+ elif attempt_two["totalSize"] > 0:
315
+ attachments = attempt_two
316
+ case["Attachments"] = attachments["records"] if attachments else []
317
+
318
+
319
+ def map_case_to_regscale_issue(case: dict, config: dict, parent_id: int, parent_module: str) -> Issue:
320
+ """
321
+ Map Salesforce case to a RegScale issue
322
+
323
+ :param dict case: Salesforce case
324
+ :param dict config: Application config
325
+ :param int parent_id: Parent record ID in RegScale
326
+ :param str parent_module: Parent record module in RegScale
327
+ :return: Issue object of the newly created issue in RegScale
328
+ :rtype: Issue
329
+ """
330
+ import pandas as pd # Optimize import performance
331
+
332
+ due_date = map_case_due_date(case.get("Priority"), config)
333
+ issue = Issue(
334
+ title=case.get("Subject"),
335
+ severityLevel=Issue.assign_severity(case.get("Priority")),
336
+ issueOwnerId=config["userId"],
337
+ dueDate=due_date,
338
+ # pd.DataFrame.T transposes the columns and headers
339
+ description=f"{pd.DataFrame([case]).T.to_html(justify='left', border=1)}",
340
+ status=("Closed" if case["IsClosed"] else config["issues"]["salesforce"]["status"]),
341
+ salesforceId=case["CaseNumber"],
342
+ parentId=parent_id,
343
+ parentModule=parent_module,
344
+ dateCreated=get_current_datetime(),
345
+ dateCompleted=get_current_datetime() if case["IsClosed"] else None,
346
+ )
347
+ return issue
348
+
349
+
350
+ def map_case_due_date(priority: Optional[str], config: dict) -> str:
351
+ """
352
+ Use the provided priority to determine the due date of the issue in RegScale
353
+
354
+ :param Optional[str] priority: The priority of the case in Salesforce
355
+ :param dict config: Application config
356
+ :return: due date as a string
357
+ :rtype: str
358
+ """
359
+ if not priority or priority.lower() not in config["issues"]["salesforce"].values():
360
+ due_date = datetime.now() + timedelta(days=config["issues"]["salesforce"]["low"])
361
+ else:
362
+ due_date = datetime.now() + timedelta(days=config["issues"]["salesforce"][priority.lower()])
363
+ return convert_datetime_to_regscale_string(due_date)
364
+
365
+
366
+ def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
367
+ """
368
+ Function to create or update issues in RegScale from Salesforce
369
+
370
+ :param Tuple args: Tuple of args to use during the process
371
+ :param int thread: Thread number of current thread
372
+ :rtype: None
373
+ """
374
+ # set up local variables from the passed args
375
+ (
376
+ sf_cases,
377
+ sf_client,
378
+ regscale_issues,
379
+ add_attachments,
380
+ app,
381
+ parent_id,
382
+ parent_module,
383
+ task,
384
+ ) = args
385
+ # find which records should be executed by the current thread
386
+ threads = thread_assignment(thread=thread, total_items=len(sf_cases))
387
+
388
+ # iterate through the thread assignment items and process them
389
+ for i in range(len(threads)):
390
+ case: OrderedDict = sf_cases[threads[i]]
391
+ regscale_issue: Optional[Issue] = next(
392
+ (issue for issue in regscale_issues if issue.salesforceId == case.get("CaseNumber")),
393
+ None,
394
+ )
395
+ # see if the Salesforce case needs to be created as closed in RegScale
396
+ if case["IsClosed"] and regscale_issue:
397
+ # update the status and date completed of the RegScale issue
398
+ regscale_issue.status = "Closed"
399
+ regscale_issue.dateCompleted = get_current_datetime()
400
+ # update the issue in RegScale
401
+ updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
402
+ elif regscale_issue:
403
+ # update the issue in RegScale
404
+ updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
405
+ else:
406
+ # map the Salesforce case to a RegScale issue object
407
+ issue = map_case_to_regscale_issue(
408
+ case=case,
409
+ config=app.config,
410
+ parent_id=parent_id,
411
+ parent_module=parent_module,
412
+ )
413
+ # create the issue in RegScale
414
+ if regscale_issue := Issue.insert_issue(
415
+ app=app,
416
+ issue=issue,
417
+ ):
418
+ logger.debug(
419
+ "Created issue #%i-%s in RegScale.",
420
+ regscale_issue.id,
421
+ regscale_issue.title,
422
+ )
423
+ new_regscale_issues.append(regscale_issue)
424
+ else:
425
+ logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
426
+ if add_attachments and regscale_issue:
427
+ # determine which attachments need to be uploaded to prevent duplicates by
428
+ # getting the hashes of all Salesforce & RegScale attachments
429
+ compare_files_for_dupes_and_upload(
430
+ case=case,
431
+ regscale_issue=regscale_issue,
432
+ sf_client=sf_client,
433
+ api=Api(),
434
+ )
435
+ # update progress bar
436
+ job_progress.update(task, advance=1)
437
+
438
+
439
+ def sync_regscale_to_sf(
440
+ regscale_issues: list[Issue],
441
+ sf_client: Salesforce,
442
+ api: Api,
443
+ sync_attachments: bool = True,
444
+ sf_status: Optional[str] = None,
445
+ attachments: Optional[list[File]] = None,
446
+ ) -> list[Issue]:
447
+ """
448
+ Sync issues from RegScale to Salesforce cases
449
+ :param list[Issue] regscale_issues: list of RegScale issues to sync to Salesforce
450
+ :param Salesforce sf_client: Salesforce client to use for case creation in Salesforce
451
+ :param Api api: RegScale API client to move files, if sync_attachments is True
452
+ :param bool sync_attachments: Sync attachments from RegScale to Sales, defaults to True
453
+ :param Optional[str] sf_status: Status to set the Salesforce case to, defaults to None
454
+ :param Optional[list[File]] attachments: List of files to sync from RegScale to Salesforce, defaults to None
455
+ :return: list of RegScale issues that need to be updated
456
+ :rtype: list[Issue]
457
+ """
458
+ issues_to_update = []
459
+ for issue in regscale_issues:
460
+ new_case_details = None
461
+ # see if Salesforce case already exists
462
+ if not issue.salesforceId:
463
+ new_case = create_case_in_sf(
464
+ issue=issue,
465
+ sf_client=sf_client,
466
+ sf_status=sf_status,
467
+ add_attachments=sync_attachments,
468
+ attachments=attachments,
469
+ )
470
+ # get the case number, creating a case only returns the ID
471
+ new_case_details = sf_client.Case.get(new_case["id"])
472
+ # update the RegScale issue for the Salesforce case number
473
+ issue.salesforceId = new_case_details.get("CaseNumber")
474
+ # add the issue to the update_issues global list
475
+ issues_to_update.append(issue)
476
+ elif issue.salesforceId:
477
+ try:
478
+ new_case_details = sf_client.Case.get_by_custom_id("CaseNumber", issue.salesforceId)
479
+ except simple_salesforce.exceptions.SalesforceResourceNotFound:
480
+ logger.warning("Unable to get details for Salesforce case #%s.", issue.salesforceId)
481
+ continue
482
+ if sync_attachments and attachments and (new_case_details and issue.salesforceId):
483
+ compare_files_for_dupes_and_upload(
484
+ case=new_case_details,
485
+ regscale_issue=issue,
486
+ sf_client=sf_client,
487
+ api=api,
488
+ )
489
+ return issues_to_update
490
+
491
+
492
+ def create_case_in_sf(
493
+ issue: Issue,
494
+ sf_client: Salesforce,
495
+ sf_status: Optional[str] = None,
496
+ add_attachments: Optional[bool] = True,
497
+ attachments: Optional[list[Issue]] = None,
498
+ api: Optional[Api] = None,
499
+ ) -> dict:
500
+ """
501
+ Create a new case in Salesforce
502
+
503
+ :param Issue issue: RegScale issue object
504
+ :param Salesforce sf_client: Salesforce client to use for case creation in Salesforce
505
+ :param Optional[str] sf_status: Status to set the Salesforce case to, defaults to None
506
+ :param Optional[bool] add_attachments: Whether to add attachments to new case, defaults to true
507
+ :param Optional[list[Issue]] attachments: Dictionary containing attachments, defaults to None
508
+ :param Optional[Api] api: API object to download attachments from RegScale, defaults to None
509
+ :return: Newly created case in Salesforce
510
+ :rtype: dict
511
+ """
512
+ new_case = sf_client.Case.create(
513
+ {
514
+ "Subject": f"RegScale Issue {issue.id}: {issue.title}",
515
+ "Description": f"RegScale Issue #{issue.id} description: {issue.description}",
516
+ "Priority": map_regscale_severity_to_sf(issue.severityLevel),
517
+ "Status": sf_status if sf_status else "New",
518
+ }
519
+ )
520
+ if new_case["errors"]:
521
+ logger.error(
522
+ "Unable to create case in Salesforce for RegScale issue #%i.\n%s",
523
+ issue.id,
524
+ new_case["errors"],
525
+ )
526
+ new_sf_cases.append(new_case)
527
+ # add the attachments to the new case in Salesforce
528
+ if add_attachments and attachments:
529
+ if not api:
530
+ api = Api()
531
+ api.timeout = 60
532
+ compare_files_for_dupes_and_upload(
533
+ case=sf_client.Case.get(new_case["id"]),
534
+ regscale_issue=issue,
535
+ sf_client=sf_client,
536
+ api=api,
537
+ )
538
+ return new_case
539
+
540
+
541
+ def map_regscale_severity_to_sf(severity: str) -> str:
542
+ """
543
+ Map RegScale severity to Salesforce priority
544
+ :param str severity: RegScale severity
545
+ :return: Salesforce priority
546
+ :rtype: str
547
+ """
548
+ priority = "Low"
549
+ if severity.startswith("I -"):
550
+ priority = "High"
551
+ elif severity.startswith("II -"):
552
+ priority = "Medium"
553
+ return priority
554
+
555
+
556
+ def compare_files_for_dupes_and_upload(
557
+ case: OrderedDict, regscale_issue: Issue, sf_client: Salesforce, api: Api
558
+ ) -> None:
559
+ """
560
+ Compare files for provided Salesforce Case and RegScale issue via hash to prevent duplicates
561
+ and then upload the attachments to the Salesforce case and/or RegScale issue
562
+ :param OrderedDict case: Salesforce case to compare attachments from
563
+ :param Issue regscale_issue: RegScale issue object to compare attachments from
564
+ :param Salesforce sf_client: Salesforce client to fetch attachments for the provided case
565
+ :param Api api:API client to move files, if sync_attachments is True
566
+ :rtype: None
567
+ """
568
+ sf_uploaded_attachments = []
569
+ regscale_uploaded_attachments = []
570
+ # create a temporary directory to store the downloaded attachments from Salesforce and RegScale
571
+ with tempfile.TemporaryDirectory() as temp_dir:
572
+ # write attachments to the temporary directory
573
+ sf_dir, regscale_dir = download_attachments_to_directory(
574
+ directory=temp_dir,
575
+ case=case,
576
+ regscale_issue=regscale_issue,
577
+ api=api,
578
+ sf_client=sf_client,
579
+ )
580
+ # get the hashes for the attachments in the RegScale and Salesforce directories
581
+ # iterate all files in the Salesforce directory and compute their hashes
582
+ sf_attachment_hashes = compute_hashes_in_directory(sf_dir)
583
+ regscale_attachment_hashes = compute_hashes_in_directory(regscale_dir)
584
+
585
+ sf_uploaded_attachments = process_attachments_and_upload_to_sf(
586
+ regscale_attachment_hashes=regscale_attachment_hashes,
587
+ sf_attachment_hashes=sf_attachment_hashes,
588
+ case=case,
589
+ regscale_issue=regscale_issue,
590
+ sf_client=sf_client,
591
+ api=api,
592
+ )
593
+
594
+ regscale_uploaded_attachments = process_attachments_and_upload_to_regscale(
595
+ regscale_attachment_hashes=regscale_attachment_hashes,
596
+ sf_attachment_hashes=sf_attachment_hashes,
597
+ regscale_issue=regscale_issue,
598
+ api=api,
599
+ )
600
+ output_console_summary(
601
+ regscale_issue=regscale_issue,
602
+ sf_case=case,
603
+ regscale_uploads=regscale_uploaded_attachments,
604
+ sf_uploads=sf_uploaded_attachments,
605
+ )
606
+
607
+
608
+ def process_attachments_and_upload_to_sf(
609
+ regscale_attachment_hashes: dict,
610
+ sf_attachment_hashes: dict,
611
+ case: OrderedDict,
612
+ regscale_issue: Issue,
613
+ sf_client: Salesforce,
614
+ api: Api,
615
+ ) -> int:
616
+ """
617
+ Process attachment hashes for RegScale and Salesforce and upload missing
618
+ files to Salesforce case
619
+
620
+ :param dict regscale_attachment_hashes: Dictionary containing RegScale attachment & hashes
621
+ :param dict sf_attachment_hashes: Dictionary containing Salesforce attachment & hashes
622
+ :param OrderedDict case: Salesforce case to compare attachments from
623
+ :param Issue regscale_issue: RegScale issue object to compare attachments from
624
+ :param Salesforce sf_client: Salesforce client to fetch attachments for the provided case
625
+ :param Api api: API client to move files
626
+ :return: # of attachments uploaded to Salesforce case
627
+ :rtype: int
628
+ """
629
+ sf_uploaded_attachments = 0
630
+ # check where the files need to be uploaded to before uploading
631
+ for file_hash, file in regscale_attachment_hashes.items():
632
+ if file_hash not in sf_attachment_hashes:
633
+ if upload_file_to_sf(
634
+ # If it was a newly created case, it has id, if it already existed it
635
+ # we will use Id, with the or statement it will use whatever
636
+ # key is in the case dictionary
637
+ case_id=case.get("Id") or case.get("id"),
638
+ file_path=file,
639
+ file_name=f"RegScale_Issue_{regscale_issue.id}_{Path(file).name}",
640
+ sf_url=sf_client.base_url,
641
+ sf_session_id=sf_client.session_id,
642
+ api=api,
643
+ ):
644
+ sf_uploaded_attachments += 1
645
+ logger.debug(
646
+ "Uploaded %s to Salesforce case #%s.",
647
+ Path(file).name,
648
+ case.get("Id") or case.get("id"),
649
+ )
650
+ else:
651
+ logger.warning(
652
+ "Unable to upload %s to Salesforce case #%s.",
653
+ Path(file).name,
654
+ case.get("Id") or case.get("id"),
655
+ )
656
+ return sf_uploaded_attachments
657
+
658
+
659
+ def process_attachments_and_upload_to_regscale(
660
+ regscale_attachment_hashes: dict,
661
+ sf_attachment_hashes: dict,
662
+ regscale_issue: Issue,
663
+ api: Api,
664
+ ) -> int:
665
+ """
666
+ Process attachment hashes for RegScale and Salesforce and upload missing files to RegScale issue
667
+
668
+ :param dict regscale_attachment_hashes: Dictionary containing RegScale attachment & hashes
669
+ :param dict sf_attachment_hashes: Dictionary containing Salesforce attachment & hashes
670
+ :param Issue regscale_issue: RegScale issue to upload attachments to
671
+ :param Api api: API client to upload files to RegScale
672
+ :return: # of attachments uploaded to RegScale issue
673
+ :rtype: int
674
+ """
675
+ regscale_uploaded_attachments = 0
676
+ for file_hash, file in sf_attachment_hashes.items():
677
+ if file_hash not in regscale_attachment_hashes:
678
+ with open(file, "rb") as in_file:
679
+ if File.upload_file_to_regscale(
680
+ file_name=f"Salesforce_attachment_{Path(file).name}",
681
+ parent_id=regscale_issue.id,
682
+ parent_module="issues",
683
+ api=api,
684
+ file_data=in_file.read(),
685
+ ):
686
+ regscale_uploaded_attachments += 1
687
+ logger.debug(
688
+ "Uploaded %s to RegScale issue #%i.",
689
+ Path(file).name,
690
+ regscale_issue.id,
691
+ )
692
+ else:
693
+ logger.warning(
694
+ "Unable to upload %s to RegScale issue #%i.",
695
+ Path(file).name,
696
+ regscale_issue.id,
697
+ )
698
+ return regscale_uploaded_attachments
699
+
700
+
701
+ def output_console_summary(
702
+ regscale_issue: Issue,
703
+ sf_case: dict,
704
+ regscale_uploads: Optional[int] = None,
705
+ sf_uploads: Optional[int] = None,
706
+ ) -> None:
707
+ """
708
+ Output a summary of the files uploaded to Salesforce and RegScale to the console
709
+
710
+ :param Issue regscale_issue: RegScale issue object
711
+ :param dict sf_case: Salesforce case dictionary
712
+ :param Optional[int] regscale_uploads: # of files uploaded to RegScale issue
713
+ :param Optional[int] sf_uploads: # of files uploaded to Salesforce case
714
+ :rtype: None
715
+ """
716
+ if regscale_uploads and sf_uploads:
717
+ logger.info(
718
+ "%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to Salesforce Case #%s.",
719
+ regscale_uploads,
720
+ regscale_issue.id,
721
+ sf_uploads,
722
+ sf_case["CaseNumber"],
723
+ )
724
+ elif sf_uploads:
725
+ logger.info(
726
+ "%i file(s) uploaded to Salesforce case #%s.",
727
+ sf_uploads,
728
+ sf_case["CaseNumber"],
729
+ )
730
+ elif regscale_uploads:
731
+ logger.info(
732
+ "%i file(s) uploaded to RegScale issue #%i.",
733
+ regscale_uploads,
734
+ regscale_issue.id,
735
+ )
736
+
737
+
738
+ def download_attachments_to_directory(
739
+ directory: str,
740
+ case: OrderedDict,
741
+ regscale_issue: Issue,
742
+ api: Api,
743
+ sf_client: Salesforce,
744
+ ) -> tuple[str, str]:
745
+ """
746
+ Function to download attachments from Salesforce and RegScale to a directory
747
+
748
+ :param str directory: Directory to store the files in
749
+ :param OrderedDict case: Salesforce case to download the attachments for
750
+ :param Issue regscale_issue: RegScale issue to download the attachments for
751
+ :param Api api: Api object to use for interacting with RegScale
752
+ :param Salesforce sf_client: Salesforce client to use for interacting with Salesforce
753
+ :return: Tuple of strings containing the Salesforce and RegScale directories
754
+ :rtype: tuple[str, str]
755
+ """
756
+ # determine which attachments need to be uploaded to prevent duplicates by checking hashes
757
+ sf_dir = os.path.join(directory, "salesforce")
758
+ check_file_path(sf_dir, False)
759
+ fetch_sf_attachments(
760
+ sf_client=sf_client,
761
+ case=case,
762
+ )
763
+ if case.get("Attachments"):
764
+ # download all attachments from Salesforce case to the Salesforce directory in temp_dir
765
+ for attachment in case["Attachments"]:
766
+ download_attachment_from_sf(
767
+ sf_url=sf_client.base_url,
768
+ sf_session_id=sf_client.session_id,
769
+ attachment=attachment,
770
+ api=api,
771
+ directory=sf_dir,
772
+ )
773
+
774
+ # get the regscale issue attachments
775
+ regscale_issue_attachments = File.get_files_for_parent_from_regscale(
776
+ api=api,
777
+ parent_id=regscale_issue.id,
778
+ parent_module="issues",
779
+ )
780
+ # create a directory for the regscale attachments
781
+ regscale_dir = os.path.join(directory, "regscale")
782
+ check_file_path(regscale_dir, False)
783
+ # download regscale attachments to the directory
784
+ for attachment in regscale_issue_attachments:
785
+ with open(os.path.join(regscale_dir, attachment.trustedDisplayName), "wb") as file:
786
+ file.write(
787
+ File.download_file_from_regscale_to_memory(
788
+ api=api,
789
+ record_id=regscale_issue.id,
790
+ module="issues",
791
+ stored_name=attachment.trustedStorageName,
792
+ file_hash=(attachment.shaHash if attachment.shaHash else attachment.fileHash),
793
+ )
794
+ )
795
+ return sf_dir, regscale_dir
796
+
797
+
798
+ def download_attachment_from_sf(
799
+ sf_url: str,
800
+ sf_session_id: str,
801
+ attachment: OrderedDict,
802
+ api: Api,
803
+ directory: str,
804
+ ) -> None:
805
+ """
806
+ Function to download an attachment from Salesforce and save it to the provided directory
807
+
808
+ :param str sf_url: Salesforce base url to use for downloading the file
809
+ :param str sf_session_id: Salesforce session id to use for authenticating the API call
810
+ :param OrderedDict attachment: Attachment to download from Salesforce
811
+ :param Api api: Api object to use for downloading the file
812
+ :param str directory: Directory to save the file to
813
+ :rtype: None
814
+ """
815
+ attachment_id = None
816
+ endpoint = None
817
+ headers = {
818
+ "Authorization": f"Bearer {sf_session_id}",
819
+ }
820
+ if attachment.get("Id"):
821
+ attachment_id = attachment["Id"]
822
+ endpoint = "Attachment"
823
+ elif attachment.get("ContentDocumentId"):
824
+ attachment_id = attachment["ContentDocumentId"]
825
+ endpoint = "Document"
826
+ if not attachment_id or not endpoint:
827
+ logger.warning(
828
+ "Unable to download attachment from Salesforce case. No attachment ID found.\n%s",
829
+ attachment,
830
+ )
831
+ return
832
+ # Prepare Salesforce REST API endpoint for standard URL
833
+ url_standard = f"{sf_url}/sobjects/{endpoint}/{attachment_id}/body"
834
+
835
+ response = None
836
+ # Try to download from standard URL
837
+ try:
838
+ response = api.get(url_standard, headers=headers)
839
+ response.raise_for_status()
840
+ except HTTPError:
841
+ # Download from standard URL fails, try Lightning URL
842
+ # Prepare Salesforce REST API endpoint for Lightning URL
843
+ url_lightning = (
844
+ f'https://{sf_url.split("//")[1].split(".")[0]}.lightning.force.com'
845
+ f"/services/data/v58.0/sobjects/{endpoint}/{attachment_id}/body"
846
+ )
847
+ try:
848
+ response = api.get(url_lightning, headers=headers)
849
+ response.raise_for_status()
850
+ except HTTPError:
851
+ file_url = (
852
+ f'https://{sf_url.split("//")[1].split(".")[0]}.file.force.com'
853
+ f"/sfc/servlet.shepherd/document/download/{attachment_id}"
854
+ )
855
+ try:
856
+ response = api.get(file_url, headers=headers)
857
+ response.raise_for_status()
858
+ except HTTPError:
859
+ logger.error(
860
+ "Failed to download '%s.%s' from both standard and Lightning URLs in Salesforce.",
861
+ attachment["Title"],
862
+ attachment["FileType"].lower(),
863
+ )
864
+ # If a response was received, write the file data to a file
865
+ if response.ok:
866
+ try:
867
+ file_name = attachment["Name"]
868
+ except KeyError:
869
+ file_name = f'{attachment["ContentDocument"]["Title"]}.{attachment["ContentDocument"]["FileExtension"]}'
870
+ if not file_name:
871
+ logger.error(
872
+ "Unable to determine file name for attachment from Salesforce attachment.\n%s",
873
+ attachment,
874
+ )
875
+ return
876
+ with open(
877
+ os.path.join(
878
+ directory,
879
+ file_name.replace(" ", "_"),
880
+ ),
881
+ "wb",
882
+ ) as out_file:
883
+ out_file.write(response.content)
884
+
885
+
886
+ def upload_file_to_sf(
887
+ case_id: str,
888
+ file_path: str,
889
+ file_name: str,
890
+ sf_url: str,
891
+ sf_session_id: str,
892
+ api: Api,
893
+ ) -> bool:
894
+ """
895
+ Function to upload a file to Salesforce via API
896
+
897
+ :param str case_id: Salesforce Case ID to upload the file to
898
+ :param str file_path: File path to the file to upload
899
+ :param str file_name: Desired name of the file once uploaded
900
+ :param str sf_url: Salesforce URL to use for uploading the file
901
+ :param str sf_session_id: Salesforce session ID to use for authenticating the API call
902
+ :param Api api: Api object to use for uploading the file
903
+ :return: If upload was successful
904
+ :rtype: bool
905
+ """
906
+ response = None
907
+ with open(file_path, "rb") as in_file:
908
+ data = in_file.read()
909
+
910
+ # Encode file data in base64
911
+ data = base64.b64encode(data).decode()
912
+
913
+ # Prepare Salesforce REST API headers
914
+ headers = {
915
+ "Authorization": "Bearer " + sf_session_id,
916
+ "Content-Type": "application/json",
917
+ }
918
+
919
+ # Prepare Salesforce REST API endpoint
920
+ url_standard = f"{sf_url}/sobjects/Attachment"
921
+
922
+ # Prepare Salesforce REST API request body
923
+ body = {
924
+ "Name": file_name,
925
+ "ParentId": case_id,
926
+ "Body": data,
927
+ "ContentType": mimetypes.types_map[get_file_type(file_path)],
928
+ }
929
+
930
+ # Make Salesforce REST API request
931
+ try:
932
+ response = api.post(url_standard, headers=headers, json=body)
933
+ response.raise_for_status()
934
+ except HTTPError:
935
+ # If upload with standard URL fails, try Lightning URL
936
+ # Prepare Salesforce REST API endpoint for Lightning URL
937
+ url_lightning = (
938
+ f'https://{sf_url.split("//")[1].split(".")[0]}.lightning.force.com'
939
+ f"/services/data/v52.0/sobjects/Attachment"
940
+ )
941
+ try:
942
+ response = api.post(url_lightning, headers=headers, json=body)
943
+ response.raise_for_status()
944
+ except HTTPError:
945
+ logger.error(
946
+ "Failed to upload '%s' using both standard and Lightning URLs in Salesforce.",
947
+ get_file_name(file_path),
948
+ )
949
+ return response.ok or False
950
+
951
+
952
+ def update_regscale_issues(args: Tuple, thread: int) -> None:
953
+ """
954
+ Function to compare Salesforce cases and RegScale issues with threads
955
+
956
+ :param Tuple args: Tuple of args to use during the process
957
+ :param int thread: Thread number of current thread
958
+ :rtype: None
959
+ """
960
+ # set up local variables from the passed args
961
+ (
962
+ regscale_issues,
963
+ app,
964
+ task,
965
+ ) = args
966
+ # find which records should be executed by the current thread
967
+ threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
968
+ # iterate through the thread assignment items and process them
969
+ for i in range(len(threads)):
970
+ # set the issue for the thread for later use in the function
971
+ issue = regscale_issues[threads[i]]
972
+ # update the issue in RegScale
973
+ Issue.update_issue(app=app, issue=issue)
974
+ update_counter.append(Issue)
975
+ logger.info(
976
+ "RegScale Issue %i was updated with the Salesforce case number.",
977
+ issue.id,
978
+ )
979
+ # update progress bar
980
+ job_progress.update(task, advance=1)