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,620 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """RegScale Email Reminders Class used in admin_actions.py"""
4
+
5
+ # standard python imports
6
+ import re
7
+ import sys
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from datetime import datetime, timedelta
10
+ from typing import Tuple, Optional
11
+ from urllib.parse import urljoin
12
+
13
+ from requests import JSONDecodeError
14
+ from rich.console import Console
15
+
16
+ from regscale.core.app.api import Api
17
+ from regscale.core.app.application import Application
18
+ from regscale.core.app.utils.app_utils import (
19
+ create_progress_object,
20
+ error_and_exit,
21
+ flatten_dict,
22
+ get_css,
23
+ reformat_str_date,
24
+ uncamel_case,
25
+ )
26
+ from regscale.models import Email, User
27
+ from regscale.models.app_models.pipeline import Pipeline
28
+ from regscale.utils.threading.threadhandler import create_threads, thread_assignment
29
+
30
+
31
+ class SendReminders:
32
+ """
33
+ Class to send reminders to users with upcoming and/or outstanding Tasks, Assessments,
34
+ Data Calls, Issues, Security Plans, and Workflows
35
+
36
+ :param Application app: CLI Application
37
+ :param int days: # of days to look for upcoming and/or outstanding items, default is 30 days
38
+ """
39
+
40
+ def __init__(self, app: Application, days: int = 30):
41
+ self.app = app
42
+ self.logger = self.app.logger
43
+ self.api = Api()
44
+ self.config = self.app.config
45
+ self.days = days
46
+ self.base_url = urljoin(self.config["domain"], "/api/")
47
+ self.users = []
48
+ self.activated_users = []
49
+ self.email_data = None
50
+ self.tenant_pipeline = []
51
+ self.final_pipeline = []
52
+ self.emails = []
53
+ self.job_progress = create_progress_object()
54
+
55
+ def get_and_send_reminders(self) -> None:
56
+ """
57
+ Function to get and send reminders for users in RegScale that have email notifications
58
+ enabled and have upcoming or outstanding Tasks, Assessments, Data Calls, Issues, Security Plans,
59
+ and Workflows
60
+
61
+ :rtype: None
62
+ """
63
+ import pandas as pd # Optimize import performance
64
+
65
+ # make sure config is set before processing
66
+ if self.config["domain"] == "":
67
+ error_and_exit("The domain is blank in the initialization file.")
68
+ if self.config["token"] == "":
69
+ error_and_exit("The token has not been set in the initialization file.")
70
+
71
+ # get the user's tenant id, used to get all active
72
+ # users for that instance of the application
73
+ url = urljoin(self.base_url, f"accounts/find/{self.config['userId']}")
74
+ self.logger.debug("Fetching tenant information from %s.", url)
75
+ try:
76
+ res = self.api.get(url=url)
77
+ self.logger.debug("Response: %i: %s=%s", res.status_code, res.reason, res.text)
78
+ self.logger.debug(res.json())
79
+ res = res.json()
80
+ except JSONDecodeError as ex:
81
+ error_and_exit(f"Unable to retrieve tenant information from RegScale.\n{ex}")
82
+ ten_id = res["tenantId"]
83
+
84
+ # Use the api to get a list of all active users
85
+ # with emailNotifications set to True for
86
+ # the tenant id of the current user
87
+ response = self.api.get(url=urljoin(self.base_url, f"accounts/{ten_id}/True"))
88
+ activated_users_response = self.api.get(url=urljoin(self.base_url, "accounts"))
89
+ # try to convert the response to a json file, exit if it errors
90
+ try:
91
+ self.users = response.json()
92
+ self.activated_users = activated_users_response.json()
93
+ # if error encountered, exit the application
94
+ except JSONDecodeError as ex:
95
+ error_and_exit(f"Unable to retrieve active users from RegScale.\n{ex}")
96
+
97
+ # start a console progress bar and threads for the given task
98
+ # create the threads with the given function, arguments and thread count
99
+ with self.job_progress:
100
+ if len(self.users) == 0:
101
+ self.logger.warning("No users have email notifications enabled!")
102
+ return
103
+ self.logger.info("Fetching pipeline for %s user(s).", len(self.users))
104
+ getting_items = self.job_progress.add_task(
105
+ f"[#f8b737]Fetching pipeline for {len(self.users)} user(s)...",
106
+ total=len(self.users),
107
+ )
108
+ max_workers = self.config.get("maxThreads", 30)
109
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
110
+ futures = [
111
+ executor.submit(
112
+ self.get_upcoming_or_expired_items,
113
+ user,
114
+ )
115
+ for user in self.users
116
+ ]
117
+ for future in as_completed(futures):
118
+ pipeline = future.result()
119
+ self.tenant_pipeline.append(pipeline)
120
+ self.job_progress.update(getting_items, advance=1)
121
+
122
+ if len(self.tenant_pipeline) > 0:
123
+ self.logger.info("Analyzing pipeline for %s user(s).", len(self.tenant_pipeline))
124
+ # start a console progress bar and threads for the given task
125
+ analyze_items = self.job_progress.add_task(
126
+ f"[#ef5d23]Analyzing pipeline for {len(self.tenant_pipeline)} user(s)...",
127
+ total=len(self.tenant_pipeline),
128
+ )
129
+ # convert user list into a dictionary using ID as the key for each user dictionary
130
+ dict_users = {
131
+ self.activated_users[i]["id"]: self.activated_users[i] for i in range(len(self.activated_users))
132
+ }
133
+
134
+ create_threads(
135
+ process=self.analyze_pipeline,
136
+ args=(self.config, analyze_items, dict_users),
137
+ thread_count=len(self.tenant_pipeline),
138
+ )
139
+ self.logger.info("Sending an email to %s user(s).", len(self.final_pipeline))
140
+ # start a console progress bar and threads for the given task
141
+ emailing_users = self.job_progress.add_task(
142
+ f"[#21a5bb]Sending an email to {len(self.final_pipeline)} user(s)...",
143
+ total=len(self.final_pipeline),
144
+ )
145
+ create_threads(
146
+ process=self.format_and_email,
147
+ args=(self.api, self.config, emailing_users),
148
+ thread_count=len(self.final_pipeline),
149
+ )
150
+ else:
151
+ self.logger.info("No outstanding or upcoming items!")
152
+ sys.exit()
153
+
154
+ # create one data table from all pandas data tables in emails
155
+ self.email_data = pd.concat(self.emails)
156
+
157
+ # create console variable and print # of emails sent successfully
158
+ self.logger.info("Successfully sent an email to %s user(s)...", self.email_data.Emailed.sum())
159
+ console = Console()
160
+ console.print(f"[green]Successfully sent an email to {self.email_data.Emailed.sum()} user(s)...")
161
+
162
+ # format email to notify person that called the command of the outcome
163
+ email = Email(
164
+ fromEmail="Support@RegScale.com",
165
+ emailSenderId=self.config["userId"],
166
+ to=res["email"],
167
+ subject=f"RegScale Reminders Sent to {self.email_data.Emailed.sum()} User(s)",
168
+ body=get_css("email_style.css")
169
+ + self.email_data.to_html(justify="left", index=False)
170
+ .replace('border="1"', 'border="0"')
171
+ .replace("&", "&")
172
+ .replace(">", ">")
173
+ .replace("&lt;", "<")
174
+ .replace("’", "'"),
175
+ )
176
+
177
+ # send the email to the user
178
+ email.send()
179
+
180
+ def get_upcoming_or_expired_items(self, user: dict) -> Optional[Pipeline]:
181
+ """
182
+ Function used by threads to send emails to users with upcoming and/or outstanding
183
+ Tasks, Assessments, Data Calls, Issues, Security Plans, and Workflows
184
+
185
+ :param dict user: Dictionary of user to get upcoming and/or outstanding items for
186
+ :return: Pipeline object for the provided user, if they have one or more items
187
+ :rtype: Optional[Pipeline]
188
+ """
189
+ # calculate date with the # of days provided
190
+ # have to explicitly convert the days to an int because of Airflow
191
+ before_date = datetime.now() + timedelta(days=int(self.days))
192
+ after_date = datetime.now() - timedelta(days=int(self.days))
193
+
194
+ # format the date to a string the server will recognize
195
+ before_date = before_date.strftime("%Y-%m-%dT%H:%M:%S")
196
+ after_date = after_date.strftime("%Y-%m-%dT%H:%M:%S")
197
+
198
+ # get all the assessments, issues, tasks, data calls, security plans and workflows
199
+ # for the user we can email, using the # of days entered by the user using graphql,
200
+ # if no days were entered, the default is 30 days
201
+ query = f"""
202
+ query {{
203
+ assessments(
204
+ take: 50
205
+ skip: 0
206
+ order: {{ plannedFinish: DESC }}
207
+ where: {{
208
+ leadAssessorId: {{ eq: "{user["id"]}" }}
209
+ plannedFinish: {{ lte: "{before_date}" }}
210
+ status: {{ nin: ["Complete", "Cancelled"] }}
211
+ }}
212
+ ) {{
213
+ items {{
214
+ uuid
215
+ id
216
+ title
217
+ leadAssessorId
218
+ assessmentType
219
+ plannedFinish
220
+ createdById
221
+ dateCreated
222
+ status
223
+ assessmentResult
224
+ actualFinish
225
+ }}
226
+ totalCount
227
+ pageInfo {{
228
+ hasNextPage
229
+ }}
230
+ }}
231
+ dataCalls(
232
+ take: 50
233
+ skip: 0
234
+ order: {{ dateDue: DESC }}
235
+ where: {{
236
+ createdById: {{ eq: "{user["id"]}" }}
237
+ dateDue: {{ lte: "{before_date}" }}
238
+ status: {{ nin: ["Completed", "Cancelled"] }}
239
+ }}
240
+ ) {{
241
+ items {{
242
+ uuid
243
+ id
244
+ title
245
+ dataCallLeadId
246
+ dateDue
247
+ createdById
248
+ dateCreated
249
+ status
250
+ }}
251
+ totalCount
252
+ pageInfo {{
253
+ hasNextPage
254
+ }}
255
+ }}
256
+ securityPlans(
257
+ take: 50
258
+ skip: 0
259
+ order: {{ expirationDate: DESC }}
260
+ where: {{
261
+ systemOwnerId: {{ eq: "{user["id"]}" }}
262
+ expirationDate: {{ lte: "{before_date}" }}
263
+ }}
264
+ ) {{
265
+ items {{
266
+ uuid
267
+ id
268
+ systemName
269
+ systemOwnerId
270
+ status
271
+ systemType
272
+ expirationDate
273
+ overallCategorization
274
+ createdById
275
+ dateCreated
276
+ }}
277
+ totalCount
278
+ pageInfo {{
279
+ hasNextPage
280
+ }}
281
+ }}
282
+ workflowInstances(
283
+ take: 50
284
+ skip: 0
285
+ order: {{ startDate: DESC }}
286
+ where: {{
287
+ ownerId: {{ eq: "{user["id"]}" }}
288
+ status: {{ neq: "Complete" }}
289
+ startDate: {{ gte: "{after_date}" }}
290
+ endDate: {{ eq: null }}
291
+ }}
292
+ ) {{
293
+ items {{
294
+ id
295
+ name
296
+ status
297
+ startDate
298
+ endDate
299
+ comments
300
+ currentStep
301
+ createdById
302
+ dateCreated
303
+ lastUpdatedById
304
+ ownerId
305
+ atlasModule
306
+ parentId
307
+ }}
308
+ totalCount
309
+ pageInfo {{
310
+ hasNextPage
311
+ }}
312
+ }}
313
+ tasks(
314
+ take: 50
315
+ skip: 0
316
+ order: {{ dueDate: DESC }}
317
+ where: {{
318
+ assignedToId: {{ eq: "{user["id"]}" }}
319
+ dueDate: {{ lte: "{before_date}" }}
320
+ status: {{ nin: ["Closed", "Cancelled"] }}
321
+ }}
322
+ ) {{
323
+ items {{
324
+ uuid
325
+ id
326
+ title
327
+ assignedToId
328
+ dueDate
329
+ createdById
330
+ status
331
+ percentComplete
332
+ }}
333
+ totalCount
334
+ pageInfo {{
335
+ hasNextPage
336
+ }}
337
+ }}
338
+ issues(
339
+ take: 50
340
+ skip: 0
341
+ order: {{ dueDate: DESC }}
342
+ where: {{
343
+ issueOwnerId: {{ eq: "{user["id"]}" }}
344
+ dueDate: {{ lte: "{before_date}" }}
345
+ status: {{ nin: ["Closed", "Cancelled"] }}
346
+ }}
347
+ ) {{
348
+ items {{
349
+ uuid
350
+ id
351
+ title
352
+ issueOwnerId
353
+ severityLevel
354
+ createdById
355
+ dateCreated
356
+ status
357
+ dueDate
358
+ }}
359
+ totalCount
360
+ pageInfo {{
361
+ hasNextPage
362
+ }}
363
+ }}
364
+ }}
365
+ """
366
+ # get the data from GraphQL
367
+ res_data = self.api.graph(query=query)
368
+
369
+ # create list that has dictionaries of the user's pipeline and categories
370
+ pipelines = {
371
+ "Assessments": {"Pipeline": res_data["assessments"]["items"]},
372
+ "Issues": {"Pipeline": res_data["issues"]["items"]},
373
+ "Tasks": {"Pipeline": res_data["tasks"]["items"]},
374
+ "Data Calls": {"Pipeline": res_data["dataCalls"]["items"]},
375
+ "Security Plans": {"Pipeline": res_data["securityPlans"]["items"]},
376
+ "Workflow": {"Pipeline": res_data["workflowInstances"]["items"]},
377
+ }
378
+ # iterate through the user's pipeline tallying their items and check the amount
379
+ total_tasks = sum(len(pipeline["Pipeline"]) for pipeline in pipelines.values())
380
+ if total_tasks > 0:
381
+ # map and add the data to a self variable
382
+ return Pipeline(
383
+ email=user["email"],
384
+ fullName=f'{user["firstName"]} {user["lastName"]}',
385
+ pipelines=pipelines,
386
+ totalTasks=total_tasks,
387
+ )
388
+ return None
389
+
390
+ # flake8: noqa: C901
391
+ def analyze_pipeline(self, args: Tuple, thread: int) -> None:
392
+ """
393
+ Function to set up data tables from the user's pipeline while using threading
394
+
395
+ :param Tuple args: Tuple of args to use during the process
396
+ :param int thread: Thread number of current thread
397
+ :rtype: None
398
+ """
399
+ import pandas as pd # Optimize import performance
400
+
401
+ config, task, users = args
402
+
403
+ id_fields = ["leadassessorid", "assignedtoid", "datacallleadid"]
404
+
405
+ # get the assigned threads
406
+ threads = thread_assignment(
407
+ thread=thread,
408
+ total_items=len(self.tenant_pipeline),
409
+ )
410
+ for i in range(len(threads)):
411
+ # get the pipeline from the self.tenant_pipeline
412
+ pipelines = self.tenant_pipeline[threads[i]].pipelines
413
+
414
+ # set up local variable for user pipeline
415
+ user_pipeline = []
416
+
417
+ # check if the user has already been analyzed
418
+ if not self.tenant_pipeline[threads[i]].analyzed:
419
+ # change the user's status to analyzed
420
+ self.tenant_pipeline[threads[i]].analyzed = True
421
+
422
+ # start out in the beginning of the pipelines
423
+ # and iterate through all of their items
424
+ for pipe in pipelines:
425
+ # creating variable to store html table for the user's email
426
+ prelim_pipeline = []
427
+
428
+ # iterate through the items in the pipeline category while
429
+ # creating legible table headers
430
+ for item in pipelines[pipe]["Pipeline"]:
431
+ # flatten the dict to remove nested dictionaries
432
+ item = flatten_dict(item)
433
+
434
+ # create list variable to store the renamed column names
435
+ headers = []
436
+ # iterate through all columns for the item and see if the header
437
+ # has to be changed to Title Case and if the data has to revalued
438
+ for key in item.keys():
439
+ # change the camelcase header to a Title Case Header
440
+ fixed_key = uncamel_case(key)
441
+
442
+ # check the keys to revalue the data accordingly
443
+ if key.lower() == "uuid" or (pipe.lower() == "workflow" and key.lower() == "id"):
444
+ # create html url using data for the html table
445
+ href = f'{config["domain"]}/form/{pipe.lower().replace(" ", "")}/{item["id"]}'
446
+ # have to add an if clause for mso to display the view button correctly
447
+ url = (
448
+ '<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"'
449
+ f'xmlns:w="urn:schemas-microsoft-com:office:word" href="{href}" '
450
+ 'style="height:40px;v-text-anchor:middle;width:60px;" arcsize="5%" '
451
+ 'strokecolor="#22C2DC" fillcolor="#1DC3EB"><w:anchorlock/><center'
452
+ ' style="color:#ffffff;font-family:Roboto, Arial, sans-serif;font'
453
+ '-size:14px;">View</center></v:roundrect><![endif]-->'
454
+ )
455
+ url += f'<a href="{href}" style="mso-hide:all;">View</a>'
456
+
457
+ headers.append("Action")
458
+ if pipe.lower() == "workflow":
459
+ update_dict = {"UUID": url}
460
+ item = {**update_dict, **item}
461
+ headers.append("ID")
462
+ else:
463
+ # replace the UUID with the HTML url
464
+ item[key] = url
465
+ elif ("ById" in key or "ownerid" in key.lower() or key.lower() in id_fields) and item[key]:
466
+ # remove ById from the key
467
+ new_key = key.replace("Id", "")
468
+
469
+ # uncamel_case() the key
470
+ new_key = uncamel_case(new_key)
471
+
472
+ # replace the user id string with a user's name
473
+ user_id = item[key]
474
+ try:
475
+ # try to replace the ID with a user from all active users
476
+ item[key] = f'{users[user_id]["firstName"]} {users[user_id]["lastName"]}'
477
+ except KeyError:
478
+ # means the user is not activated, fetch them via API
479
+ user = User.get_user_by_id(user_id)
480
+ item[key] = f"{user.firstName} {user.lastName}"
481
+ # add the updated key to the table headers
482
+ headers.append(new_key)
483
+ elif key.lower() == "atlasmodule":
484
+ headers.append("Parent Module")
485
+ elif ("date" in key.lower() or "finish" in key.lower()) and item[key]:
486
+ try:
487
+ # convert string to a date & reformat the date to a legible string
488
+ item[key] = reformat_str_date(item[key], "%b %d, %Y")
489
+ except ValueError:
490
+ headers.append(fixed_key)
491
+ continue
492
+ # append the Title Case header to the headers list
493
+ headers.append(fixed_key)
494
+ elif key == "id":
495
+ # change the key to all uppercase
496
+ headers.append(key.upper())
497
+ elif isinstance(item[key], str) and "<" in item[key]:
498
+ # replace </br> with \n
499
+ text = item[key].replace("</br>", "\n")
500
+
501
+ # strip other html codes from string values
502
+ item[key] = re.sub("<[^<]+?>", "", text)
503
+
504
+ # append the Title Case header to headers
505
+ headers.append(fixed_key)
506
+ elif key.lower() == "currentstep":
507
+ item[key] += 1
508
+ headers.append(fixed_key)
509
+ elif key.lower() == "workflowinstancesteps":
510
+ del item[key]
511
+ else:
512
+ headers.append(fixed_key)
513
+ # add it to the final pipeline for the user
514
+ prelim_pipeline.append(item)
515
+ # check to see if there is an item for the bucket before
516
+ # appending it to the self.final_pipeline for the email
517
+ if len(prelim_pipeline) > 0:
518
+ # convert the item to a pandas data table
519
+ data = pd.DataFrame(prelim_pipeline)
520
+
521
+ # replace the columns with our legible data headers
522
+ data.columns = headers
523
+
524
+ # append the data item and bucket to our local user_pipeline list
525
+ user_pipeline.append({"bucket": pipe, "items": data})
526
+ # add the user's pipeline data to the self.pipeline for the emails
527
+ self.final_pipeline.append(
528
+ Pipeline(
529
+ email=self.tenant_pipeline[threads[i]].email,
530
+ fullName=self.tenant_pipeline[threads[i]].fullName,
531
+ pipelines=user_pipeline,
532
+ totalTasks=self.tenant_pipeline[threads[i]].totalTasks,
533
+ analyzed=True,
534
+ )
535
+ )
536
+ self.job_progress.update(task, advance=1)
537
+
538
+ def format_and_email(self, args: Tuple, thread: int) -> None:
539
+ """
540
+ Function to email all users with an HTML formatted email
541
+
542
+ :param Tuple args: Tuple of args to use during the process
543
+ :param int thread: Thread number of current thread
544
+ :rtype: None
545
+ """
546
+ # set up my args from the args tuple
547
+ import pandas as pd # Optimize import performance
548
+
549
+ api, config, task = args
550
+
551
+ threads = thread_assignment(
552
+ thread=thread,
553
+ total_items=len(self.final_pipeline),
554
+ )
555
+
556
+ # update api pool limits to max_thread count from init.yaml
557
+ api.pool_connections = config["maxThreads"]
558
+ api.pool_maxsize = config["maxThreads"]
559
+
560
+ # get assigned threads
561
+ for i in range(len(threads)):
562
+ # get the user's pipeline details
563
+ email = self.final_pipeline[threads[i]].email
564
+ total_tasks = self.final_pipeline[threads[i]].totalTasks
565
+
566
+ # create list to store the html tables
567
+ tables = []
568
+
569
+ # see if the user has been emailed already
570
+ if not self.final_pipeline[threads[i]].emailed:
571
+ # set the emailed flag to true
572
+ self.final_pipeline[threads[i]].emailed = True
573
+
574
+ # iterate through all items in self.final_pipeline to
575
+ # set up data tables as a html tables using pandas
576
+ for item in self.final_pipeline[threads[i]].pipelines:
577
+ tables.extend(
578
+ (
579
+ f'<h1>{item["bucket"]}</h1>',
580
+ item["items"].to_html(justify="left", index=False).replace('border="1"', 'border="0"'),
581
+ )
582
+ )
583
+ # join all the items in tables and separate them all with a </br> tag
584
+ tables = "</br>".join(tables)
585
+
586
+ # fix any broken html tags
587
+ tables = tables.replace("&amp;", "&").replace("&gt;", ">").replace("&lt;", "<").replace("’", "'")
588
+
589
+ # create email payload
590
+ email_notification = Email(
591
+ fromEmail="Support@RegScale.com",
592
+ emailSenderId=config["userId"],
593
+ to=email,
594
+ subject=f"RegScale Reminder: {total_tasks} Upcoming Items",
595
+ body=get_css("email_style.css") + tables,
596
+ )
597
+
598
+ # send the email and get the response
599
+ if email_notification.send():
600
+ emailed = True
601
+ else:
602
+ emailed = False
603
+
604
+ # set up dict to use for pandas data
605
+ data = {
606
+ "Email Address": '<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"'
607
+ 'xmlns:w="urn:schemas-microsoft-com:office:word" href="mailto:'
608
+ f'{email}"style="height:auto;v-text-anchor:middle;mso-width-'
609
+ 'percent:150;" arcsize="5%" strokecolor="#22C2DC" fillcolor='
610
+ '"#1DC3EB"><w:anchorlock/><center style="color:#ffffff;font-'
611
+ f'family:Roboto, Arial, sans-serif;font-size:14px;">{email}'
612
+ '</center></v:roundrect><![endif]--><a href="mailto:'
613
+ f'{email}" style="mso-hide:all;">{email}</a>',
614
+ "User Name": self.final_pipeline[threads[i]].fullName,
615
+ "Total Tasks": total_tasks,
616
+ "Emailed": emailed,
617
+ }
618
+ table = pd.DataFrame([data])
619
+ self.emails.append(table)
620
+ self.job_progress.update(task, advance=1)