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,873 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Module to allow user to make changes to Assessments in an Excel
4
+ spreadsheet for user-friendly experience"""
5
+
6
+ # standard python imports
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ import pandas as pd # Type Checking
11
+ from regscale.core.app.api import Api
12
+ from regscale.core.app.application import Application
13
+
14
+ import math
15
+ import os
16
+ import shutil
17
+ from pathlib import Path
18
+ from typing import Optional, Union, Any
19
+
20
+ import click
21
+
22
+ from openpyxl import Workbook, load_workbook
23
+ from openpyxl.styles import Protection, Font, NamedStyle
24
+ from openpyxl.worksheet.datavalidation import DataValidation
25
+ from openpyxl.worksheet.worksheet import Worksheet
26
+
27
+ from regscale.core.app.logz import create_logger
28
+ from regscale.core.app.utils.app_utils import (
29
+ check_file_path,
30
+ error_and_exit,
31
+ reformat_str_date,
32
+ get_user_names,
33
+ get_current_datetime,
34
+ check_empty_nan,
35
+ )
36
+ from regscale.models.app_models.click import regscale_id, regscale_module
37
+ from regscale.models.regscale_models import Assessment
38
+ from regscale.models.regscale_models.modules import Modules
39
+
40
+ ALL_ASSESSMENTS = "all_assessments.xlsx"
41
+ NEW_ASSESSMENTS = "new_assessments.xlsx"
42
+ OLD_ASSESSMENTS = "old_assessments.xlsx"
43
+ DIFFERENCES_FILE = "differences.txt"
44
+ SELECT_PROMT = "Please select an option from the dropdown list."
45
+ DATE_ENTRY_PROMPT = "Please enter a valid date in the following format: mm/dd/yyyy"
46
+ SELECTION_ERROR = "Your entry is not one of the available options."
47
+ INVALID_ENTRY_ERROR = "Your entry is not a valid option."
48
+ INVALID_ENTRY_TITLE = "Invalid Entry"
49
+
50
+
51
+ @click.group(name="assessments")
52
+ def assessments():
53
+ """
54
+ Performs actions on Assessments CLI Feature to create new or update assessments to RegScale.
55
+ """
56
+
57
+
58
+ # Make Empty Spreadsheet for creating new assessments.
59
+ @assessments.command(name="generate_new_file")
60
+ @click.option(
61
+ "--path",
62
+ type=click.Path(exists=False, dir_okay=True, path_type=Path),
63
+ help="Provide the desired path for excel files to be generated into.",
64
+ default=os.path.join(os.getcwd(), "artifacts"),
65
+ required=True,
66
+ )
67
+ def generate_new_file(path: Path):
68
+ """This function will build an Excel spreadsheet for users to be
69
+ able to create new assessments."""
70
+ new_assessment(path)
71
+
72
+
73
+ def new_assessment(path: Path) -> None:
74
+ """
75
+ Function to build Excel spreadsheet for creation of new assessments
76
+
77
+ :param Path path: directory of file location
78
+ :rtype: None
79
+ """
80
+ import pandas as pd # Optimize import performance
81
+
82
+ logger = create_logger()
83
+
84
+ check_file_path(path)
85
+
86
+ # create excel file and setting formatting
87
+
88
+ workbook = Workbook()
89
+ worksheet = workbook.active
90
+ worksheet.title = "New_Assessments"
91
+
92
+ column_headers = [
93
+ "Title",
94
+ "LeadAssessor",
95
+ "Facility",
96
+ "Organization",
97
+ "AssessmentType",
98
+ "PlannedStart",
99
+ "PlannedFinish",
100
+ "Status",
101
+ "ActualFinish",
102
+ "AssessmentResult",
103
+ "ParentId",
104
+ "ParentModule",
105
+ ]
106
+ for col, val in enumerate(column_headers, start=1):
107
+ worksheet.cell(row=1, column=col).value = val
108
+
109
+ for col in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]:
110
+ for cell in worksheet[col]:
111
+ if cell.row == 1:
112
+ cell.font = Font(bold=True)
113
+
114
+ # create and format reference worksheets for dropdowns
115
+ workbook.create_sheet(title="Facilities")
116
+ workbook.create_sheet(title="Organizations")
117
+ workbook.create_sheet(title="Accounts")
118
+ workbook.create_sheet(title="Modules")
119
+ workbook.create_sheet(title="AssessmentTypes")
120
+ workbook.create_sheet(title="Assessment_Ids")
121
+
122
+ workbook.save(filename=path / NEW_ASSESSMENTS)
123
+
124
+ # pull in Facility, Organization, Module, and Account Usernames into Excel Spreadsheet to create drop downs
125
+ list_of_modules = Modules().api_names()
126
+ module_names = pd.DataFrame(list_of_modules, columns=["name"])
127
+ with pd.ExcelWriter(
128
+ path / NEW_ASSESSMENTS,
129
+ mode="a",
130
+ engine="openpyxl",
131
+ if_sheet_exists="overlay",
132
+ ) as writer:
133
+ get_field_names(field_name="facilities").to_excel(
134
+ writer,
135
+ sheet_name="Facilities",
136
+ index=False,
137
+ )
138
+ get_field_names(field_name="organizations").to_excel(
139
+ writer,
140
+ sheet_name="Organizations",
141
+ index=False,
142
+ )
143
+ get_user_names().to_excel(
144
+ writer,
145
+ sheet_name="Accounts",
146
+ index=False,
147
+ )
148
+ module_names.to_excel(
149
+ writer,
150
+ sheet_name="Modules",
151
+ index=False,
152
+ )
153
+ get_assessment_types().to_excel(
154
+ writer,
155
+ sheet_name="AssessmentTypes",
156
+ index=False,
157
+ )
158
+
159
+ # Creating data Validation for fields
160
+ workbook = load_workbook(os.path.join(path.absolute(), NEW_ASSESSMENTS))
161
+ worksheet = workbook.active
162
+ # lock worksheets containing data for dropdowns
163
+ for sheet in [
164
+ "Facilities",
165
+ "Accounts",
166
+ "Organizations",
167
+ "AssessmentTypes",
168
+ "Modules",
169
+ ]:
170
+ workbook[sheet].protection.sheet = True
171
+ # Data structure for variable elements
172
+ data_validations_info = [
173
+ {"sheet": "Accounts", "columns": ["B"], "allow_blank": False},
174
+ {"sheet": "Facilities", "columns": ["C"], "allow_blank": True},
175
+ {"sheet": "Organizations", "columns": ["D"], "allow_blank": True},
176
+ {"sheet": "AssessmentTypes", "columns": ["E"], "allow_blank": False},
177
+ {"sheet": "Modules", "columns": ["L"], "allow_blank": True},
178
+ {
179
+ "formula1": '"Scheduled, In Progress, Complete, Cancelled"',
180
+ "columns": ["H"],
181
+ "allow_blank": True,
182
+ },
183
+ {
184
+ "formula1": '"Pass, Fail, N/A, Partial Pass"',
185
+ "columns": ["J"],
186
+ "allow_blank": True,
187
+ },
188
+ {"type": "date", "columns": ["F", "G"], "allow_blank": False},
189
+ {"type": "date", "columns": ["I"], "allow_blank": True},
190
+ ]
191
+ # Create data validations
192
+ create_data_validations(
193
+ data_validations_info=data_validations_info,
194
+ workbook=workbook,
195
+ worksheet=worksheet,
196
+ )
197
+ workbook.save(filename=os.path.join(path.absolute(), NEW_ASSESSMENTS))
198
+
199
+ # Freezing top row and adding data style to date columns to assure validation
200
+
201
+ workbook = load_workbook(os.path.join(path.absolute(), NEW_ASSESSMENTS))
202
+ worksheet = workbook.active
203
+ freeze_range = worksheet.cell(2, 14)
204
+ worksheet.freeze_panes = freeze_range
205
+ date_style = NamedStyle(name="date_style", number_format="mm/dd/yyyy")
206
+ workbook.add_named_style(date_style)
207
+
208
+ for col in ["F", "G", "I"]: # Columns to edit
209
+ for cell in worksheet[col]:
210
+ if cell.row > 1:
211
+ cell.style = date_style
212
+
213
+ # Adjusting width of columns
214
+ adjust_column_widths_and_styles(worksheet)
215
+
216
+ workbook.save(filename=path / NEW_ASSESSMENTS)
217
+
218
+ logger.info(
219
+ "Your excel workbook has been created. Please open the new_assessments workbook and add new assessments."
220
+ )
221
+ return None
222
+
223
+
224
+ @assessments.command(name="generate")
225
+ @regscale_id()
226
+ @regscale_module()
227
+ @click.option(
228
+ "--path",
229
+ type=click.Path(exists=False, dir_okay=True, path_type=Path),
230
+ help="Provide the desired path for excel files to be generated into.",
231
+ default=os.path.join(os.getcwd(), "artifacts"),
232
+ required=True,
233
+ )
234
+ def generate(regscale_id: int, regscale_module: str, path: Path):
235
+ """
236
+ This function will build and populate a spreadsheet of all assessments
237
+ with the selected RegScale Parent Id and RegScale Module for users to any necessary edits.
238
+ """
239
+ all_assessments(parent_id=regscale_id, parent_module=regscale_module, path=path)
240
+
241
+
242
+ def all_assessments(parent_id: int, parent_module: str, path: Path) -> None:
243
+ """Function takes organizer record and module and build excel worksheet of assessments
244
+
245
+ :param int parent_id: RegScale Parent Id
246
+ :param str parent_module: RegScale Parent Module
247
+ :param Path path: directory of file location
248
+ :rtype: None
249
+ """
250
+ import pandas as pd # Optimize import performance
251
+ from regscale.core.app.application import Application
252
+
253
+ app = Application()
254
+ existing_assessment_data = Assessment.fetch_all_assessments_by_parent(
255
+ app=app,
256
+ parent_id=parent_id,
257
+ parent_module=parent_module,
258
+ org_and_facil=True,
259
+ )
260
+ if (
261
+ existing_assessment_data["assessments"]["totalCount"] > 0
262
+ ): # Checking to see if assessment exists for selected RegScale Id and RegScale Module.
263
+ check_file_path(path)
264
+ sheet_names = ["Facilities", "Organizations", "Accounts", "AssessmentTypes"]
265
+
266
+ all_assessments_wb = path / ALL_ASSESSMENTS
267
+ old_assessments_wb = path / OLD_ASSESSMENTS
268
+
269
+ # Loading data from db into two workbooks.
270
+ workbook = Workbook()
271
+ worksheet = workbook.active
272
+ worksheet.title = f"Assessments({parent_id}_{parent_module})"
273
+ for worksheet in sheet_names:
274
+ workbook.create_sheet(title=worksheet)
275
+ workbook.save(filename=path / ALL_ASSESSMENTS)
276
+ shutil.copy(
277
+ all_assessments_wb,
278
+ old_assessments_wb,
279
+ )
280
+ assessments_data = [
281
+ [
282
+ a["id"],
283
+ a["title"],
284
+ f"{a['leadAssessor']['lastName'].strip()}, {a['leadAssessor']['firstName'].strip()} "
285
+ + f"({a['leadAssessor']['userName'].strip()})",
286
+ a["facility"]["name"] if a["facility"] else None,
287
+ a["org"]["name"] if a["org"] else None,
288
+ a["assessmentType"],
289
+ reformat_str_date(a["plannedStart"]),
290
+ reformat_str_date(a["plannedFinish"]),
291
+ a["status"],
292
+ reformat_str_date(a["actualFinish"]) if a["actualFinish"] else None,
293
+ a["assessmentResult"] or None,
294
+ a["parentId"],
295
+ a["parentModule"],
296
+ ]
297
+ for a in existing_assessment_data["assessments"]["items"]
298
+ ]
299
+
300
+ all_ass_df = pd.DataFrame(
301
+ assessments_data,
302
+ columns=[
303
+ "Id",
304
+ "Title",
305
+ "LeadAssessor",
306
+ "Facility",
307
+ "Organization",
308
+ "AssessmentType",
309
+ "PlannedStart",
310
+ "PlannedFinish",
311
+ "Status",
312
+ "ActualFinish",
313
+ "AssessmentResult",
314
+ "ParentId",
315
+ "ParentModule",
316
+ ],
317
+ )
318
+
319
+ with pd.ExcelWriter(
320
+ all_assessments_wb,
321
+ mode="a",
322
+ engine="openpyxl",
323
+ if_sheet_exists="overlay",
324
+ ) as writer:
325
+ all_ass_df.to_excel(
326
+ writer,
327
+ sheet_name=f"Assessments({parent_id}_{parent_module})",
328
+ index=False,
329
+ )
330
+ with pd.ExcelWriter(
331
+ old_assessments_wb,
332
+ mode="a",
333
+ engine="openpyxl",
334
+ if_sheet_exists="overlay",
335
+ ) as writer:
336
+ all_ass_df.to_excel(
337
+ writer,
338
+ sheet_name=f"Assessments({parent_id}_{parent_module})",
339
+ index=False,
340
+ )
341
+
342
+ # Pulling in Facility Names into Excel Spreadsheet to create dropdown.
343
+ with pd.ExcelWriter(
344
+ all_assessments_wb,
345
+ mode="a",
346
+ engine="openpyxl",
347
+ if_sheet_exists="overlay",
348
+ ) as writer:
349
+ get_field_names(field_name="facilities").to_excel(
350
+ writer,
351
+ sheet_name="Facilities",
352
+ index=False,
353
+ )
354
+ get_field_names(field_name="organizations").to_excel(
355
+ writer,
356
+ sheet_name="Organizations",
357
+ index=False,
358
+ )
359
+ get_user_names().to_excel(
360
+ writer,
361
+ sheet_name="Accounts",
362
+ index=False,
363
+ )
364
+ get_assessment_types().to_excel(
365
+ writer,
366
+ sheet_name="AssessmentTypes",
367
+ index=False,
368
+ )
369
+
370
+ # Adding protection to OLD_ASSESSMENTS_WB file that will be used as reference.
371
+ workbook2 = load_workbook(old_assessments_wb)
372
+ worksheet2 = workbook2.active
373
+ worksheet2.protection.sheet = True
374
+ workbook2.save(filename=old_assessments_wb)
375
+
376
+ # Adding Data Validation to ALL_ASSESSMENTS_WB file to be adjusted internally.
377
+ workbook = load_workbook(all_assessments_wb)
378
+ worksheet = workbook.active
379
+ # lock worksheets containing data for dropdowns
380
+ for sheet in sheet_names:
381
+ workbook[sheet].protection.sheet = True
382
+
383
+ data_validations_info = [
384
+ {"sheet": "Accounts", "columns": ["C"], "allow_blank": False},
385
+ {"sheet": "Facilities", "columns": ["D"], "allow_blank": True},
386
+ {"sheet": "Organizations", "columns": ["E"], "allow_blank": True},
387
+ {"sheet": "AssessmentTypes", "columns": ["F"], "allow_blank": False},
388
+ {
389
+ "formula1": '"Scheduled, In Progress, Complete, Cancelled"',
390
+ "columns": ["I"],
391
+ "allow_blank": True,
392
+ },
393
+ {
394
+ "formula1": '"Pass, Fail, N/A, Partial Pass"',
395
+ "columns": ["K"],
396
+ "allow_blank": True,
397
+ },
398
+ {"type": "date", "columns": ["G", "H"], "allow_blank": False},
399
+ {"type": "date", "columns": ["J"], "allow_blank": True},
400
+ ]
401
+ create_data_validations(
402
+ data_validations_info=data_validations_info,
403
+ workbook=workbook,
404
+ worksheet=worksheet,
405
+ )
406
+
407
+ # Worksheet freeze top row
408
+ freeze_range = worksheet.cell(2, 17)
409
+ worksheet.freeze_panes = freeze_range
410
+ date_style = NamedStyle(name="date_style", number_format="mm/dd/yyyy")
411
+ workbook.add_named_style(date_style)
412
+
413
+ # Adding Date Style to Worksheet, formatting cells, and unlocking
414
+ # cells that can be edited in each assessment
415
+ adjust_column_widths_and_styles(
416
+ worksheet=worksheet,
417
+ editable_columns=["C", "D", "E", "F", "G", "H", "I", "J", "K"],
418
+ date_columns=["G", "H", "J"],
419
+ date_col_style=date_style,
420
+ )
421
+ workbook.save(filename=all_assessments_wb)
422
+
423
+ return app.logger.info(
424
+ "Your data has been loaded into your excel workbook. Please open the all_assessments workbook "
425
+ "and make your desired changes."
426
+ )
427
+ else:
428
+ app.logger.info("Please check your selections for RegScale Id and RegScale Module and try again.")
429
+ error_and_exit(
430
+ "There was an error creating your workbook. No assessments exist for the given RegScale Id "
431
+ "and RegScale Module."
432
+ )
433
+
434
+
435
+ @assessments.command(name="load")
436
+ @click.option(
437
+ "--path",
438
+ type=click.Path(exists=False, dir_okay=True, path_type=Path),
439
+ help="Provide the desired path of excel workbook locations.",
440
+ default=os.path.join(os.getcwd(), "artifacts"),
441
+ required=True,
442
+ )
443
+ def load(path: Path) -> None:
444
+ """
445
+ This function uploads updated assessments and new assessments to
446
+ RegScale from the Excel files that users have edited.
447
+ """
448
+ upload_data(path=path)
449
+
450
+
451
+ def upload_data(path: Path) -> None:
452
+ """
453
+ Function will upload assessments to RegScale if user as made edits to any
454
+ of the assessment excel workbooks
455
+
456
+ :param Path path: directory of file location
457
+ :rtype: None
458
+ """
459
+ import pandas as pd # Optimize import performance
460
+ import numpy as np # Optimize import performance
461
+ from regscale.core.app.application import Application
462
+
463
+ app = Application()
464
+ new_assessments_wb = path / NEW_ASSESSMENTS
465
+ all_assessments_wb = path / ALL_ASSESSMENTS
466
+ old_assessments_wb = path / OLD_ASSESSMENTS
467
+
468
+ # Checking if new assessments have been created and updating RegScale database.
469
+ if os.path.isfile(new_assessments_wb):
470
+ new_files = new_assessments_wb
471
+ new = map_workbook_to_dict(new_files)
472
+ new_assessments = [
473
+ Assessment(
474
+ leadAssessorId=value["LeadAssessorId"] or app.config["userId"],
475
+ title=value["Title"],
476
+ assessmentType=value["AssessmentType"],
477
+ plannedStart=map_pandas_timestamp(value["PlannedStart"]),
478
+ plannedFinish=map_pandas_timestamp(value["PlannedFinish"]),
479
+ status=value["Status"],
480
+ parentModule=check_empty_nan(value["ParentModule"]),
481
+ facilityId=check_empty_nan(value.get("FacilityId")),
482
+ orgId=check_empty_nan(value.get("OrganizationId")),
483
+ assessmentResult=check_assessment_result(value["AssessmentResult"]),
484
+ actualFinish=map_pandas_timestamp(value["ActualFinish"]),
485
+ parentId=check_empty_nan(value["ParentId"]),
486
+ lastUpdatedById=app.config["userId"],
487
+ dateLastUpdated=get_current_datetime(),
488
+ ).create()
489
+ for value in new.values()
490
+ ]
491
+ post_and_save_assessments(
492
+ app=app,
493
+ new_assessments=new_assessments,
494
+ workbook_path=path,
495
+ )
496
+ else:
497
+ app.logger.info("No new assessments detected. Checking for edited assessments")
498
+
499
+ if os.path.isfile(all_assessments_wb):
500
+ # Checking all_assessments file for differences before updating database
501
+
502
+ df1 = pd.read_excel(old_assessments_wb, sheet_name=0, index_col="Id")
503
+
504
+ df2 = pd.read_excel(all_assessments_wb, sheet_name=0, index_col="Id")
505
+
506
+ if df1.equals(df2):
507
+ error_and_exit("No differences detected.")
508
+
509
+ else:
510
+ app.logger.warning("Differences found!")
511
+
512
+ diff_mask = (df1 != df2) & ~(df1.isnull() & df2.isnull())
513
+ ne_stacked = diff_mask.stack()
514
+ changed = ne_stacked[ne_stacked]
515
+ changed.index.names = ["Id", "Column"]
516
+ difference_locations = np.where(diff_mask)
517
+ changed_from = df1.values[difference_locations]
518
+ changed_to = df2.values[difference_locations]
519
+ changes = pd.DataFrame({"From": changed_from, "To": changed_to}, index=changed.index)
520
+ changes.to_csv(
521
+ path / DIFFERENCES_FILE,
522
+ header=True,
523
+ index=True,
524
+ sep=" ",
525
+ mode="w+",
526
+ )
527
+ app.logger.info(
528
+ "Please check differences.txt file located in %s to see changes made.",
529
+ path,
530
+ )
531
+ # Loading in differences.txt file and using Id to parse xlsx file for rows to update
532
+
533
+ diff = pd.read_csv(path / DIFFERENCES_FILE, header=0, sep=" ", index_col=None)
534
+ ids = []
535
+
536
+ for _, row in diff.iterrows():
537
+ ids.append(row["Id"])
538
+
539
+ id_df = pd.DataFrame(ids, index=None, columns=["Id"])
540
+ id_df2 = id_df.drop_duplicates()
541
+ updated_files = all_assessments_wb
542
+ df3 = pd.read_excel(updated_files, sheet_name=0, index_col=None)
543
+ updated = df3[df3["Id"].isin(id_df2["Id"])]
544
+ updated = map_workbook_to_dict(updated_files, updated)
545
+ _ = [
546
+ Assessment(
547
+ leadAssessorId=value["LeadAssessorId"],
548
+ id=value["Id"],
549
+ title=value["Title"],
550
+ assessmentType=value["AssessmentType"],
551
+ plannedStart=value["PlannedStart"],
552
+ plannedFinish=value["PlannedFinish"],
553
+ status=value["Status"],
554
+ parentModule=value["ParentModule"],
555
+ facilityId=check_empty_nan(value.get("FacilityId")),
556
+ orgId=check_empty_nan(value.get("OrganizationId")),
557
+ assessmentResult=check_assessment_result(value["AssessmentResult"]),
558
+ actualFinish=check_empty_nan(value["ActualFinish"]),
559
+ parentId=value["ParentId"],
560
+ lastUpdatedById=app.config["userId"],
561
+ dateLastUpdated=get_current_datetime(),
562
+ ).save(bulk=True)
563
+ for value in updated.values()
564
+ ]
565
+
566
+ Assessment.bulk_save()
567
+
568
+ else:
569
+ app.logger.info("No Assessments exist to load to RegScale.")
570
+ return app.logger.info(
571
+ "Assessment files have been uploaded. Changes made to existing files can be seen in "
572
+ "differences.txt file. Thank you!"
573
+ )
574
+
575
+
576
+ @assessments.command(name="delete_files")
577
+ @click.option(
578
+ "--path",
579
+ type=click.Path(exists=False, dir_okay=True, path_type=Path),
580
+ help="Provide the desired path of file location.",
581
+ default=Path("./artifacts"),
582
+ required=True,
583
+ )
584
+ def delete_files(path: Path):
585
+ """This command will delete files used during the Assessment editing process."""
586
+ delete_file(path)
587
+
588
+
589
+ def delete_file(path: Path) -> int:
590
+ """
591
+ Deletes files used during the process
592
+
593
+ :param Path path: directory of file location
594
+ :return: Number of files deleted
595
+ :rtype: int
596
+ """
597
+ logger = create_logger()
598
+ file_names = [
599
+ NEW_ASSESSMENTS,
600
+ ALL_ASSESSMENTS,
601
+ OLD_ASSESSMENTS,
602
+ DIFFERENCES_FILE,
603
+ ]
604
+ deleted_files = []
605
+
606
+ for file_name in file_names:
607
+ if os.path.isfile(path / file_name):
608
+ os.remove(path / file_name)
609
+ deleted_files.append(file_name)
610
+ else:
611
+ logger.warning("No %s file found. Checking for other files before exiting.", file_name)
612
+ logger.info("%i file(s) have been deleted: %s", len(deleted_files), ", ".join(deleted_files))
613
+ return len(deleted_files)
614
+
615
+
616
+ def get_maximum_rows(*, sheet_object: Any) -> int:
617
+ """
618
+ This function finds the last row containing data in a spreadsheet
619
+
620
+ :param Any sheet_object: excel worksheet to be referenced
621
+ :return: integer representing last row with data in spreadsheet
622
+ :rtype: int
623
+ """
624
+ return sum(any(col.value is not None for col in row) for max_row, row in enumerate(sheet_object, 1))
625
+
626
+
627
+ def get_field_names(field_name: str) -> "pd.DataFrame":
628
+ """
629
+ This function uses GraphQL to retrieve all names of a given parent table in database
630
+
631
+ :param str field_name: name of parent table to retrieve names from
632
+ :return: pandas dataframe with facility names
633
+ :rtype: pd.DataFrame
634
+ """
635
+ import pandas as pd # Optimize import performance
636
+ from regscale.core.app.api import Api
637
+
638
+ api = Api()
639
+
640
+ body = """
641
+ query {
642
+ field_name(skip: 0, take: 50, order: {name: ASC}, ) {
643
+ items {
644
+ name
645
+ id
646
+ }
647
+ totalCount
648
+ pageInfo {
649
+ hasNextPage
650
+ }
651
+ }
652
+ }
653
+ """.replace(
654
+ "field_name", field_name
655
+ )
656
+
657
+ field_items = api.graph(query=body)
658
+ names = field_items[str(field_name)]["items"]
659
+ field_names = [[i["name"], i["id"]] for i in names]
660
+ all_names = pd.DataFrame(field_names, index=None, columns=["name", "id"])
661
+
662
+ return all_names
663
+
664
+
665
+ def get_assessment_types() -> "pd.DataFrame":
666
+ """
667
+ This function uses GraphQL to retrieve all assessment types in database
668
+
669
+ :return: pandas dataframe with assessment types
670
+ :rtype: pd.DataFrame
671
+ """
672
+ import pandas as pd # Optimize import performance
673
+ from regscale.core.app.api import Api
674
+
675
+ api = Api()
676
+
677
+ body = """
678
+ query{
679
+ assessments (skip: 0, take: 50, order: {assessmentType: ASC}, ) {
680
+ items {
681
+ assessmentType
682
+ }
683
+ totalCount
684
+ pageInfo {
685
+ hasNextPage
686
+ }
687
+ }
688
+ } """
689
+
690
+ assessments_raw = api.graph(query=body)
691
+ assessment_types = assessments_raw["assessments"]["items"]
692
+ field_names = [i["assessmentType"] for i in assessment_types]
693
+ all_assessment_types = pd.DataFrame(field_names, index=None, columns=["assessmentType"])
694
+ return all_assessment_types.drop_duplicates()
695
+
696
+
697
+ def check_assessment_result(value: Any) -> Union[str, float]:
698
+ """
699
+ This function takes a given value for an assessment and
700
+ checks if value is empty or NaN based on value type.
701
+
702
+ :param Any value: A string or float object
703
+ :return: A string value, float value. or ""
704
+ :rtype: Union[str, float]
705
+ """
706
+ # this function has to be checked separate to account for API
707
+ # only accepting empty string unlike other class params
708
+ if isinstance(value, str) and value.strip() == "":
709
+ return ""
710
+ if isinstance(value, float) and math.isnan(value):
711
+ return ""
712
+ return value
713
+
714
+
715
+ def adjust_column_widths_and_styles(
716
+ worksheet: Worksheet,
717
+ editable_columns: Optional[list[str]] = None,
718
+ date_columns: Optional[list[str]] = None,
719
+ date_col_style: Optional[NamedStyle] = None,
720
+ ) -> None:
721
+ """
722
+ Function to adjust column widths based on length of data in column, and apply
723
+ styles to specific columns and rows
724
+
725
+ :param Worksheet worksheet: Worksheet to adjust column widths for
726
+ :param Optional[list[str]] editable_columns: List of rows to unlock for editing
727
+ :param Optional[list[str]] date_columns: List of columns to add date style to
728
+ :param Optional[NamedStyle] date_col_style: NamedStyle object to apply to date columns, defaults to None
729
+ :rtype: None
730
+ """
731
+ editable_columns = editable_columns or []
732
+ date_columns = date_columns or []
733
+ for col in worksheet.columns:
734
+ max_length = 0
735
+ column_letter = col[0].column_letter
736
+
737
+ for cell in col:
738
+ # Determine max length for column width
739
+ cell_length = len(str(cell.value))
740
+ max_length = max(max_length, cell_length)
741
+
742
+ # Set cell protection for specific columns
743
+ if column_letter in editable_columns and cell.row > 1:
744
+ cell.protection = Protection(locked=False)
745
+
746
+ # Apply date style for specific columns and rows
747
+ if column_letter in date_columns and cell.row > 1 and date_col_style:
748
+ cell.style = date_col_style
749
+
750
+ # Set adjusted column width
751
+ adjusted_width = (max_length + 2) * 1.2
752
+ worksheet.column_dimensions[column_letter].width = adjusted_width
753
+
754
+
755
+ def create_data_validations(data_validations_info: list[dict], workbook: Workbook, worksheet: Worksheet) -> None:
756
+ """
757
+ Function to create data validations for excel worksheet
758
+
759
+ :param list[dict] data_validations_info: List containing dictionaries with
760
+ information for data validations
761
+ :param Workbook workbook: Workbook object to add data validations to
762
+ :param Worksheet worksheet: The worksheet object to add data validations to
763
+ :rtype: None
764
+ """
765
+ for _, dv_info in enumerate(data_validations_info, start=1):
766
+ formula1 = dv_info.get("formula1")
767
+ if sheet_name := dv_info.get("sheet"):
768
+ formula1 = f"={sheet_name}!$A$2:$A${str(get_maximum_rows(sheet_object=workbook[sheet_name]))}"
769
+
770
+ data_validation = DataValidation(
771
+ type=dv_info.get("type", "list"),
772
+ formula1=formula1,
773
+ allow_blank=dv_info.get("allow_blank", True),
774
+ showDropDown=False,
775
+ error=(SELECTION_ERROR if dv_info.get("type", "list") == "list" else INVALID_ENTRY_ERROR),
776
+ errorTitle=INVALID_ENTRY_TITLE,
777
+ prompt=(SELECT_PROMT if dv_info.get("type", "list") == "list" else DATE_ENTRY_PROMPT),
778
+ showErrorMessage=True if dv_info.get("type", "date") else None,
779
+ showInputMessage=True if dv_info.get("type", "date") else None,
780
+ )
781
+
782
+ worksheet.add_data_validation(data_validation)
783
+ for column in dv_info["columns"]:
784
+ data_validation.add(f"{column}2:{column}1048576")
785
+
786
+
787
+ def post_and_save_assessments(app: "Application", new_assessments: list[Assessment], workbook_path: Path) -> None:
788
+ """
789
+ Function to post new assessments to RegScale and save assessment ids to excel workbook
790
+
791
+ :param Application app: RegScale CLI Application object
792
+ :param list[Assessment] new_assessments: List of new assessments to post to RegScale
793
+ :param Path workbook_path: Path to workbook to save assessment ids to
794
+ :rtype: None
795
+ """
796
+ import pandas as pd # Optimize import performance
797
+
798
+ new_assessments_df = pd.DataFrame([assessment.id for assessment in new_assessments], columns=["id_number"])
799
+ for file_name in [NEW_ASSESSMENTS, ALL_ASSESSMENTS]:
800
+ with pd.ExcelWriter(
801
+ workbook_path / file_name,
802
+ mode="a",
803
+ engine="openpyxl",
804
+ if_sheet_exists="overlay",
805
+ ) as writer:
806
+ new_assessments_df.to_excel(
807
+ writer,
808
+ sheet_name="Assessment_Ids",
809
+ index=False,
810
+ )
811
+ app.logger.info(
812
+ "%i total assessment(s) were added to RegScale.",
813
+ len(new_assessments),
814
+ )
815
+
816
+
817
+ def map_pandas_timestamp(date_time: "pd.Timestamp") -> Optional[str]:
818
+ """
819
+ Function to map pandas timestamp to string
820
+
821
+ :param pd.Timestamp date_time: Timestamp to map to string
822
+ :return: String representation of pandas timestamp
823
+ :rtype: Optional[str]
824
+ """
825
+ import pandas as pd # Optimize import performance
826
+
827
+ if isinstance(date_time, float):
828
+ return None
829
+ elif date_time is not None and not pd.isna(date_time) and not isinstance(date_time, str):
830
+ return date_time.strftime("%Y-%m-%d %H:%M:%S")
831
+ else:
832
+ return date_time or None
833
+
834
+
835
+ def map_workbook_to_dict(file_path: str, workbook_data: Optional["pd.DataFrame"] = None) -> dict:
836
+ """
837
+ Function to map workbook to dictionary
838
+
839
+ :param str file_path: Path to workbook file
840
+ :param Optional[pd.DataFrame] workbook_data: Dataframe to map to dictionary
841
+ :return: Dictionary representation of workbook
842
+ :rtype: dict
843
+ """
844
+ import pandas as pd # Optimize import performance
845
+
846
+ if workbook_data is not None:
847
+ wb_data = workbook_data
848
+ else:
849
+ wb_data = pd.read_excel(file_path)
850
+ wb_data["Facility"] = wb_data["Facility"].astype(str).fillna("None") # Handle missing facilities
851
+ wb_data["Organization"] = wb_data["Organization"].astype(str).fillna("None") # Handle missing organizations
852
+
853
+ # Reading and preparing the 'Facilities' sheet
854
+ facilities = pd.read_excel(file_path, sheet_name="Facilities")
855
+ facilities = facilities.rename(columns={"name": "Facility", "id": "FacilityId"})
856
+ facilities["Facility"] = facilities["Facility"].astype(str) # Ensure consistent data type
857
+
858
+ # Reading and preparing the 'Organizations' sheet
859
+ organizations = pd.read_excel(file_path, sheet_name="Organizations")
860
+ organizations = organizations.rename(columns={"name": "Organization", "id": "OrganizationId"})
861
+ organizations["Organization"] = (
862
+ organizations["Organization"].astype(str).fillna("None")
863
+ ) # Handle missing organizations
864
+
865
+ # Reading and preparing the 'Accounts' sheet
866
+ accounts = pd.read_excel(file_path, sheet_name="Accounts")
867
+ accounts = accounts.rename(columns={"User": "LeadAssessor", "UserId": "LeadAssessorId"})
868
+
869
+ # Merging dataframes
870
+ wb_data = wb_data.merge(accounts, how="left", on="LeadAssessor", validate="many_to_many")
871
+ wb_data = wb_data.merge(facilities, how="left", on="Facility", validate="many_to_many")
872
+ wb_data = wb_data.merge(organizations, how="left", on="Organization", validate="many_to_many")
873
+ return wb_data.T.to_dict()